Carry current master through Pi public installation

This commit is contained in:
Dotta committed 2026-10-08 14:22:49 -05:00
commit e00258afd9
2195 files changed
+255923 -79198

No files matched your search

@@ -0,0 +1,32 @@
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { cursorConsumerDockerArgs, prepareCursorInstallAccount } from '../cursor-public-install-sandbox.mjs';
const values = (args, flag) => args.flatMap((value, index) => value === flag ? [args[index + 1]] : []);
test('setup and offline admission share a real unprivileged account and persistent home', () => {
const root = mkdtempSync(join(tmpdir(), 'cursor-install-account-'));
try {
const account = prepareCursorInstallAccount(root, 1001, 1001);
assert.match(readFileSync(account.passwd, 'utf8'), /paperclip:x:1001:1001:Paperclip verification:\/home\/paperclip:/);
assert.equal(statSync(account.home).mode & 0o777, 0o700);
for (const download of [true, false]) {
const args = cursorConsumerDockerArgs({ account, assets: '/assets', consumer: '/consumer', cache: '/cache', uid: 1001, gid: 1001, command: ['node', '/packages/probe.mjs'], download, readOnlyConsumer: true });
assert.deepEqual(values(args, '--user'), ['1001:1001']);
assert.deepEqual(values(args, '--network'), [download ? 'bridge' : 'none']);
assert.ok(args.includes('--read-only'));
assert.ok(values(args, '--env').includes('HOME=/home/paperclip'));
assert.ok(values(args, '--mount').includes('type=bind,src=/consumer,dst=/consumer,readonly'));
assert.ok(values(args, '--mount').includes(`type=bind,src=${account.passwd},dst=/etc/passwd,readonly`));
assert.ok(values(args, '--mount').includes(`type=bind,src=${account.home},dst=/home/paperclip`));
}
} finally { rmSync(root, { recursive: true, force: true }); }
});
test('an invalid account is rejected before writing a passwd file', () => {
for (const uid of [0, -1, '1001', undefined]) {
assert.throws(() => prepareCursorInstallAccount('/does-not-exist', uid, 1001), /unprivileged/);
}
});
@@ -92,17 +92,37 @@ test("a route/authz suite never leaks into the general-server shards", () => {
const shard = dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", "0", "--shard-count", SHARD_COUNT.toString()]);
for (const file of shard.selectedGeneralServerSuites) {
assert.ok(
!/[^/]*(?:route|routes|authz)[^/]*\.test\.ts$/.test(file),
!(file.startsWith("server/src/__tests__/") && /[^/]*(?:route|routes|authz)[^/]*\.test\.ts$/.test(file)),
`route/authz suite must stay in the serialized lane, not general-server: ${file}`,
);
}
});
test("general shards retain scripts and route-named suites outside the serialized directory", () => {
const general = dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", "0", "--shard-count", "1"]);
for (const file of [
"server/scripts/verify-runner-vendor-dependencies.test.mjs",
"server/src/services/openrouter-models.test.ts",
"server/src/routes/setup-token-route.test.ts",
]) assert.ok(general.selectedGeneralServerSuites.includes(file), `missing configured server suite: ${file}`);
});
test("shard flags are rejected for the workspaces-b group", () => {
const result = dryRun(["--mode", "general", "--group", "general-workspaces-b", "--shard-index", "0", "--shard-count", "3"]);
assert.notEqual(result.status, 0, "workspaces-b must not accept shard flags");
});
test("workspace lanes cover every non-server project in the root Vitest configuration", () => {
const config = readFileSync(path.join(repoRoot, "vitest.config.ts"), "utf8");
const roots = [...config.matchAll(/^\s+"([^"]+)",?\s*$/gm)].map(match => match[1]);
assert.ok(roots.includes("server"), "expected the explicit root Vitest project list");
const expected = roots.filter(root => root !== "server")
.map(root => JSON.parse(readFileSync(path.join(repoRoot, root, "package.json"), "utf8")).name).sort();
const actual = ["general-workspaces-a", "general-workspaces-b"]
.flatMap(group => dryRunJson(["--mode", "general", "--group", group]).workspaceProjects).sort();
assert.deepEqual(actual, expected, "no configured project may be silently omitted or run twice");
});
test("workspaces-a shards map to Vitest native --shard slices over a stable project list", () => {
const shards = [0, 1].map((index) =>
dryRunJson([
@@ -273,6 +293,9 @@ test("the real shard partition is duration-balanced", () => {
const chatSuitePath = "server/src/__tests__/chat-channels.integration.test.ts";
const nativeRunnerSuitePath =
"server/src/services/native-runtime/native-codex-runner.integration.test.ts";
const dotRunnerSuitePath = "server/src/__tests__/dot-runner.test.ts";
const restartRecoverySuitePath =
"server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts";
// Mirrors pr-trusted.yml (12 shards, called by pr.yml so GITHUB_WORKFLOW is
// "PR"): the chat suite runs in its dedicated lanes and the cargo-dependent
@@ -288,7 +311,9 @@ test("12 PR without-chat shards plus the dedicated chat and native-runner lanes
const files = shards.flatMap((shard) => shard.selectedGeneralServerSuites);
assert.ok(!files.includes(chatSuitePath));
assert.ok(!files.includes(nativeRunnerSuitePath));
assert.deepEqual([...files, chatSuitePath, nativeRunnerSuitePath].sort(), full.selectedGeneralServerSuites.sort());
assert.ok(!files.includes(dotRunnerSuitePath));
assert.ok(!files.includes(restartRecoverySuitePath));
assert.deepEqual([...files, chatSuitePath, nativeRunnerSuitePath, dotRunnerSuitePath, restartRecoverySuitePath].sort(), full.selectedGeneralServerSuites.sort());
assert.equal(new Set(files).size, files.length);
const defaultRun = dryRunJson([], prEnv);
assert.ok(defaultRun.generalServerSuiteCount === full.generalServerSuiteCount);
@@ -307,15 +332,19 @@ for (const [caller, envOverrides] of [["Release", { GITHUB_WORKFLOW: "Release" }
const files = shards.flatMap((shard) => shard.selectedGeneralServerSuites);
assert.ok(!files.includes(chatSuitePath));
assert.ok(files.includes(nativeRunnerSuitePath));
assert.ok(files.includes(dotRunnerSuitePath));
assert.ok(files.includes(restartRecoverySuitePath));
assert.deepEqual([...files, chatSuitePath].sort(), full.selectedGeneralServerSuites.sort());
assert.equal(new Set(files).size, files.length);
});
}
test("the native-runner lane runs exactly the cargo-dependent vertical-slice suite", () => {
test("the native-runner lane runs exactly the cargo-dependent suites", () => {
const lane = dryRunJson(["--mode", "general", "--group", "general-server-native-runner"]);
assert.deepEqual(lane.selectedGeneralServerSuites, [
"server/src/services/native-runtime/native-codex-runner.integration.test.ts",
dotRunnerSuitePath,
restartRecoverySuitePath,
]);
});
@@ -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/);
});
+32
View File
@@ -0,0 +1,32 @@
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { grokConsumerDockerArgs } from './grok-public-install-sandbox.mjs';
export const CURSOR_INSTALL_ACCOUNT_HOME = '/home/paperclip';
// The host UID need not exist in the pinned image. Give it a real, isolated
// account home that survives the separate installation and offline probe runs.
export function prepareCursorInstallAccount(root, uid, gid) {
if (!Number.isSafeInteger(uid) || uid <= 0 || !Number.isSafeInteger(gid) || gid <= 0) {
throw new Error('Public-install verification requires an unprivileged host user');
}
const home = join(root, 'account-home');
const passwd = join(root, 'account-passwd');
mkdirSync(home, { mode: 0o700 });
writeFileSync(passwd, `root:x:0:0:root:/root:/usr/sbin/nologin\npaperclip:x:${uid}:${gid}:Paperclip verification:${CURSOR_INSTALL_ACCOUNT_HOME}:/usr/sbin/nologin\n`, { mode: 0o444, flag: 'wx' });
return { home, passwd };
}
export function cursorConsumerDockerArgs({ account, readOnlyConsumer = false, ...options }) {
const args = grokConsumerDockerArgs({ ...options, temporarySizeMb: 1024, command: [] });
const image = args.pop();
args[args.indexOf('HOME=/tmp')] = `HOME=${CURSOR_INSTALL_ACCOUNT_HOME}`;
if (readOnlyConsumer) {
const mount = `type=bind,src=${options.consumer},dst=/consumer`;
args[args.indexOf(mount)] = `${mount},readonly`;
}
return [...args,
'--mount', `type=bind,src=${account.passwd},dst=/etc/passwd,readonly`,
'--mount', `type=bind,src=${account.home},dst=${CURSOR_INSTALL_ACCOUNT_HOME}`,
image, ...options.command];
}
+5 -1
View File
@@ -119,7 +119,11 @@ if (Object.keys(sortedOptDeps).length > 0) {
}
const output = JSON.stringify(publishPkg, null, 2) + "\n";
const outPath = resolve(repoRoot, "cli/package.json");
const [outputFlag, outputPath, ...extraArgs] = process.argv.slice(2);
if (extraArgs.length || (outputFlag !== undefined && (outputFlag !== "--output" || !outputPath))) {
throw new Error("Usage: generate-npm-package-json.mjs [--output <manifest-path>]");
}
const outPath = outputPath ? resolve(outputPath) : resolve(repoRoot, "cli/package.json");
writeFileSync(outPath, output);
console.log(` ✓ Generated publishable package.json (${Object.keys(sortedDeps).length} deps)`);
+3 -2
View File
@@ -7,17 +7,18 @@ export const GROK_PUBLIC_INSTALL_LIFECYCLE = [
'npm', 'rebuild', '--offline', '--ignore-scripts=false', '--dangerously-allow-all-scripts',
];
export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid, gid, download = false, prerequisite }) {
export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid, gid, download = false, prerequisite, temporarySizeMb = 256 }) {
if (!Number.isSafeInteger(uid) || uid <= 0 || !Number.isSafeInteger(gid) || gid <= 0) {
throw new Error('Public-install verification requires an unprivileged host user');
}
if (!Number.isSafeInteger(temporarySizeMb) || temporarySizeMb < 256 || temporarySizeMb > 2048) throw new Error('Invalid bounded public-install temporary size');
return [
'run', '--rm', '--platform', 'linux/amd64',
'--user', `${uid}:${gid}`, '--read-only',
'--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
'--pids-limit', '256', '--memory', '3g',
'--network', download ? 'bridge' : 'none',
'--tmpfs', '/tmp:rw,nosuid,nodev,size=256m,mode=1777',
'--tmpfs', `/tmp:rw,nosuid,nodev,size=${temporarySizeMb}m,mode=1777`,
'--env', 'HOME=/tmp', '--env', 'npm_config_cache=/cache',
'--env', 'npm_config_nodedir=/usr/local',
'--env', 'npm_config_audit=false', '--env', 'npm_config_fund=false',
+281 -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",
@@ -957,7 +1043,9 @@ const categoryBySlug = {
sentry: "developer",
similarweb: "analytics",
stripe: "commerce",
superagent: "developer",
supabase: "data",
telem: "ai",
"ticket-tailor": "commerce",
ticktick: "productivity",
todoist: "productivity",
@@ -1018,6 +1106,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 ",
@@ -1052,6 +1145,12 @@ const apiKeySpec = {
prefix: "Bearer ",
placeholder: "sbp_...",
},
superagent: { name: "Authorization", prefix: "Bearer ", placeholder: "sk_live_..." },
telem: {
name: "Authorization",
prefix: "Bearer ",
placeholder: "tlm_...",
},
youcom: {
name: "Authorization",
prefix: "Bearer ",
@@ -1124,6 +1223,20 @@ 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 },
}] } : {}),
}),
];
// Superagent's hosted server advertises protected-resource metadata, but its
// authorization server publishes no OAuth metadata, so organization API keys
// are the only working credential.
if (entry.slug === "superagent") return [
apiKeyMethodFor(entry, "mcp-api-key", entry.serverUrl, {
whenToUse: "Connect with a Superagent organization API key.",
guidanceMd: "Open Superagent Settings → API keys, create a separate key for Paperclip, and paste it below.",
consoleLinks: { keys: "https://www.superagent.sh/app/settings#api-keys", docs: entry.docsUrl },
}),
];
if (entry.slug === "zep") return [oauthMethodFor(entry, "mcp-oauth", entry.serverUrl, {
@@ -1414,6 +1527,128 @@ 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 === "telem") {
// Telem's hosted server takes an API key only. The optional settings are
// per-connection request headers that the server reads; each one is left
// out of the request when it is empty, so an unset field keeps the
// server's default (auto routing off, default tier, all providers).
const providerList = (key, label, header, helperMd) => ({
key,
label,
type: "textarea",
advanced: true,
placeholder: "Optional comma-separated provider names",
helperMd,
validation: { pattern: "^[A-Za-z0-9_.-]+(,[A-Za-z0-9_.-]+)*$", maxLength: 500 },
transport: { location: "header", name: header, format: "csv" },
});
const tenantFields = [
{
key: "autoRouting",
label: "Auto routing",
type: "select",
advanced: true,
options: [
{ value: "off", label: "Off" },
{ value: "accuracy", label: "Accuracy" },
],
helperMd:
"Optional. Accuracy lets Telem choose the search providers for each query. Off, or no selection, keeps auto routing off.",
transport: { location: "header", name: "X-Telem-Auto-Routing" },
},
{
key: "tier",
label: "Tier",
type: "select",
advanced: true,
options: [
{ value: "minimalist", label: "Minimalist" },
{ value: "default", label: "Default" },
{ value: "extended", label: "Extended" },
{ value: "max", label: "Max" },
],
helperMd:
"Optional. Sets how much search work Telem does for each query. No selection uses the Default tier.",
transport: { location: "header", name: "X-Telem-Tier" },
},
providerList(
"providersInclude",
"Providers to include",
"X-Telem-Providers-Include",
"Optional. Telem searches only these providers. Leave it empty to allow all providers.",
),
providerList(
"providersExclude",
"Providers to exclude",
"X-Telem-Providers-Exclude",
"Optional. Telem does not search these providers.",
),
];
return [
apiKeyMethodFor(entry, "mcp-api-key", entry.serverUrl, {
guidanceMd:
"Create an API key in the Telem console at app.telem.ai. Paste the key below. Open Advanced to set auto routing, the tier, or the providers to include or exclude.",
whenToUse: "Connect with a Telem API key.",
consoleLinks: { keys: "https://app.telem.ai", docs: entry.docsUrl },
tenantFields,
}),
];
}
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 +1750,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.", superagent: "Review security findings, start red-team reports, and score content and packages before agents trust them.", 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.", telem: "Search the web and read pages across many search providers with one API key." })[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 +1888,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 +1952,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 });
});
@@ -0,0 +1,179 @@
#!/usr/bin/env node
import { createHash, randomUUID } from "node:crypto";
import { once } from "node:events";
import { createServer } from "node:http";
import { pathToFileURL } from "node:url";
/** A loopback-only MCP + OAuth provider; no vendor account or fetch mock needed. */
export async function startOAuthRefreshFixture({
port = 0,
supportsOfflineAccess = true,
grantTypes = ["authorization_code", "refresh_token"],
challengeScope = true,
resourceScopes = ["mcp:read"],
accessTokenLifetimeSeconds = 3600,
} = {}) {
const clients = new Map();
const codes = new Map();
const accessTokens = new Map();
const refreshTokens = new Map();
// Diagnostics deliberately contain no credentials, codes, or PKCE verifiers.
const events = [];
let clockOffset = 0;
let origin;
const now = () => Date.now() + clockOffset;
const issuer = () => `${origin}/identity/acme`;
const resource = () => `${origin}/mcp`;
const supportedScopes = () => ["mcp:read", "admin:all", ...(supportsOfflineAccess ? ["offline_access"] : [])];
const json = (res, status, body) => {
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
res.end(JSON.stringify(body));
};
const fail = (res, error) => json(res, 400, { error });
function issueTokens(grant) {
const accessToken = `fixture-access-${randomUUID()}`;
accessTokens.set(accessToken, { ...grant, expiresAt: now() + accessTokenLifetimeSeconds * 1000 });
const refreshToken = grant.offline ? `fixture-refresh-${randomUUID()}` : null;
if (refreshToken) refreshTokens.set(refreshToken, grant);
return {
access_token: accessToken,
...(refreshToken ? { refresh_token: refreshToken } : {}),
expires_in: accessTokenLifetimeSeconds,
token_type: "Bearer",
scope: grant.scopes.join(" "),
};
}
const server = createServer(async (req, res) => {
try {
const url = new URL(req.url, origin);
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const rawBody = Buffer.concat(chunks).toString("utf8");
const form = new URLSearchParams(rawBody);
const path = url.pathname;
if (path === "/.well-known/oauth-protected-resource/mcp") {
json(res, 200, { resource: resource(), authorization_servers: [issuer()], scopes_supported: resourceScopes });
} else if (path === "/.well-known/oauth-authorization-server/identity/acme") {
json(res, 200, {
issuer: issuer(),
authorization_endpoint: `${issuer()}/authorize`,
token_endpoint: `${issuer()}/token`,
registration_endpoint: `${issuer()}/register`,
scopes_supported: supportedScopes(),
grant_types_supported: grantTypes,
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
token_endpoint_auth_methods_supported: ["none"],
});
} else if (path === "/identity/acme/register" && req.method === "POST") {
const metadata = JSON.parse(rawBody);
if (!Array.isArray(metadata.redirect_uris) || metadata.redirect_uris.some((uri) => {
const parsed = new URL(uri);
return parsed.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(parsed.hostname);
})) return fail(res, "invalid_redirect_uri");
const requestedGrantTypes = metadata.grant_types ?? ["authorization_code"];
if (!Array.isArray(requestedGrantTypes) || requestedGrantTypes.some((type) => !grantTypes.includes(type))) return fail(res, "invalid_client_metadata");
metadata.grant_types = requestedGrantTypes;
const clientId = `fixture-client-${randomUUID()}`;
clients.set(clientId, metadata);
events.push({ kind: "registration", grantTypes: metadata.grant_types });
json(res, 201, { ...metadata, client_id: clientId });
} else if (path === "/identity/acme/authorize") {
const params = req.method === "POST" ? form : url.searchParams;
const clientId = params.get("client_id");
const client = clients.get(clientId);
const redirectUri = params.get("redirect_uri");
const scopes = (params.get("scope") ?? "").split(/\s+/).filter(Boolean);
if (!client || !client.redirect_uris.includes(redirectUri)) return fail(res, "invalid_client");
if (params.get("response_type") !== "code" || params.get("code_challenge_method") !== "S256") return fail(res, "invalid_request");
if (params.get("resource") !== resource()) return fail(res, "invalid_target");
if (scopes.some((scope) => !supportedScopes().includes(scope))) return fail(res, "invalid_scope");
if (req.method === "GET") {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
const escape = (value) => String(value).replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
res.end(`<!doctype html><html><title>Test MCP consent</title><h1>Connect the test MCP server</h1><p>Requested scopes: ${escape(scopes.join(" "))}</p><form method="post">${[...params].map(([key, value]) => `<input type="hidden" name="${escape(key)}" value="${escape(value)}">`).join("")}<button name="decision" value="allow">Allow</button><button name="decision" value="deny">Deny</button></form></html>`);
return;
}
const callback = new URL(redirectUri);
callback.searchParams.set("state", params.get("state") ?? "");
callback.searchParams.set("iss", issuer());
if (params.get("decision") !== "allow") callback.searchParams.set("error", "access_denied");
else {
const offline = grantTypes.includes("refresh_token") && scopes.includes("offline_access") && params.get("prompt")?.split(/\s+/).includes("consent") && client.grant_types.includes("refresh_token");
const code = `fixture-code-${randomUUID()}`;
codes.set(code, {
clientId, redirectUri, resource: resource(), codeChallenge: params.get("code_challenge"),
scopes: offline ? scopes : scopes.filter((scope) => scope !== "offline_access"), offline,
});
events.push({ kind: "authorization", scopes, prompt: params.get("prompt"), offline: Boolean(offline) });
callback.searchParams.set("code", code);
}
res.writeHead(302, { Location: callback.toString() });
res.end();
} else if (path === "/identity/acme/token" && req.method === "POST") {
const grantType = form.get("grant_type");
if (!grantTypes.includes(grantType)) return fail(res, "unsupported_grant_type");
let grant;
if (grantType === "authorization_code") {
const code = form.get("code");
grant = codes.get(code);
const challenge = createHash("sha256").update(form.get("code_verifier") ?? "").digest("base64url");
if (!grant || grant.clientId !== form.get("client_id") || grant.redirectUri !== form.get("redirect_uri") || grant.resource !== form.get("resource") || grant.codeChallenge !== challenge) return fail(res, "invalid_grant");
codes.delete(code);
} else if (grantType === "refresh_token") {
const refreshToken = form.get("refresh_token");
grant = refreshTokens.get(refreshToken);
if (!grant || grant.clientId !== form.get("client_id") || grant.resource !== form.get("resource")) return fail(res, "invalid_grant");
refreshTokens.delete(refreshToken); // Rotation: reuse must fail.
} else return fail(res, "unsupported_grant_type");
events.push({ kind: "token", grantType, resource: grant.resource, offline: Boolean(grant.offline) });
json(res, 200, issueTokens(grant));
} else if (path === "/mcp") {
const token = accessTokens.get(req.headers.authorization?.replace(/^Bearer /, ""));
if (!token || token.expiresAt <= now()) {
events.push({ kind: "mcp_unauthorized" });
res.writeHead(401, { "WWW-Authenticate": `Bearer resource_metadata="${origin}/.well-known/oauth-protected-resource/mcp"${challengeScope ? ', scope="mcp:read"' : ""}` });
res.end();
return;
}
if (!token.scopes.includes("mcp:read")) { json(res, 403, { error: "insufficient_scope" }); return; }
if (req.method !== "POST") { res.writeHead(405); res.end(); return; }
const rpc = JSON.parse(rawBody);
events.push({ kind: "mcp", method: rpc.method });
if (rpc.method === "notifications/initialized") { res.writeHead(202); res.end(); return; }
let result;
if (rpc.method === "initialize") result = { protocolVersion: rpc.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "offline-access-fixture", version: "1.0.0" } };
else if (rpc.method === "tools/list") result = { tools: [{ name: "read_status", description: "Read the test server status", inputSchema: { type: "object", properties: {} }, annotations: { readOnlyHint: true } }] };
else if (rpc.method === "tools/call" && rpc.params.name === "read_status") result = { content: [{ type: "text", text: "Authenticated MCP call succeeded" }] };
else { json(res, 200, { jsonrpc: "2.0", id: rpc.id, error: { code: -32601, message: "Method not found" } }); return; }
json(res, 200, { jsonrpc: "2.0", id: rpc.id, result });
} else { json(res, 404, { error: "not_found" }); }
} catch {
json(res, 400, { error: "invalid_request" });
}
});
server.listen(port, "127.0.0.1");
await once(server, "listening");
origin = `http://127.0.0.1:${server.address().port}`;
return {
origin, mcpUrl: resource(), issuer: issuer(), events,
/** Advance only provider time; callers can separately expire their cached token. */
advanceTime(ms) { clockOffset += ms; },
async close() {
const closed = new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
server.closeAllConnections();
await closed;
},
};
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const fixture = await startOAuthRefreshFixture({ port: Number(process.env.PORT ?? 0), accessTokenLifetimeSeconds: 120 });
console.log(JSON.stringify({ event: "ready", mcpUrl: fixture.mcpUrl, accessTokenLifetimeSeconds: 120 }));
const stop = async () => { await fixture.close(); process.exit(0); };
process.once("SIGINT", stop);
process.once("SIGTERM", stop);
}
@@ -0,0 +1,92 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import test from "node:test";
import { startOAuthRefreshFixture } from "./oauth-refresh-fixture.mjs";
async function connect(fixture, { offline = true, consent = true, grantTypes = ["authorization_code", "refresh_token"] } = {}) {
const redirectUri = "http://127.0.0.1:3100/api/tools/oauth/callback";
const registration = await fetch(`${fixture.issuer}/register`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ redirect_uris: [redirectUri], grant_types: grantTypes }),
});
assert.equal(registration.status, 201);
const client = await registration.json();
const verifier = "fixture-proof-key-for-tests";
const authorization = new URLSearchParams({
client_id: client.client_id, redirect_uri: redirectUri, response_type: "code", state: "fixture-state",
resource: fixture.mcpUrl, code_challenge_method: "S256",
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
scope: offline ? "mcp:read offline_access" : "mcp:read", decision: "allow",
...(consent ? { prompt: "consent" } : {}),
});
const response = await fetch(`${fixture.issuer}/authorize`, { method: "POST", body: authorization, redirect: "manual" });
assert.equal(response.status, 302);
const callback = new URL(response.headers.get("location"));
const exchangeBody = new URLSearchParams({
grant_type: "authorization_code", client_id: client.client_id, redirect_uri: redirectUri,
resource: fixture.mcpUrl, code_verifier: verifier, code: callback.searchParams.get("code"),
});
const exchange = await fetch(`${fixture.issuer}/token`, { method: "POST", body: exchangeBody });
assert.equal(exchange.status, 200);
return { tokens: await exchange.json(), clientId: client.client_id };
}
async function call(fixture, accessToken) {
return fetch(fixture.mcpUrl, {
method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}` },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "read_status", arguments: {} } }),
});
}
for (const options of [{ offline: false }, { consent: false }]) {
test(`access-only grant disconnects after expiry: ${JSON.stringify(options)}`, async () => {
const fixture = await startOAuthRefreshFixture();
try {
const { tokens } = await connect(fixture, options);
assert.equal(tokens.refresh_token, undefined);
assert.equal((await call(fixture, tokens.access_token)).status, 200);
fixture.advanceTime(3_601_000);
assert.equal((await call(fixture, tokens.access_token)).status, 401);
} finally { await fixture.close(); }
});
}
test("authorization-code-only provider rejects refresh registration and token requests", async () => {
const fixture = await startOAuthRefreshFixture({ grantTypes: ["authorization_code"] });
try {
const registration = await fetch(`${fixture.issuer}/register`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ redirect_uris: ["http://127.0.0.1:3100/api/tools/oauth/callback"], grant_types: ["authorization_code", "refresh_token"] }),
});
assert.equal(registration.status, 400);
assert.deepEqual(await registration.json(), { error: "invalid_client_metadata" });
const { tokens, clientId } = await connect(fixture, { grantTypes: ["authorization_code"] });
assert.equal(tokens.refresh_token, undefined);
assert.equal((await call(fixture, tokens.access_token)).status, 200);
const body = new URLSearchParams({ grant_type: "refresh_token", client_id: clientId, resource: fixture.mcpUrl, refresh_token: "unissued-token" });
const refresh = await fetch(`${fixture.issuer}/token`, { method: "POST", body });
assert.equal(refresh.status, 400);
assert.deepEqual(await refresh.json(), { error: "unsupported_grant_type" });
fixture.advanceTime(3_601_000);
assert.equal((await call(fixture, tokens.access_token)).status, 401);
} finally { await fixture.close(); }
});
test("offline consent permits refresh, requires the same resource, and rejects refresh-token reuse", async () => {
const fixture = await startOAuthRefreshFixture();
try {
const { tokens, clientId } = await connect(fixture);
assert.ok(tokens.refresh_token);
fixture.advanceTime(3_601_000);
assert.equal((await call(fixture, tokens.access_token)).status, 401);
const body = new URLSearchParams({ grant_type: "refresh_token", client_id: clientId, resource: `${fixture.origin}/other`, refresh_token: tokens.refresh_token });
assert.equal((await fetch(`${fixture.issuer}/token`, { method: "POST", body })).status, 400);
body.set("resource", fixture.mcpUrl);
const refresh = await fetch(`${fixture.issuer}/token`, { method: "POST", body });
assert.equal(refresh.status, 200);
const rotated = await refresh.json();
assert.notEqual(rotated.refresh_token, tokens.refresh_token);
assert.equal((await call(fixture, rotated.access_token)).status, 200);
assert.equal((await fetch(`${fixture.issuer}/token`, { method: "POST", body })).status, 400);
} finally { await fixture.close(); }
});
+4 -163
View File
@@ -1,166 +1,7 @@
#!/usr/bin/env bash
# Keep the repository command compatible; the deployable implementation belongs
# to the skill so agents do not need a checkout of this repository.
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
scripts/paperclip-issue-update.sh [--issue-id ID] [--status STATUS] [--comment TEXT] [--dry-run]
Reads a multiline markdown comment from stdin when stdin is piped. This preserves
newlines when building the JSON payload for PATCH /api/issues/{issueId}.
Examples:
scripts/paperclip-issue-update.sh --issue-id "$PAPERCLIP_TASK_ID" --status in_progress <<'MD'
Investigating formatting
- Pulled the raw comment body
- Comparing it with the run transcript
MD
scripts/paperclip-issue-update.sh --issue-id "$PAPERCLIP_TASK_ID" --status done --dry-run <<'MD'
Done
- Fixed the issue update helper
MD
EOF
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
printf 'Missing required command: %s\n' "$1" >&2
exit 1
fi
}
issue_id="${PAPERCLIP_TASK_ID:-}"
status=""
comment_arg=""
dry_run=0
while [[ $# -gt 0 ]]; do
case "$1" in
--issue-id)
issue_id="${2:-}"
shift 2
;;
--status)
status="${2:-}"
shift 2
;;
--comment)
comment_arg="${2:-}"
shift 2
;;
--dry-run)
dry_run=1
shift
;;
--help|-h)
usage
exit 0
;;
*)
printf 'Unknown argument: %s\n' "$1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$issue_id" ]]; then
printf 'Missing issue id. Pass --issue-id or set PAPERCLIP_TASK_ID.\n' >&2
exit 1
fi
comment=""
if [[ -n "$comment_arg" ]]; then
comment="$comment_arg"
elif [[ ! -t 0 ]]; then
comment="$(cat)"
fi
require_command jq
payload="$(
jq -nc \
--arg status "$status" \
--arg comment "$comment" \
'
(if $status == "" then {} else {status: $status} end) +
(if $comment == "" then {} else {comment: $comment} end)
'
)"
if [[ "$dry_run" == "1" ]]; then
printf '%s\n' "$payload"
exit 0
fi
if [[ -z "${PAPERCLIP_API_URL:-}" || -z "${PAPERCLIP_API_KEY:-}" || -z "${PAPERCLIP_RUN_ID:-}" ]]; then
printf 'Missing PAPERCLIP_API_URL, PAPERCLIP_API_KEY, or PAPERCLIP_RUN_ID.\n' >&2
exit 1
fi
# A successful PATCH always returns the updated issue JSON. An empty body or a
# connection-level failure means the write did NOT land, even when a pipeline
# exit code says otherwise, so verify the response instead of inferring success.
# Two attempts total: the shared heartbeat policy stops a control-plane write
# after two consecutive failures, so the helper must not send a third.
max_attempts=2
attempt=1
while :; do
http_code=""
body=""
set +e
response="$(
curl -sS -m 30 -X PATCH \
"$PAPERCLIP_API_URL/api/issues/$issue_id" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
-H 'Content-Type: application/json' \
--data-binary "$payload" \
-w '\n%{http_code}'
)"
curl_exit=$?
set -e
if [[ "$curl_exit" -eq 0 ]]; then
http_code="${response##*$'\n'}"
body="${response%$'\n'*}"
fi
if [[ "$curl_exit" -eq 0 && "$http_code" == 2* ]]; then
if [[ -z "$body" ]]; then
printf 'Issue update FAILED: HTTP %s with an empty response body. A real update echoes the issue JSON; treat this write as not saved.\n' "$http_code" >&2
exit 1
fi
if [[ -n "$status" ]]; then
returned_status="$(jq -r '.status // empty' <<<"$body" 2>/dev/null || true)"
if [[ "$returned_status" != "$status" ]]; then
printf 'Issue update FAILED: server echoed status %s instead of requested %s.\n' "${returned_status:-<none>}" "$status" >&2
printf '%s\n' "$body" >&2
exit 1
fi
fi
printf '%s\n' "$body"
exit 0
fi
# 4xx (other than 429) is a definitive rejection; retrying cannot change it.
if [[ "$curl_exit" -eq 0 && "$http_code" == 4* && "$http_code" != "429" ]]; then
printf 'Issue update rejected (HTTP %s).\n' "$http_code" >&2
[[ -n "$body" ]] && printf '%s\n' "$body" >&2
exit 1
fi
if (( attempt >= max_attempts )); then
printf 'Issue update FAILED after %d attempts (curl exit %s, HTTP %s). The status/comment was NOT saved — report this write as failed, do not assume it landed.\n' "$max_attempts" "$curl_exit" "${http_code:-000}" >&2
[[ -n "$body" ]] && printf '%s\n' "$body" >&2
exit 1
fi
printf 'Issue update attempt %d/%d failed (curl exit %s, HTTP %s); retrying...\n' "$attempt" "$max_attempts" "$curl_exit" "${http_code:-000}" >&2
sleep $((attempt * 2))
attempt=$((attempt + 1))
done
paperclip_repo_scripts="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
exec bash "$paperclip_repo_scripts/../skills/paperclip/scripts/paperclip-issue-update.sh" "$@"
+6
View File
@@ -9,6 +9,7 @@ paperclip_instance_id="${PAPERCLIP_INSTANCE_ID:-default}"
paperclip_dir="$worktree_cwd/.paperclip"
worktree_config_path="$paperclip_dir/config.json"
seed_manifest_path="$paperclip_dir/seed-manifest.json"
seed_empty_path="$paperclip_dir/seed-empty"
if [[ ! -d "$base_cwd" ]]; then
echo "Base workspace does not exist: $base_cwd" >&2
@@ -20,6 +21,11 @@ if [[ ! -d "$worktree_cwd" ]]; then
exit 1
fi
if [[ -e "$seed_empty_path" ]]; then
echo "Worktree instance is explicitly empty; automatic database copying is disabled." >&2
exit 0
fi
if [[ -e "$seed_manifest_path" ]]; then
seed_manifest_state="$(SEED_MANIFEST_PATH="$seed_manifest_path" node <<'EOF'
const fs = require("node:fs");
+80 -15
View File
@@ -19,6 +19,7 @@ const serializedShardDurations = loadShardDurations(
const serverRoot = path.join(repoRoot, "server");
const serverSrcDir = path.join(repoRoot, "server", "src");
const serverTestsDir = path.join(repoRoot, "server", "src", "__tests__");
const serverScriptsDir = path.join(repoRoot, "server", "scripts");
const nonServerProjects = [
"@paperclipai/shared",
"@paperclipai/skills-catalog",
@@ -26,9 +27,15 @@ const nonServerProjects = [
"@paperclipai/adapter-utils",
"@paperclipai/adapter-claude-local",
"@paperclipai/adapter-codex-local",
"@paperclipai/adapter-cursor-cloud",
"@paperclipai/adapter-cursor-local",
"@paperclipai/adapter-gemini-local",
"@paperclipai/adapter-grok-local",
"@paperclipai/hermes-paperclip-adapter",
"@paperclipai/adapter-kimi-local",
"@paperclipai/adapter-openclaw-gateway",
"@paperclipai/adapter-opencode-local",
"@paperclipai/adapter-pi-local",
"@paperclipai/plugin-daytona",
"@paperclipai/plugin-sdk",
"@paperclipai/create-paperclip-plugin",
@@ -72,12 +79,16 @@ const generalServerWithoutChatGroupName = "general-server-without-chat";
const generalChatGroupName = "general-chat";
const generalServerNativeRunnerGroupName = "general-server-native-runner";
const chatSuite = "server/src/__tests__/chat-channels.integration.test.ts";
// This suite rebuilds the Runner release binaries with cargo in beforeAll.
// The first suite rebuilds the Runner release binaries with cargo in beforeAll.
// Inside the PR workflow's plain server shards, which carry no Rust cache,
// that build was a ~4m30s cold compile of every third-party crate on each run
// (277s of a 291s shard vitest step, actions run 35246999382, 2026-09-17).
const nativeRunnerSuite =
"server/src/services/native-runtime/native-codex-runner.integration.test.ts";
const nativeRunnerSuites = [
"server/src/services/native-runtime/native-codex-runner.integration.test.ts",
"server/src/__tests__/dot-runner.test.ts",
// These process cases otherwise skip in server shards without Runner binaries.
"server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts",
];
// In the PR workflow (pr.yml, the caller of pr-trusted.yml — reusable
// workflows inherit the caller's GITHUB_WORKFLOW), the last Verify Paperclip
// Runner vitest shard runs the native-runner group instead, because those
@@ -90,7 +101,7 @@ const nativeRunnerSuite =
const prWorkflowName = "PR";
const nativeRunnerSuiteRunsInRustCachedLane = process.env.GITHUB_WORKFLOW === prWorkflowName;
const withoutChatExcludedSuites = nativeRunnerSuiteRunsInRustCachedLane
? [chatSuite, nativeRunnerSuite]
? [chatSuite, ...nativeRunnerSuites]
: [chatSuite];
const generalWorkspacesAGroupName = "general-workspaces-a";
const generalWorkspacesBGroupName = "general-workspaces-b";
@@ -132,6 +143,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;
@@ -314,6 +356,8 @@ function runVitest(args, label, testShard = null) {
const env = {
...process.env,
NODE_ENV: "test",
PAPERCLIP_TEST_HOST_HOME: process.env.PAPERCLIP_TEST_HOST_HOME
?? (process.env.PAPERCLIP_HOME?.trim() || path.join(os.homedir(), ".paperclip")),
PAPERCLIP_HOME: path.join(testRoot, "h"),
// Config discovery otherwise prefers the checkout's .paperclip/config.json
// over PAPERCLIP_HOME, importing preview scheduling policy into unit tests.
@@ -324,6 +368,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,
@@ -333,14 +379,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,
@@ -383,7 +443,7 @@ function runGeneralGroup(routeTests, groupName, shardIndex = null, shardCount =
}
if (groupName === generalServerNativeRunnerGroupName) {
runVitest(
["--project", "@paperclipai/server", ...serializedServerVitestArgs, nativeRunnerSuite],
["--project", "@paperclipai/server", ...serializedServerVitestArgs, ...nativeRunnerSuites],
"native runner vertical-slice suite",
);
return;
@@ -492,10 +552,15 @@ const routeTests = walk(serverTestsDir)
// config pins maxWorkers to 1, so the only way to parallelize is across jobs.
// Suites are partitioned by recorded duration (scripts/general-server-shard.mjs)
// rather than round-robin, so one slow suite cluster can't stretch a single shard.
const generalServerTestFiles = walk(serverSrcDir)
const serializedRepoPaths = new Set(routeTests.map(test => test.repoPath));
const generalServerTestFiles = [
...walk(serverSrcDir).filter(file => file.endsWith(".test.ts")),
...walk(serverScriptsDir).filter(file => file.endsWith(".test.mjs")),
]
.map((file) => toRepoPath(file))
.filter((repoPath) => repoPath.endsWith(".test.ts"))
.filter((repoPath) => !isRouteOrAuthzTest(repoPath))
// Only exclude suites actually assigned to the serialized lane. A name
// such as services/openrouter-models.test.ts is not a serialized route.
.filter((repoPath) => !serializedRepoPaths.has(repoPath))
.sort((a, b) => a.localeCompare(b));
const options = parseCliOptions(process.argv.slice(2));
@@ -517,7 +582,7 @@ if (options.dryRun) {
generalServerSuiteCount: generalServerTestFiles.length,
selectedGeneralServerSuites:
options.mode === generalModeName && options.group === generalServerNativeRunnerGroupName
? [nativeRunnerSuite]
? nativeRunnerSuites
: options.mode === generalModeName &&
[generalServerGroupName, generalServerWithoutChatGroupName].includes(options.group) &&
options.shardCount !== null
+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;
+7 -5
View File
@@ -18,10 +18,10 @@ export function storybookAgentAvatarAssets() {
})();
server.middlewares.use((req, res, next) => {
const pathname = new URL(req.url ?? "/", "http://storybook.local").pathname;
const match = /^\/agent-avatar-images\/cap-v1\/([^/]+)\/([^/]+)-(\d+)-([12])\.png$/.exec(pathname);
const match = /^\/agent-avatar-images\/cap-v1\/([^/]+)\/([^/]+)-(\d+)-([12])(-paperclip-dark-v1)?\.png$/.exec(pathname);
if (!match) return next();
void (async () => {
const [, palette, pose, sizeText, scaleText] = match;
const [, palette, pose, sizeText, scaleText, darkBackground] = match;
const { pool, AGENT_PALETTE_IDS, AGENT_AVATAR_SIZES, CHARACTER_STATES, appearanceForPalette } = await renderer();
const size = Number(sizeText);
const scale = Number(scaleText);
@@ -33,7 +33,7 @@ export function storybookAgentAvatarAssets() {
}
if (!images.has(pathname)) {
const appearance = appearanceForPalette(muted ? AGENT_PALETTE_IDS[0] : palette);
images.set(pathname, pool.render({ appearance, size, scale, pose, muted }).catch((error) => {
images.set(pathname, pool.render({ appearance, size, scale, pose, muted, background: darkBackground ? "paperclip-dark" : "transparent" }).catch((error) => {
images.delete(pathname);
throw error;
}));
@@ -60,6 +60,8 @@ export function storybookAgentAvatarAssets() {
(muted ? [AGENT_PALETTE_IDS[0]] : AGENT_PALETTE_IDS).flatMap(palette =>
CHARACTER_STATES.flatMap(pose => AGENT_AVATAR_SIZES.flatMap(size =>
[1, 2].map(scale => ({ appearance: appearanceForPalette(palette), size, scale, pose, muted }))))));
// Slack exports use this one opaque preset; ordinary UI avatars stay clear.
requests.push(...AGENT_PALETTE_IDS.map(palette => ({ appearance: appearanceForPalette(palette), size: 512, scale: 1, pose: "rest", muted: false, background: "paperclip-dark" })));
const pool = createAgentAvatarPool(2);
const images = [];
let next = 0;
@@ -68,8 +70,8 @@ export function storybookAgentAvatarAssets() {
while (next < requests.length) {
const request = requests[next++];
const source = await pool.render(request);
const { appearance, size, scale, pose, muted } = request;
const fileName = agentAvatarUrl(appearance, size, scale, pose, muted).slice(2);
const { appearance, size, scale, pose, muted, background } = request;
const fileName = agentAvatarUrl(appearance, size, scale, pose, muted, background).slice(2);
this.emitFile({ type: "asset", fileName, source });
images.push({ path: fileName, sha256: createHash("sha256").update(source).digest("hex"), pixels: size * scale });
}
@@ -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: [],
@@ -50,7 +50,7 @@ if (args.length === 0 || (args.length === 1 && args[0] === "--help")) {
// Never silently qualify a debug fallback or a caller-selected alternate runner.
assert.match(
runnerBinary,
/\/dist\/bin\/paperclip-runnerd$/u,
/\/dist\/bin\/(?:darwin-(?:arm64|x64)\/|linux-x64\/)?paperclip-runnerd$/u,
"normal_staged_runner_required",
);
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
@@ -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}).`);
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env node
// Public npm graph + explicit setup only. No credentials, inference, publication,
// checkout mount, candidate overrides, or native Cursor assets in the tarballs.
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { materializePublishManifest, prepareBundledPackage } from './prepare-bundled-package.mjs';
import { GROK_PUBLIC_INSTALL_LIFECYCLE, GROK_PUBLIC_INSTALL_IMAGE } from './grok-public-install-sandbox.mjs';
import { cursorConsumerDockerArgs, prepareCursorInstallAccount } from './cursor-public-install-sandbox.mjs';
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const root = mkdtempSync(join(tmpdir(), 'paperclip-cursor-public-install-'));
console.log(`Verification evidence: ${root}`);
const env = { ...process.env, NODE_PATH: '', PAPERCLIP_RELEASE_REUSE_UI_DIST: '1', npm_config_ignore_scripts: 'false', npm_config_audit: 'false', npm_config_fund: 'false' };
for (const key of Object.keys(env)) if (/^(OPENAI|ANTHROPIC|OPENROUTER|DAYTONA|XAI|GROK|CURSOR|COPILOT|GITHUB|GH)(_|$)/.test(key) || /CANDIDATE/.test(key)) delete env[key];
const run = (command, args, cwd = root) => execFileSync(command, args, { cwd, env, stdio: 'pipe', maxBuffer: 64 * 1024 * 1024 });
const sourceRevision = run('git', ['rev-parse', 'HEAD'], repo).toString().trim();
const releaseBinaries = JSON.parse(readFileSync(join(repo, 'server/dist/vendor/paperclip-runner/bin/release-manifest.json'), 'utf8'));
assert.equal(releaseBinaries.schema, 'paperclip.runner.release-binaries.v1');
assert.match(releaseBinaries.sourceRevision, /^[a-f0-9]{40}$/);
run('git', ['merge-base', '--is-ancestor', releaseBinaries.sourceRevision, sourceRevision], repo);
// Controller and TypeScript admission may follow a frozen daemon. Keep its
// exact provenance and require the Rust sources and protocol to remain identical.
run('git', ['diff', '--quiet', releaseBinaries.sourceRevision, sourceRevision, '--', 'packages/paperclip-runner/runner', 'packages/paperclip-runner/protocol'], repo);
assert.deepEqual(Object.keys(releaseBinaries.platforms).sort(), ['darwin-arm64', 'darwin-x64', 'linux-x64']);
assert.equal(releaseBinaries.remoteProviderPack?.target, 'linux-x64', 'Release assembly must include the expected ordinary Daytona image pack');
const releaseVersion = `0.0.0-cursor-verify.${sourceRevision.slice(0, 12)}`;
const listing = run(process.execPath, [join(repo, 'scripts/release-package-map.mjs'), 'list'], repo).toString().trim().split('\n').map(line => line.split('\t'));
const packages = new Map(listing.map(([dir, name]) => [name, {dir, manifest: JSON.parse(readFileSync(join(repo, dir, 'package.json')))}]));
const cliManifestPath = join(repo, 'cli/package.json'); const original = readFileSync(cliManifestPath);
const generatedManifest = join(root, 'cli-manifest.json');
run(process.execPath, [join(repo, 'scripts/generate-npm-package-json.mjs'), '--output', generatedManifest], repo);
const publishedCli = JSON.parse(readFileSync(generatedManifest));
publishedCli.dependencies['@paperclipai/server'] = releaseVersion;
packages.set('paperclipai', { dir: 'cli', manifest: publishedCli });
const needed = new Set();
function visit(name) {
if (needed.has(name)) return;
const entry = packages.get(name); assert.ok(entry, `Missing public package ${name}`); needed.add(name);
for (const [dependency, version] of Object.entries({...entry.manifest.dependencies, ...entry.manifest.optionalDependencies})) if (version.startsWith('workspace:') || dependency === '@paperclipai/server' && name === 'paperclipai') visit(dependency);
}
visit('paperclipai');
// Run after the candidate build, including its required standalone adapters.
for (const name of needed) assert.ok(existsSync(join(repo, packages.get(name).dir, 'dist')), `Missing built public package ${name}`);
run('bash', [join(repo, 'scripts/prepare-server-ui-dist.sh')], repo);
const assets = join(root, 'assets'); const consumer = join(root, 'consumer'); const cache = join(root, 'cache');
for (const path of [assets, consumer, cache]) mkdirSync(path, { mode: 0o755 });
writeFileSync(join(consumer, 'package.json'), JSON.stringify({ private: true, type: 'module' }));
for (const [index, name] of [...needed].entries()) {
const { dir, manifest } = packages.get(name); const staged = join(root, `source-${index}`); const target = join(root, `package-${index}`); mkdirSync(staged);
for (const file of manifest.files ?? ['dist']) {
const skills = file === 'skills' && ['server', 'packages/adapters/claude-local', 'packages/adapters/codex-local'].includes(dir);
if (file === 'skills' && !skills && !existsSync(join(repo,dir,file))) continue;
cpSync(skills ? join(repo,'skills') : join(repo,dir,file), join(staged,file), { recursive: true });
}
const releaseManifest = { ...manifest, version: releaseVersion };
writeFileSync(join(staged,'package.json'), JSON.stringify(releaseManifest));
if ((manifest.bundleDependencies ?? manifest.bundledDependencies ?? []).length) prepareBundledPackage(staged,target);
else { cpSync(staged,target,{recursive:true}); writeFileSync(join(target,'package.json'),JSON.stringify(materializePublishManifest(releaseManifest))); }
run('npm',['pack','--ignore-scripts','--pack-destination',assets],target);
console.log(`Packed ${name}`);
}
const sentinelSource = join(root,'lifecycle-sentinel'); mkdirSync(sentinelSource);
writeFileSync(join(sentinelSource,'package.json'), JSON.stringify({name:'paperclip-verification-lifecycle-sentinel',version:'1.0.0',private:true,scripts:{postinstall:'node -e "require(\'node:fs\').writeFileSync(\'lifecycle-ran\', \'ok\')"'}}));
run('npm',['pack','--ignore-scripts','--pack-destination',assets],sentinelSource);
for (const file of readdirSync(assets)) chmodSync(join(assets,file),0o644);
const account = prepareCursorInstallAccount(root, process.getuid(), process.getgid());
const isolated = (command, options={}) => run('docker',cursorConsumerDockerArgs({assets,consumer,cache,account,uid:process.getuid(),gid:process.getgid(),command,...options}));
isolated(['npm','install','--ignore-scripts','--omit=dev',...readdirSync(assets).filter(f=>f.endsWith('.tgz')).map(f=>`/packages/${f}`)],{download:true});
const sentinel = join(consumer,'node_modules/paperclip-verification-lifecycle-sentinel/lifecycle-ran');
assert.equal(existsSync(sentinel),false);
const lock = readFileSync(join(consumer,'package-lock.json'),'utf8');
// Ordinary npm hooks may fetch platform dependency assets (e.g. esbuild).
// Cursor itself must still remain absent until its explicit setup command.
isolated(GROK_PUBLIC_INSTALL_LIFECYCLE, { download: true });
assert.equal(readFileSync(sentinel,'utf8'),'ok'); assert.equal(readFileSync(join(consumer,'package-lock.json'),'utf8'),lock);
const installedRunner = join(consumer,'node_modules/@paperclipai/server/dist/vendor/paperclip-runner');
assert.equal(existsSync(join(installedRunner,'provider-assets/cursor')),false,'npm must never download Cursor');
assert.equal(existsSync(join(account.home,'.paperclip/runtimes/cursor')),false,'npm must not provision the account runtime cache');
const setupOutput = isolated(['node','node_modules/paperclipai/dist/index.js','runtime','setup','cursor'],{download:true,readOnlyConsumer:true}).toString();
console.log(setupOutput.trim());
assert.match(setupOutput,/Verified Cursor 2026\.09\.26-dd393fe \(linux-x64\), paperclip-cursor-usage-v4/);
const probe = `import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { userInfo } from 'node:os';
import { join } from 'node:path';
import { defaultCapabilityRunnerdBinary } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/live/runnerd-codex-transport.js';
import { resolvePaperclipRunnerBinary } from '/consumer/node_modules/@paperclipai/server/dist/services/native-runtime/native-codex-runner.js';
import { requireServerAdapter } from '/consumer/node_modules/@paperclipai/server/dist/adapters/registry.js';
import { runnerBinaryTarget } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/live/runner-binary.js';
import { bundledRemoteRunnerBinary, bundledRemoteProviderPackManifestPath } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/live/bundled-remote-provider-pack.js';
import { verifyAcpxProfileInstallation } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/drivers/acpx/profile-installation.js';
import { resolveQualifiedAcpxProfile } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/drivers/acpx/qualified-profiles.js';
import { CURSOR_DISTRIBUTION_PINS } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/drivers/acpx/generated-profiles.js';
const profile = resolveQualifiedAcpxProfile('cursor', 'gpt-5.6-luna[context=272k,reasoning=medium,fast=false]');
const installation = await verifyAcpxProfileInstallation(profile);
assert.equal(userInfo().homedir, '/home/paperclip');
assert.equal(installation.agentServerPackageJsonPath,join(userInfo().homedir,'.paperclip/runtimes/cursor/linux-x64',CURSOR_DISTRIBUTION_PINS['linux-x64'].closureSha256,'.paperclip-cursor-closure.json'));
assert.equal(installation.agentRuntimePackageJsonPath,null);
assert.equal(installation.commandDigest,profile.commandDigest);
await (await installation.openCommand()).close();
const readiness = await requireServerAdapter('paperclip_runner').testEnvironment({ companyId: 'installation-probe', adapterType: 'paperclip_runner', config: { provider: 'acpx', acpxAgent: 'cursor', model: profile.reportedModelId } });
assert.equal(readiness.status, 'pass', JSON.stringify(readiness));
assert.equal(readiness.checks[0]?.code, 'acpx_runtime_ready');
const binaryRoot = '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/bin';
const manifest = JSON.parse(readFileSync(binaryRoot + '/release-manifest.json', 'utf8'));
assert.equal(manifest.schema, 'paperclip.runner.release-binaries.v1');
assert.deepEqual(Object.keys(manifest.platforms).sort(), ['darwin-arm64', 'darwin-x64', 'linux-x64']);
for (const [target, artifact] of Object.entries(manifest.platforms)) {
assert.equal(artifact.path, target + '/paperclip-runnerd');
const bytes = readFileSync(binaryRoot + '/' + artifact.path);
assert.equal(runnerBinaryTarget(bytes), target);
assert.equal('sha256:' + createHash('sha256').update(bytes).digest('hex'), artifact.sha256);
}
const binary = defaultCapabilityRunnerdBinary();
assert.equal(binary, binaryRoot + '/linux-x64/paperclip-runnerd');
assert.equal(resolvePaperclipRunnerBinary(), binary, 'The installed server must use the same verified platform daemon');
assert.equal(bundledRemoteRunnerBinary(), binary);
const remotePack = JSON.parse(readFileSync(bundledRemoteProviderPackManifestPath(), 'utf8'));
assert.equal(remotePack.digest, manifest.remoteProviderPack.digest);
assert.equal(remotePack.payload.providers.cursor.qualification, 'qualified');
const metadata = JSON.parse(execFileSync(binary, ['--build-metadata'], { encoding: 'utf8' }));
assert.equal(metadata.schema, 'paperclip-runner/runnerd-build-metadata/v1');
assert.equal(metadata.binaryContractVersion, 2);
console.log('All three packaged daemon identities verified; the ordinary installed resolver launched Linux x64');
console.log('Installed Cursor closure and command lease verified without credentials or candidate overrides');`;
writeFileSync(join(assets,'probe.mjs'),probe,{mode:0o644});
console.log(isolated(['node','/packages/probe.mjs'],{readOnlyConsumer:true}).toString().trim());
assert.equal(existsSync(join(installedRunner,'provider-assets/cursor')),false,'explicit setup must leave the read-only npm package unchanged');
for (const name of needed) assert.equal(JSON.parse(readFileSync(join(consumer,'node_modules',name,'package.json'))).version,releaseVersion);
const report = {schema:'paperclip.cursor.public-npm-install.v1',sourceRevision,releaseVersion,consumerImage:GROK_PUBLIC_INSTALL_IMAGE,packageCount:needed.size,sourceCliManifestPreserved:readFileSync(cliManifestPath).equals(original),lifecycleScriptsEnabled:true,lifecycleSentinelVerified:true,lifecycleNetwork:'bridge',consumerLockPreserved:true,npmProvisionedCursor:false,publicSetupCommand:true,readOnlyConsumer:true,accountCacheVerified:true,pinnedClosureVerified:true,packagedDaemonTargetsVerified:['darwin-arm64','darwin-x64','linux-x64'],releaseBinaries,ordinaryInstalledDaemonLaunched:true,providerCalls:0};
writeFileSync(join(root,'report.json'),JSON.stringify(report,null,2)+'\n'); console.log(JSON.stringify(report));