Merge master and reconcile Dot with the released MCP gateway

Reuse scoped browser and device consent, verified client metadata, signed event admissions and warm-standby gates. Preserve the dedicated agent resource and Runner authority. Regenerate the Dot-only migration after the merged gateway history and cover issuer, company, viewer and token isolation.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-06 17:47:24 -05:00
commit 1be3f69703
1212 files changed
+142427 -60490

No files matched your search

@@ -43,6 +43,29 @@ test("allows each current CODEOWNER on a feature branch; reads policy from maste
assert.equal(f.calls[0].path, ".github/CODEOWNERS");
}
});
test("owners listed only for release docs may initiate and rerun Storybook", async (t) => {
const triggeringActor = process.env.GITHUB_TRIGGERING_ACTOR;
t.after(() => { process.env.GITHUB_TRIGGERING_ACTOR = triggeringActor; });
const codeowners = `${ownerFile}doc/RELEASING.md @release-owner @release-reviewer\n`;
for (const actor of ["cryppadotta", "release-owner", "release-reviewer"]) {
for (const rerunner of ["cryppadotta", "release-owner", "release-reviewer"]) {
process.env.GITHUB_TRIGGERING_ACTOR = rerunner;
const f = fixture({ context: { actor }, codeowners });
await authorize(f);
assert.equal(f.calls[0].ref, "master");
}
}
});
test("removing an owner from CODEOWNERS revokes initiating and rerunning access", async (t) => {
const triggeringActor = process.env.GITHUB_TRIGGERING_ACTOR;
t.after(() => { process.env.GITHUB_TRIGGERING_ACTOR = triggeringActor; });
for (const actor of ["release-owner", "release-reviewer"]) {
process.env.GITHUB_TRIGGERING_ACTOR = "cryppadotta";
await assert.rejects(authorize(fixture({ context: { actor } })), /Only default-branch CODEOWNERS/);
process.env.GITHUB_TRIGGERING_ACTOR = actor;
await assert.rejects(authorize(fixture()), /Only default-branch CODEOWNERS/);
}
});
test("rejects non-owner initiators", async () => {
await assert.rejects(authorize(fixture({ context: { actor: "contributor" } })), /Only default-branch CODEOWNERS/);
});
+196 -8
View File
@@ -807,6 +807,91 @@ const apps = [
{ requiredResourceFilters: ["team", "project", "environment"] },
),
],
// Enterpret advertises RFC 9728 -> RFC 8414 discovery from its own 401
// challenge (issuer https://oauth.enterpret.com, PKCE S256, registration
// endpoint present, token_endpoint_auth_method "none"), so `defaults` ships
// `serverUrl` only and the broker resolves endpoints at connect time.
// Both methods target the official read-only Enterpret MCP. Enterpret Agent's
// beta write MCP is a separate service and is outside this connector's scope.
// OAuth scope reporting and revocation-cache fixes await provider deployment
// and fresh live validation; scope names alone do not prove write capability.
// The provider documents no customer-registered OAuth app, so OAuth stays
// `dcr` only rather than `["customer", "dcr"]`.
[
"enterpret",
"Enterpret",
"Ask questions about your customer feedback and pull verbatim quotes with citations.",
"analytics",
"enterpret.com",
["https://wisdom-api.enterpret.com/*"],
[
method(
"mcp-api-key",
"mcp_remote",
"api_key",
{ serverUrl: "https://wisdom-api.enterpret.com/server/mcp" },
"S3",
"Generate an auth token in Enterpret under Settings, Enterpret MCP, then paste it below. One token belongs to one Enterpret organization. This is the recommended connection method.",
{
label: "Use an auth token",
grantKinds: ["organization"],
whenToUse:
"Recommended. Use an organization auth token from Settings → Enterpret MCP. This is the primary, store-ready connection method.",
credentialFields: [
field(
"authorization",
"Enterpret auth token",
"Paste the token from Settings, Enterpret MCP",
),
],
keyPlacement: {
location: "header",
name: "Authorization",
prefix: "Bearer ",
},
consoleLinks: {
docs: "https://enterpret.support.site/article/enterpret-mcp-server",
},
warnings: [
"Check your auth token's expiry in Enterpret Settings > Enterpret MCP and replace it before it lapses.",
"This connection reads customer feedback, including verbatim quotes with speaker attribution.",
"run_graph_query starts as Ask first. Cypher is not established as read-only even when Enterpret advertises readOnlyHint.",
],
},
),
method(
"mcp-oauth",
"mcp_remote",
"oauth",
{
serverUrl: "https://wisdom-api.enterpret.com/server/mcp",
scopesHint: ["mcp:read"],
},
"S3",
"Sign in to Enterpret in the browser to query the official read-only MCP. Each person connects with their own Enterpret account, and Enterpret attributes their queries individually.",
{
label: "Sign in with Enterpret",
ownershipModes: ["dcr"],
grantKinds: ["user"],
whenToUse:
"Use browser sign-in when each person should query feedback under their own Enterpret account.",
consoleLinks: {
docs: "https://enterpret.support.site/article/enterpret-mcp-server",
},
warnings: [
"The official Enterpret MCP is read-only. Enterpret previously reported broader OAuth scopes, including mcp:write and email, than Paperclip requested. Enterpret is correcting this scope reporting; it does not establish access to the separate beta Agent MCP.",
"After revocation, Enterpret may cache token validity for up to 24 hours. Disconnect this connection to stop Paperclip access immediately. Enterpret is reducing this delay.",
"You need an Enterpret account with access to your organization's feedback.",
"This connection reads customer feedback, including verbatim quotes with speaker attribution.",
],
},
),
],
{
docsUrl: "https://enterpret.support.site/article/enterpret-mcp-server",
redirectConstraints: "https-or-loopback-http",
},
],
[
"anthropic",
"Anthropic",
@@ -945,6 +1030,7 @@ const categoryBySlug = {
miro: "productivity",
mixpanel: "analytics",
netlify: "developer",
neon: "data",
notion: "content",
oreilly: "content",
pagerduty: "developer",
@@ -1018,6 +1104,11 @@ const apiKeySpec = {
placeholder: "Paste your Kernel API key",
},
mem0: { name: "Authorization", prefix: "Bearer ", placeholder: "Paste your Mem0 API key" },
neon: {
name: "Authorization",
prefix: "Bearer ",
placeholder: "napi_... or neon_project_key_...",
},
oreilly: {
name: "Authorization",
prefix: "Bearer ",
@@ -1124,6 +1215,10 @@ const specialMethodsFor = (entry) => {
apiKeyMethodFor(entry, "mcp-api-key", entry.serverUrl, {
guidanceMd: `Open the ${entry.name} dashboard, create an API key for the account agents should use, and paste it below.`,
consoleLinks: { keys: entry.slug === "mem0" ? "https://app.mem0.ai/dashboard/api-keys" : "https://app.honcho.dev", docs: entry.docsUrl },
...(entry.slug === "honcho" ? { tenantFields: [{
key: "workspaceId", label: "Honcho workspace", type: "text", required: true,
placeholder: "Workspace ID", validation: { maxLength: 512 },
}] } : {}),
}),
];
if (entry.slug === "zep") return [oauthMethodFor(entry, "mcp-oauth", entry.serverUrl, {
@@ -1414,6 +1509,61 @@ const specialMethodsFor = (entry) => {
}),
];
}
if (entry.slug === "neon") {
// Neon's hosted server narrows itself with documented query options:
// `projectId` pins one project and `readonly=true` limits SQL to SELECT
// and schema inspection. Its repeatable `category` filter has no
// comma-joined form, so catalog narrowing stays with per-action policies.
const tenantFields = [
{
key: "projectId",
label: "Pin to project ID",
type: "text",
advanced: true,
placeholder: "Optional Neon project ID",
helperMd:
"Optional. Restrict this connection to one project. Copy the project ID from Neon Console → Project settings → General.",
validation: { pattern: "^[a-z0-9-]+$", maxLength: 64 },
transport: { location: "query", name: "projectId" },
},
{
key: "readOnly",
label: "Read-only mode",
type: "checkbox",
defaultValue: false,
helperMd:
"Enable this to limit SQL to SELECT queries and schema inspection.",
transport: {
location: "query",
name: "readonly",
format: "boolean",
omitFalse: true,
},
},
];
const warning =
"Neon recommends its hosted server for development and testing. Review write and destructive actions before execution.";
return [
oauthMethodFor(entry, "mcp-oauth", entry.serverUrl, {
guidanceMd:
"Connect Neon in the browser. Open Advanced to pin one project or enable read-only mode. Write tools start enabled and remain governed by Paperclip's action policies.",
tenantFields,
warnings: [entry.prerequisite, warning],
requiredResourceFilters: ["project"],
}),
apiKeyMethodFor(entry, "mcp-api-key", entry.serverUrl, {
guidanceMd:
"Use a customer-created Neon API key. Prefer a project-scoped key for one development project; personal and organization keys reach every project they can access. Write tools start enabled and remain governed by Paperclip's action policies.",
consoleLinks: {
keys: "https://console.neon.tech/app/settings/api-keys",
docs: entry.docsUrl,
},
tenantFields,
warnings: [entry.prerequisite, warning],
requiredResourceFilters: ["project"],
}),
];
}
if (entry.slug === "youcom") {
// You.com also serves a documented keyless profile at ?profile=free with a
// reduced read-only tool set. That is a real user choice: try web search
@@ -1515,7 +1665,7 @@ for (const entry of researchManifest.entries) {
schemaVersion: 1,
slug: entry.slug,
name: entry.name,
description: ({ mem0: "Remember preferences, conversations, events, and agent state.", zep: "Retrieve temporal graph memory and authorized business context.", supermemory: "Search and save shared memories, documents, and profiles.", honcho: "Remember conversations and retrieve context about peers." })[entry.slug] ?? (entry.slug === "fireflies"
description: ({ neon: "Manage Postgres projects and branches, run SQL, and inspect schemas in Neon.", mem0: "Remember preferences, conversations, events, and agent state.", zep: "Retrieve temporal graph memory and authorized business context.", supermemory: "Search and save shared memories, documents, and profiles.", honcho: "Remember conversations and retrieve context about peers." })[entry.slug] ?? (entry.slug === "fireflies"
? "Search meeting transcripts, read summaries and action items, and connect meeting-ready routines."
: `Connect ${entry.name}'s provider-hosted MCP server.`),
categories: [categoryBySlug[entry.slug] ?? "other"],
@@ -1653,13 +1803,36 @@ const inferState = (slug, state) => {
};
};
// Runtime credentials share the provider catalog, but never expose tool actions.
for (const [slug, name, subscription, envKey] of [["anthropic", "Claude", true, "ANTHROPIC_API_KEY"], ["openai", "OpenAI", true, "OPENAI_API_KEY"], ["openrouter", "OpenRouter", false, "OPENROUTER_API_KEY"], ["xai", "Grok", true, "XAI_API_KEY"]]) {
let app=apps.find(a=>a.slug===slug);
if(!app){app={schemaVersion:1,slug,name,description:`Connect ${name} accounts for your agents.`,categories:["ai"],branding:brandingFor(slug),urlPatterns:[{"openai":"https://api.openai.com/*","openrouter":"https://openrouter.ai/api/*","xai":"https://api.x.ai/*"}[slug]],methods:[]};apps.push(app);}
const methods=(subscription?["subscription","api_key"]:["api_key"]).map(authMethod=>({key:`ai-${authMethod}`,label:authMethod==="subscription"?`${name} subscription`:`${name} API key`,purpose:"ai",transport:"runtime_auth",auth:authMethod==="subscription"?"oauth":"api_key",ai:{provider:slug,method:authMethod},grantKinds:["user","organization"],ownershipModes:["customer"],whenToUse:"Authenticate an agent with this account.",guidanceMd:"Use your personal account or an explicitly shared company account.",riskTier:"S3",...(authMethod==="api_key"?{credentialFields:[field("apiKey","API key","Enter API key")],keyPlacement:{location:"env",name:envKey}}:{})}));
// Legacy REST entries have no tool execution adapter. Only offer the supported
// AI account flow; saved REST connections remain removable through Connections.
app.methods = [...methods, ...app.methods.filter(method => method.transport !== "rest_api")];
const aiCatalogEntries = [
{ slug: "anthropic", name: "Claude", provider: "anthropic", subscription: true, envKey: "ANTHROPIC_API_KEY" },
{ slug: "openai", name: "OpenAI", provider: "openai", subscription: true, envKey: "OPENAI_API_KEY", url: "https://api.openai.com/*" },
{ slug: "openrouter", name: "OpenRouter", provider: "openrouter", envKey: "OPENROUTER_API_KEY", url: "https://openrouter.ai/api/*" },
{ slug: "xai", name: "Grok", provider: "xai", subscription: true, envKey: "XAI_API_KEY", url: "https://api.x.ai/*" },
{ slug: "google", name: "Google Gemini", provider: "google", envKey: "GEMINI_API_KEY", url: "https://generativelanguage.googleapis.com/*" },
{ slug: "bedrock", name: "Amazon Bedrock", provider: "anthropic", envKey: "AWS_BEARER_TOKEN_BEDROCK", description: "Use Claude through Amazon Bedrock with a Bedrock API key and AWS region." },
{ slug: "responses-api", name: "Responses API", provider: "openai", envKey: "OPENAI_API_KEY", description: "Connect any compatible harness to an OpenAI Responses-compatible provider or gateway, including Emissary." },
{ slug: "messages-api", name: "Messages API", provider: "anthropic", envKey: "ANTHROPIC_API_KEY", description: "Connect any compatible harness to an Anthropic Messages-compatible provider or gateway." },
{ slug: "chat-completions-api", name: "Chat Completions API", provider: "openai", envKey: "OPENAI_API_KEY", description: "Connect any compatible harness to a Chat Completions-compatible provider or gateway." },
{ slug: "local", name: "Local endpoint", provider: "openai", envKey: "OPENAI_API_KEY", description: "Use a local model server in the agent’s execution environment." },
];
for (const { slug, name, provider, subscription, envKey, url, description } of aiCatalogEntries) {
let app = apps.find(a => a.slug === slug);
if (!app) {
app = { schemaVersion: 1, slug, name, description: description ?? `Connect ${name} accounts for your agents.`, categories: ["ai"], branding: brandingFor(slug), urlPatterns: url ? [url] : [], methods: [] };
apps.push(app);
}
app.tags = [...new Set([...(app.tags ?? []), "model-provider"])];
const methods = (subscription ? ["subscription", "api_key"] : ["api_key"]).map(authMethod => ({
key: `ai-${authMethod}`, label: authMethod === "subscription" ? `${name} subscription` : `${name} API key`,
purpose: "ai", transport: "runtime_auth", auth: authMethod === "subscription" ? "oauth" : "api_key",
ai: { provider, method: authMethod }, grantKinds: ["user", "organization"], ownershipModes: ["customer"],
whenToUse: description ?? "Authenticate an agent with this account.",
guidanceMd: "Use your personal account or an explicitly shared company account.", riskTier: "S3",
...(authMethod === "api_key" ? { credentialFields: [field("apiKey", "API key", "Enter API key")], keyPlacement: { location: "env", name: envKey } } : {}),
}));
// Legacy REST entries have no tool execution adapter. Only offer the supported
// AI account flow; saved REST connections remain removable through Connections.
app.methods = [...methods, ...app.methods.filter(method => method.transport !== "rest_api")];
}
// Every tool method has a checked-in permission review. Discovery metadata is
// evidence for reviewers, never a runtime instruction to request more scopes.
@@ -1694,6 +1867,21 @@ for (const app of apps) {
}
}
// Reviewed instruction templates are authored in each app's definition. Keep
// that optional capability intact when regenerating its transport/auth fields.
for (const app of apps) {
const definitionPath = path.join(out, `${app.slug}.json`);
if (!fs.existsSync(definitionPath)) continue;
const { agentInstructions: template } = JSON.parse(fs.readFileSync(definitionPath, "utf8"));
if (template === undefined) continue;
if (!template || typeof template.id !== "string" || !template.id.trim() || template.id.length > 160
|| !Number.isInteger(template.version) || template.version < 1
|| typeof template.text !== "string" || !template.text.trim() || template.text.length > 2000) {
throw new Error(`${app.slug}: invalid agent instruction template`);
}
app.agentInstructions = template;
}
const validateApp = (app) => {
if (
app.schemaVersion !== 1 ||
@@ -0,0 +1,43 @@
#!/usr/bin/env node
// Credential-free ACP filesystem requests, driven through the real client.
import { createInterface } from "node:readline";
const pending = new Map();
let nextId = 100;
const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
const request = (method, params) => new Promise((resolve) => {
const id = nextId++;
pending.set(id, resolve);
send({ jsonrpc: "2.0", id, method, params });
});
createInterface({ input: process.stdin }).on("line", async (line) => {
const message = JSON.parse(line);
if (pending.has(message.id)) {
pending.get(message.id)(message);
pending.delete(message.id);
return;
}
let result;
if (message.method === "initialize") {
result = {
protocolVersion: 1,
agentCapabilities: { loadSession: false, sessionCapabilities: { close: {} } },
agentInfo: { name: "paperclip-acp-filesystem-fixture", version: "1.0.0" },
};
} else if (message.method === "session/new") {
result = { sessionId: "filesystem-fixture" };
} else if (message.method === "session/prompt") {
const { operations } = JSON.parse(message.params.prompt.find((entry) => entry.type === "text").text);
for (const { method, ...params } of operations) {
const response = await request(method, { sessionId: "filesystem-fixture", ...params });
process.stderr.write(`FS_RESULT ${JSON.stringify(response)}\n`);
}
result = { stopReason: "end_turn" };
} else if (message.method === "session/cancel") {
return;
} else {
result = {};
}
if (message.id !== undefined) send({ jsonrpc: "2.0", id: message.id, result });
});
+53 -6
View File
@@ -134,6 +134,37 @@ function toServerPath(file) {
return path.relative(serverRoot, file).split(path.sep).join("/");
}
// For a multi-line `it.each([...])("name", fn)` call, Vitest 5's `vitest
// list --includeTaskLocation` and `vitest run <file>:<line>` disagree about
// which line registers the test: `list` reports (and only matches) the line
// of the wrapped "name" argument, while `run` only matches the line above
// it, where Prettier's formatting puts that call's opening `(`. Separately,
// on this file `list` also occasionally invents an entry whose "location"
// sits on an ordinary body statement that merely ends in `)(` or starts a
// line with a quote, with no `it`/`test` call anywhere nearby — a bogus
// artifact of the same collector, not a real test. Both only showed up on
// server/src/__tests__/chat-channels.integration.test.ts, a 70,000+ line
// fixture; smaller files have not reproduced either issue.
//
// chatSuiteRunLine classifies a `list`-reported line against the actual
// source and returns the line `vitest run` accepts, or null if the line
// does not belong to any real `it`/`test` call (a bogus entry to drop).
const callAnywherePattern = /(?:^|[^\w$])(?:it|test)(?:\.(?:each|skip|only))?\(/;
const inlineCallPattern = /\)\(\s*["'`]/;
function chatSuiteRunLine(sourceLines, line) {
const text = (sourceLines[line - 1] ?? "").trim();
if (callAnywherePattern.test(text) || text.endsWith(")(") || inlineCallPattern.test(text)) {
return line;
}
const previous = (sourceLines[line - 2] ?? "").trim();
const isNameArgument = text.startsWith('"') || text.startsWith("'") || text.startsWith("`");
if (isNameArgument && previous.endsWith(")(")) {
return line - 1;
}
return null;
}
function isRouteOrAuthzTest(file) {
if (routeTestPattern.test(file)) {
return true;
@@ -326,6 +357,8 @@ function runVitest(args, label, testShard = null) {
mkdirSync(env.PAPERCLIP_HOME, { recursive: true });
mkdirSync(env.TMPDIR, { recursive: true });
if (testShard) {
const file = path.resolve(repoRoot, chatSuite);
const sourceLines = readFileSync(file, "utf8").split("\n");
const collect = (filters, name) => {
const output = path.join(testRoot, `${name}.json`);
const result = spawnSync("pnpm", ["exec", "vitest", "list", ...sourceOnlyVitestArgs,
@@ -335,14 +368,28 @@ function runVitest(args, label, testShard = null) {
if (result.error || result.status !== 0) fail(`Vitest collection failed: ${result.error?.message ?? result.status}`);
return JSON.parse(readFileSync(output, "utf8"));
};
const collected = collect(args, "all");
const file = path.resolve(repoRoot, chatSuite);
const allCollected = collect(args, "all");
const collected = [];
const bogusCollected = [];
for (const test of allCollected) {
(chatSuiteRunLine(sourceLines, test.location.line) === null ? bogusCollected : collected).push(test);
}
if (bogusCollected.length > 0) {
console.warn(
`[test:run] dropping ${bogusCollected.length} bogus Vitest collection entr${bogusCollected.length === 1 ? "y" : "ies"} with no matching it()/test() call: ${bogusCollected.map((test) => `${test.location.line}:${JSON.stringify(test.name)}`).join(", ")}`,
);
}
const selected = partitionTestLines(collected, testShard.count, file)[testShard.index];
const filters = selected.lines.map((line) => `${chatSuite}:${line}`);
args = [...args.filter((arg) => arg !== chatSuite), ...filters];
assertSelectedTests(selected.tests, collect(args, "selected"), file);
// Validate shard membership with the same `list`-reported lines that
// produced `selected`, then switch to the `run`-compatible lines only for
// the filters handed to the real `vitest run` below (see
// chatSuiteRunLine).
const validationFilters = selected.lines.map((line) => `${chatSuite}:${line}`);
const validationArgs = [...args.filter((arg) => arg !== chatSuite), ...validationFilters];
assertSelectedTests(selected.tests, collect(validationArgs, "selected"), file);
console.log(`[test:run] chat shard ${testShard.index + 1}/${testShard.count}: ${selected.tests.length}/${collected.length} tests, ${selected.lines.length} source lines; exact filter coverage verified`);
args.push("--allowOnly=false");
const runFilters = selected.lines.map((line) => `${chatSuite}:${chatSuiteRunLine(sourceLines, line)}`);
args = [...args.filter((arg) => arg !== chatSuite), ...runFilters, "--allowOnly=false"];
}
const result = spawnSync("pnpm", ["exec", "vitest", "run", ...sourceOnlyVitestArgs, ...args], {
cwd: repoRoot,
+1 -1
View File
@@ -100,7 +100,7 @@ try {
if (!request.reservationId || request.maxCostUsd !== 0.5 || !["gpt-5.6-luna", "claude-sonnet-5", ...OPENROUTER_MODELS].includes(request.model)) throw new Error("Paid attempt requires ledger reservation and qualified model");
if (isOpenRouter) {
providerVersion = execFileSync(resolve("packages/paperclip-runner/node_modules/opencode-ai/bin/opencode.exe"), ["--version"], { encoding: "utf8" }).trim();
if (providerVersion !== "1.18.32") throw new Error("OpenCode profile requires version 1.18.32");
if (providerVersion !== "1.18.34") throw new Error("OpenCode profile requires version 1.18.34");
}
const providerEnvironment = isOpenRouter ? openRouterEnvironment() : request.model === "claude-sonnet-5" ? (() => {
const token = process.env.CLAUDE_CODE_OAUTH_TOKEN;
@@ -1,7 +1,7 @@
// Opt-in real Codex qualification; no model turn or live account data.
// Run from the repository root:
// node --import ./server/node_modules/tsx/dist/loader.mjs scripts/tests/native-cleanup-paginated-codex.mjs
// PAPERCLIP_TEST_CODEX_BINARY may select the exact installed Codex 0.156.0 binary.
// PAPERCLIP_TEST_CODEX_BINARY may select the exact installed Codex 0.160.0 binary.
// Fresh synthetic fixture directories are retained for inspection; never reuse live homes.
import { spawn, execFileSync } from "node:child_process";
import {
@@ -26,10 +26,10 @@ if (
execFileSync(codexBinary, ["--version"], {
encoding: "utf8",
timeout: 10000,
}).trim() !== "codex-cli 0.156.0"
}).trim() !== "codex-cli 0.160.0"
) {
throw new Error(
"This opt-in qualification requires Codex CLI 0.156.0. Requalify deliberately before changing the pin.",
"This opt-in qualification requires Codex CLI 0.160.0. Requalify deliberately before changing the pin.",
);
}
const home = await mkdtemp(join(tmpdir(), "paperclip-paginated-probe-"));
@@ -132,7 +132,7 @@ const lines =
timestamp,
cwd: home,
originator: "codex",
cli_version: "0.156.0",
cli_version: "0.160.0",
source: "cli",
model_provider: "openai",
selected_capability_roots: [],
@@ -88,7 +88,7 @@ if (args.length === 0 || (args.length === 1 && args[0] === "--help")) {
}).trim();
assert.equal(
version,
"codex-cli 0.156.0",
"codex-cli 0.160.0",
"qualified_codex_version_required",
);
const ledger = join(fixture, "provider-process-starts.txt");
@@ -309,7 +309,7 @@ if (args.length === 0 || (args.length === 1 && args[0] === "--help")) {
kind: "codex",
provider: "codex",
driver: "codex_app_server",
providerVersion: "0.156.0",
providerVersion: "0.160.0",
command: shim,
args: [
"--cd",
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env node
// Refresh only Arcade's public support claim; never update Composio verification dates.
import { readFile, writeFile } from "node:fs/promises";
const source = "https://docs.arcade.dev/en/resources/integrations";
const args = process.argv.slice(2);
const verifiedAt = args[args.indexOf("--verified-at") + 1];
if (!args.includes("--verified-at") || !/^\d{4}-\d{2}-\d{2}$/.test(verifiedAt)) {
throw new Error("Supply --verified-at YYYY-MM-DD after reviewing the official public Arcade catalog.");
}
const input = args.includes("--input") ? args[args.indexOf("--input") + 1] : null;
let html;
if (input) html = await readFile(input, "utf8");
else {
const response = await fetch(source);
if (!response.ok) throw new Error(`Arcade catalog request failed (${response.status})`);
html = await response.text();
}
const records = [...html.matchAll(/self\.__next_f\.push\(\[1,("(?:\\.|[^"\\])*?")\]\)/g)]
.map((match) => JSON.parse(match[1])).join("");
const start = records.indexOf('"toolkits":[');
if (start < 0) throw new Error("Arcade catalog payload was not found; preserve the previous snapshot.");
const rest = records.slice(start + '"toolkits":'.length);
let end = 0, depth = 0, quoted = false, escaped = false;
for (; end < rest.length; end++) {
const char = rest[end];
if (escaped) { escaped = false; continue; }
if (quoted && char === "\\") { escaped = true; continue; }
if (char === '"') { quoted = !quoted; continue; }
if (!quoted && char === "[") depth++;
if (!quoted && char === "]" && --depth === 0) { end++; break; }
}
const toolkits = JSON.parse(rest.slice(0, end));
const apps = toolkits.filter((app) => app.isComingSoon === false && app.isHidden === false).map((app) => {
const toolkit = app.relativeDocsLink.split("/").at(-1);
const slug = toolkit.replaceAll("_", "-");
const logoUrl = new URL(app.publicIconUrl, source);
const docsUrl = new URL(app.hasPage ? app.docsLink : source);
if (!/^[a-z0-9-]+$/.test(slug) || !app.label || typeof app.id !== "string"
|| logoUrl.protocol !== "https:" || !["docs.arcade.dev", "design-system.arcade.dev"].includes(logoUrl.hostname)
|| docsUrl.origin !== "https://docs.arcade.dev") throw new Error(`Invalid public Arcade entry: ${app.id}`);
return { slug, toolkit, name: app.label, logoUrl: logoUrl.href, docsUrl: docsUrl.href, aliases: [app.id, toolkit], category: app.category };
}).sort((a, b) => a.slug.localeCompare(b.slug, "en"));
if (apps.length < 100 || new Set(apps.map((app) => app.slug)).size !== apps.length) {
throw new Error("Arcade catalog is incomplete or contains duplicate slugs; preserve the previous snapshot.");
}
const target = new URL("../packages/shared/src/arcade-app-catalog.json", import.meta.url);
await writeFile(target, `${JSON.stringify({ source, verifiedAt, apps }, null, 2)}\n`);
console.log(`Indexed ${apps.length} public Arcade apps (${verifiedAt}).`);