mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 23:43:16 +01:00
Merge qualified backup expiration parent into GitHub connection work
Co-Authored-By: Paperclip <noreply@paperclip.ing> * commit 'aae32f2f493848199f6cb8c0a9996af3ee0061b1': (30 commits) Scope status-card refresh tasks to their claim revision test(chat): retain completion identity before link metadata loads test(chat): locate completion through its rendered task link fix(chat): recover committed native Agent Chat replies (#18465) perf(heartbeat): drive retry promotion from durable deadlines (#18387) fix(ui): make the Coordinator composer footer opaque (#18455) fix(testops): verify unexpanded result job runs (#18462) test(server): observe restored Retry before draining unrelated work test(runner): own listener cleanup across setup failures test(runner): buffer upgrade frames until the wire subscribes fix(runner): preserve OpenCode written-answer defaults (#18456) fix(native): fence retries when replaying an audited Stop (#18457) test: await async outcomes and drive saved deadlines deterministically (#18416) fix(ui): retain startup autosave through session recovery (#18412) perf(server): recover independent native replies concurrently (#18421) perf(chat): scope task questions and batch inbox queue checks (#18422) fix(native): recover controllers across webserver replacement (#18343) fix(ui): restore task document authoring and add persistence journey (#18356) fix(tools): stabilize tied profile entry order (#18427) fix(adapter-utils): tolerate entries removed during workspace snapshots (#18426) ...
This commit is contained in:
commit
f522ab07e2
484 files changed
+33834
-1620
No files matched your search
@@ -12,6 +12,10 @@ import {
|
||||
} from "./paperclip-runner-permissions.js";
|
||||
|
||||
describe("Paperclip Runner permission defaults", () => {
|
||||
it("exposes qualified Kimi Code with an explicit runtime credential", () => {
|
||||
expect(PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === "kimi"))
|
||||
.toMatchObject({ qualified: true, credentialEnvironment: ["KIMI_API_KEY"] });
|
||||
});
|
||||
it("marks Pi and Copilot qualified with their provider credential bindings", () => {
|
||||
expect(PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === "pi"))
|
||||
.toMatchObject({ qualified: true, credentialEnvironment: expect.arrayContaining(["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"]) });
|
||||
|
||||
@@ -253,6 +253,7 @@ export const PAPERCLIP_RUNNER_ACPX_PROFILES = Object.freeze([
|
||||
{ value: "cursor", label: "Cursor", qualified: true, credentialEnvironment: ["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"] },
|
||||
{ value: "copilot", label: "GitHub Copilot", qualified: true, credentialEnvironment: ["COPILOT_GITHUB_TOKEN"] },
|
||||
{ value: "pi", label: "Pi", qualified: true, credentialEnvironment: ["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"] },
|
||||
{ value: "kimi", label: "Kimi Code", qualified: true, credentialEnvironment: ["KIMI_API_KEY"] },
|
||||
{ value: "antigravity", label: "Google Antigravity", qualified: false, credentialEnvironment: ["GEMINI_API_KEY"] },
|
||||
] as const);
|
||||
|
||||
|
||||
@@ -50,6 +50,21 @@ it("keeps Pi draft models empty and preserves saved native Pi settings", () => {
|
||||
expect(normalizePaperclipRunnerAdapterConfig("paperclip_runner", saved)).toMatchObject(saved);
|
||||
});
|
||||
|
||||
it.each([undefined, "", "kimi-for-coding"])("keeps Kimi draft model selection explicit: %s", model => {
|
||||
const config = { provider: "acpx", acpxAgent: "kimi", ...(model === undefined ? {} : { model }),
|
||||
acpxPermissionMode: "approve-reads", lifecycleMode: "warm" };
|
||||
const normalized = normalizePaperclipRunnerAdapterConfig("paperclip_runner", config);
|
||||
expect(normalized).toMatchObject(config);
|
||||
expect(normalized.model).toBe(model);
|
||||
});
|
||||
|
||||
it("keeps Claude draft defaults and explicit model selection", () => {
|
||||
expect(normalizePaperclipRunnerAdapterConfig("paperclip_runner", { provider: "acpx", acpxAgent: "claude" }))
|
||||
.toMatchObject({ model: "claude-opus-5-5" });
|
||||
expect(normalizePaperclipRunnerAdapterConfig("paperclip_runner", { provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-5" }))
|
||||
.toMatchObject({ model: "claude-sonnet-5" });
|
||||
});
|
||||
|
||||
it("reserves identity credentials even for mixed-case inherited or configured keys", () => {
|
||||
const keys = ["PAPERCLIP_AGENT_KEY_ID", "Paperclip_Agent_Public_Key", "paperclip_agent_private_key"];
|
||||
const inherited = Object.fromEntries(keys.map(key => [key, "host-override"]));
|
||||
|
||||
@@ -4299,7 +4299,9 @@ export function normalizePaperclipRunnerAdapterConfig(
|
||||
}
|
||||
if (next.provider === "acpx") {
|
||||
next.acpxAgent ??= "claude";
|
||||
if (next.acpxAgent !== "pi") next.model = resolvePaperclipRunnerModel("acpx", config.model);
|
||||
if (next.acpxAgent !== "pi" && next.acpxAgent !== "kimi") {
|
||||
next.model = resolvePaperclipRunnerModel("acpx", config.model);
|
||||
}
|
||||
}
|
||||
return normalizePaperclipOperationalSkillPreference(adapterType, next);
|
||||
}
|
||||
|
||||
@@ -183,6 +183,86 @@ describe("workspace restore merge", () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe("entries removed by a concurrent writer during capture", () => {
|
||||
async function captureWhileRemoving(
|
||||
removeAt: "before-lstat" | "after-lstat",
|
||||
setup: (rootDir: string) => Promise<string>,
|
||||
) {
|
||||
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-restore-merge-"));
|
||||
cleanupDirs.push(rootDir);
|
||||
await writeFile(path.join(rootDir, "kept.txt"), "kept");
|
||||
const vanishing = await setup(rootDir);
|
||||
const lstatSpy = vi.spyOn(fsPromises, "lstat").mockImplementation(async (target, ...rest) => {
|
||||
if (target !== vanishing) return lstat(target, ...rest);
|
||||
if (removeAt === "before-lstat") await rm(vanishing, { recursive: true, force: true });
|
||||
const stats = await lstat(target, ...rest);
|
||||
await rm(vanishing, { recursive: true, force: true });
|
||||
return stats;
|
||||
});
|
||||
try {
|
||||
return await captureDirectorySnapshot(rootDir, { exclude: [] });
|
||||
} finally { lstatSpy.mockRestore(); }
|
||||
}
|
||||
|
||||
it("omits a listed file that is renamed away before it is read", async () => {
|
||||
const snapshot = await captureWhileRemoving("before-lstat", async (rootDir) => {
|
||||
await mkdir(path.join(rootDir, "tests", "runner-e2e"), { recursive: true });
|
||||
const temporary = path.join(rootDir, "tests", "runner-e2e", ".paperclip-merge-d3bcd290-dd7e-47bf-b2cd-7d2eb1d2b7f2");
|
||||
await writeFile(temporary, "in flight");
|
||||
return temporary;
|
||||
});
|
||||
|
||||
expect(snapshot.entries.get("kept.txt")).toMatchObject({ kind: "file" });
|
||||
expect(snapshot.entries.get("tests/runner-e2e")).toEqual({ kind: "dir" });
|
||||
expect(snapshot.entries.has("tests/runner-e2e/.paperclip-merge-d3bcd290-dd7e-47bf-b2cd-7d2eb1d2b7f2")).toBe(false);
|
||||
});
|
||||
|
||||
it("omits a file that is removed before its contents are hashed", async () => {
|
||||
const snapshot = await captureWhileRemoving("after-lstat", async (rootDir) => {
|
||||
const file = path.join(rootDir, "removed.txt");
|
||||
await writeFile(file, "removed");
|
||||
return file;
|
||||
});
|
||||
|
||||
expect(snapshot.entries.get("kept.txt")).toMatchObject({ kind: "file" });
|
||||
expect(snapshot.entries.has("removed.txt")).toBe(false);
|
||||
});
|
||||
|
||||
it("omits the children of a directory that is removed before it is opened", async () => {
|
||||
const snapshot = await captureWhileRemoving("after-lstat", async (rootDir) => {
|
||||
const dir = path.join(rootDir, "removed");
|
||||
await mkdir(dir);
|
||||
await writeFile(path.join(dir, "child.txt"), "child");
|
||||
return dir;
|
||||
});
|
||||
|
||||
expect(snapshot.entries.get("kept.txt")).toMatchObject({ kind: "file" });
|
||||
expect(snapshot.entries.has("removed/child.txt")).toBe(false);
|
||||
});
|
||||
|
||||
it("still fails when the capture root itself is missing", async () => {
|
||||
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-restore-merge-"));
|
||||
cleanupDirs.push(rootDir);
|
||||
|
||||
await expect(captureDirectorySnapshot(path.join(rootDir, "missing"), { exclude: [] }))
|
||||
.rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("still fails on errors other than a vanished entry", async () => {
|
||||
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-restore-merge-"));
|
||||
cleanupDirs.push(rootDir);
|
||||
const unreadable = path.join(rootDir, "unreadable.txt");
|
||||
await writeFile(unreadable, "secret");
|
||||
const lstatSpy = vi.spyOn(fsPromises, "lstat").mockImplementation(async (target, ...rest) => {
|
||||
if (target === unreadable) throw Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" });
|
||||
return lstat(target, ...rest);
|
||||
});
|
||||
try {
|
||||
await expect(captureDirectorySnapshot(rootDir, { exclude: [] })).rejects.toMatchObject({ code: "EACCES" });
|
||||
} finally { lstatSpy.mockRestore(); }
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyWorkspaceRestoreFailure", () => {
|
||||
it.each([
|
||||
"Daytona syncOut refusing tarball with an unparseable entry listing: private listing",
|
||||
|
||||
@@ -144,15 +144,21 @@ async function hashFile(filePath: string): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
const isVanishedEntry = (error: unknown) => (error as NodeJS.ErrnoException | null)?.code === "ENOENT";
|
||||
|
||||
async function* walkDirectory(
|
||||
root: string, exclude: readonly string[], ignored: ReturnType<typeof workspacePathMatcher>, relative = "",
|
||||
): AsyncGenerator<[string, SnapshotEntry]> {
|
||||
const readEntry = async (nextRelative: string): Promise<[string, SnapshotEntry | null]> => {
|
||||
const fullPath = path.join(root, nextRelative);
|
||||
const stats = await fs.lstat(fullPath);
|
||||
if (stats.isDirectory()) return [nextRelative, { kind: "dir" }];
|
||||
if (stats.isSymbolicLink()) return [nextRelative, { kind: "symlink", target: await fs.readlink(fullPath) }];
|
||||
if (stats.isFile()) return [nextRelative, { kind: "file", mode: stats.mode, hash: await hashFile(fullPath) }];
|
||||
try {
|
||||
const stats = await fs.lstat(fullPath);
|
||||
if (stats.isDirectory()) return [nextRelative, { kind: "dir" }];
|
||||
if (stats.isSymbolicLink()) return [nextRelative, { kind: "symlink", target: await fs.readlink(fullPath) }];
|
||||
if (stats.isFile()) return [nextRelative, { kind: "file", mode: stats.mode, hash: await hashFile(fullPath) }];
|
||||
} catch (error) {
|
||||
if (!isVanishedEntry(error)) throw error;
|
||||
}
|
||||
return [nextRelative, null];
|
||||
};
|
||||
const visitBatch = async function* (paths: string[]): AsyncGenerator<[string, SnapshotEntry]> {
|
||||
@@ -167,8 +173,13 @@ async function* walkDirectory(
|
||||
}
|
||||
};
|
||||
const current = relative ? path.join(root, relative) : root;
|
||||
const dir = await fs.opendir(current).catch((error: unknown) => {
|
||||
if (relative && isVanishedEntry(error)) return null;
|
||||
throw error;
|
||||
});
|
||||
if (!dir) return;
|
||||
let batch: string[] = [];
|
||||
for await (const entry of await fs.opendir(current)) {
|
||||
for await (const entry of dir) {
|
||||
const nextRelative = relative ? path.posix.join(relative, entry.name) : entry.name;
|
||||
if (shouldExcludePath(nextRelative, exclude) || ignored.matches(nextRelative)) continue;
|
||||
batch.push(nextRelative);
|
||||
|
||||
@@ -36,6 +36,14 @@ function makeValues(overrides: Partial<CreateConfigValues> = {}): CreateConfigVa
|
||||
}
|
||||
|
||||
describe("buildCodexLocalConfig", () => {
|
||||
it("round-trips qualified Kimi Code with its exact selected model", () => {
|
||||
const first = buildPaperclipRunnerConfig(makeValues({ model: "explicit-kimi-model", adapterSchemaValues: { provider: "acpx", acpxAgent: "kimi" } }));
|
||||
const reopened = buildPaperclipRunnerConfig(makeValues({ model: "", adapterSchemaValues: JSON.parse(JSON.stringify(first)) }));
|
||||
expect(reopened).toMatchObject({ provider: "acpx", acpxAgent: "kimi", model: "explicit-kimi-model" });
|
||||
expect(first).not.toHaveProperty("candidateHarnesses");
|
||||
expect(() => buildPaperclipRunnerConfig(makeValues({ model: "", adapterSchemaValues: { provider: "acpx", acpxAgent: "kimi" } })))
|
||||
.toThrow("kimi requires an explicit provider model");
|
||||
});
|
||||
it("refuses pending Antigravity selection before saving Runner config", () => {
|
||||
expect(() => buildPaperclipRunnerConfig(makeValues({ model: "exact-antigravity-model",
|
||||
adapterSchemaValues: { provider: "acpx", acpxAgent: "antigravity" } })))
|
||||
|
||||
@@ -131,7 +131,7 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues, context?: {
|
||||
if (provider === "acpx" && acpxAgent === "copilot" && (!configuredModel && !schemaModel || ["auto", "default"].includes((configuredModel || schemaModel).toLowerCase()))) {
|
||||
throw new Error("Select an available Copilot model before saving.");
|
||||
}
|
||||
if (provider === "acpx" && ["cursor", "pi"].includes(acpxAgent) && !configuredModel && !schemaModel) {
|
||||
if (provider === "acpx" && ["cursor", "pi", "kimi"].includes(acpxAgent) && !configuredModel && !schemaModel) {
|
||||
throw new Error(`${acpxAgent} requires an explicit provider model`);
|
||||
}
|
||||
const managedProfileId = typeof schemaValues.managedProfileId === "string"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import postgres from "postgres";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyPendingMigrations } from "./client.js";
|
||||
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./test-embedded-postgres.js";
|
||||
|
||||
const support = await getEmbeddedPostgresTestSupport();
|
||||
const indexName = "issue_comments_agent_client_request_uq";
|
||||
const migrationFile = "0419_slippery_impossible_man.sql";
|
||||
|
||||
(support.supported ? describe : describe.skip)("agent chat message migration", () => {
|
||||
it("repairs an interrupted index build and backfills old chat owners only once", async () => {
|
||||
const database = await startEmbeddedPostgresTestDatabase("agent-chat-message-migration-");
|
||||
const sql = postgres(database.connectionString, { max: 1, onnotice: () => {} });
|
||||
try {
|
||||
const content = await readFile(new URL(`./migrations/${migrationFile}`, import.meta.url), "utf8");
|
||||
const hash = createHash("sha256").update(content).digest("hex");
|
||||
await sql`DELETE FROM drizzle.__drizzle_migrations WHERE hash=${hash}`;
|
||||
await sql.unsafe(`ALTER TABLE issue_comments DROP CONSTRAINT ${indexName}`);
|
||||
const companies = await sql`INSERT INTO companies (name,issue_prefix) VALUES ('Chat migration','CMI1'),('Other company','CMI2') RETURNING id`;
|
||||
const companyId = companies[0]!.id;
|
||||
const [agent] = await sql`INSERT INTO agents (company_id,name,adapter_type) VALUES (${companyId},'Kodi','process') RETURNING id`;
|
||||
const chats = await sql`INSERT INTO issues (company_id,title,conversation_agent_id,conversation_user_id,conversation_state,assignee_agent_id)
|
||||
VALUES (${companyId},'Missing read state',${agent!.id},'legacy-owner','waiting',${agent!.id}),
|
||||
(${companyId},'Existing read state',${agent!.id},'existing-owner','waiting',${agent!.id}) RETURNING id,conversation_user_id`;
|
||||
for (const chat of chats) {
|
||||
await sql`INSERT INTO issue_comments (company_id,issue_id,author_agent_id,body,created_at)
|
||||
VALUES (${companyId},${chat.id},${agent!.id},'Existing message','2026-01-02T00:00:00Z')`;
|
||||
}
|
||||
await sql`INSERT INTO issue_read_states (company_id,issue_id,user_id,last_read_at)
|
||||
VALUES (${companyId},${chats[1]!.id},'existing-owner','2026-01-01T00:00:00Z')`;
|
||||
await expect(sql.unsafe(`CREATE UNIQUE INDEX CONCURRENTLY ${indexName} ON companies ((true))`)).rejects.toThrow();
|
||||
await Promise.all([applyPendingMigrations(database.connectionString), applyPendingMigrations(database.connectionString)]);
|
||||
const [index] = await sql`SELECT indisvalid,indisunique FROM pg_index WHERE indexrelid=${indexName}::regclass`;
|
||||
expect(index).toEqual({ indisvalid: true, indisunique: true });
|
||||
const [constraint] = await sql`SELECT count(*)::int count FROM pg_constraint WHERE conname=${indexName} AND conrelid='issue_comments'::regclass`;
|
||||
expect(constraint!.count).toBe(1);
|
||||
const readStates = await sql`SELECT user_id,last_read_at FROM issue_read_states WHERE company_id=${companyId} ORDER BY user_id`;
|
||||
expect(readStates.map(row => [row.user_id, row.last_read_at.toISOString()])).toEqual([
|
||||
["existing-owner", "2026-01-01T00:00:00.000Z"],
|
||||
["legacy-owner", "2026-01-02T00:00:00.000Z"],
|
||||
]);
|
||||
const [newChat] = await sql`INSERT INTO issues (company_id,title,conversation_agent_id,conversation_user_id,conversation_state,assignee_agent_id)
|
||||
VALUES (${companyId},'New unread chat',${agent!.id},'new-owner','waiting',${agent!.id}) RETURNING id`;
|
||||
await sql`INSERT INTO issue_comments (company_id,issue_id,author_agent_id,body)
|
||||
VALUES (${companyId},${newChat!.id},${agent!.id},'New digest')`;
|
||||
for (let replay = 0; replay < 2; replay++) {
|
||||
for (const statement of content.split("--> statement-breakpoint").filter(statement => statement.trim())) {
|
||||
await sql.unsafe(statement);
|
||||
}
|
||||
}
|
||||
expect(await sql`SELECT id FROM issue_read_states WHERE issue_id=${newChat!.id}`).toEqual([]);
|
||||
const afterReplay = await sql`SELECT user_id,last_read_at FROM issue_read_states WHERE company_id=${companyId} ORDER BY user_id`;
|
||||
expect(afterReplay).toEqual(readStates);
|
||||
const [history] = await sql`SELECT count(*)::int count FROM drizzle.__drizzle_migrations WHERE hash=${hash}`;
|
||||
expect(history!.count).toBe(1);
|
||||
} finally {
|
||||
await sql.end();
|
||||
await database.cleanup();
|
||||
}
|
||||
}, 120_000);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { checkpointDb, closeCheckpointDb, closeRegisteredClients, createDb, type Db } from "./client.js";
|
||||
import { checkpointDb, closeCheckpointDb, closeRegisteredClients, createDb, livenessDb, type Db } from "./client.js";
|
||||
|
||||
// postgres.js pools are lazy: these tests exercise actual client lifetimes but
|
||||
// never connect to a database or depend on embedded Postgres availability.
|
||||
@@ -17,6 +17,7 @@ it("fences checkpoints when shutdown precedes the first pool request", async ()
|
||||
it("keeps an already-created checkpoint pool ended after shutdown", async () => {
|
||||
const db = createDb(url);
|
||||
const pool = checkpointDb(db);
|
||||
expect(pool.$client.options.max).toBe(3);
|
||||
await closeCheckpointDb(db, 1);
|
||||
expect(checkpointDb(db)).toBe(pool);
|
||||
await expect(pool.$client`select 1`).rejects.toMatchObject({ code: "CONNECTION_ENDED" });
|
||||
@@ -30,3 +31,16 @@ it("preserves other owner lifetimes and non-owning test-double behavior", async
|
||||
await closeCheckpointDb(nonOwner, 1);
|
||||
expect(checkpointDb(nonOwner)).toBe(nonOwner);
|
||||
});
|
||||
|
||||
it("bounds one cached checkpoint pool per owner without changing request or renewal capacity", () => {
|
||||
const first = createDb(url, { maxConnections: 10 });
|
||||
const second = createDb(url, { maxConnections: 10 });
|
||||
const pool = checkpointDb(first);
|
||||
expect(pool).not.toBe(first);
|
||||
expect(checkpointDb(first)).toBe(pool);
|
||||
expect(checkpointDb(second)).not.toBe(pool);
|
||||
expect(pool.$client.options.max).toBe(3);
|
||||
expect(checkpointDb(second).$client.options.max).toBe(3);
|
||||
expect(first.$client.options.max).toBe(10);
|
||||
expect(livenessDb(first).$client.options.max).toBe(2);
|
||||
});
|
||||
@@ -295,9 +295,10 @@ export async function closeLivenessDb(db: Db, timeoutSeconds: number): Promise<v
|
||||
}
|
||||
|
||||
// Control checkpoints must not occupy the request or lease-renewal pools
|
||||
// during an aligned burst. Share at most two lazy connections per owner.
|
||||
// during an aligned burst. Share at most three lazy connections per owner.
|
||||
const checkpointDbFactories = new WeakMap<object, () => Db>();
|
||||
const checkpointDbs = new WeakMap<object, Db>();
|
||||
const CHECKPOINT_POOL_MAX_CONNECTIONS = 3;
|
||||
const closedCheckpointOwners = new WeakSet<object>();
|
||||
|
||||
/** The bounded controller checkpoint pool; transactions and test doubles keep
|
||||
@@ -342,7 +343,7 @@ function createDbClient(url: string, resolved: DatabaseClientOptions, owner?: ob
|
||||
...resolved, maxConnections: LIVENESS_POOL_MAX_CONNECTIONS, applicationName: "paperclip-controller-lease",
|
||||
}, db));
|
||||
checkpointDbFactories.set(db, () => createDbClient(url, {
|
||||
...resolved, maxConnections: 2, applicationName: "paperclip-runner-checkpoint",
|
||||
...resolved, maxConnections: CHECKPOINT_POOL_MAX_CONNECTIONS, applicationName: "paperclip-runner-checkpoint",
|
||||
}, db));
|
||||
return db;
|
||||
}
|
||||
@@ -564,6 +565,7 @@ const MIGRATIONS_WITH_TOP_LEVEL_STATEMENTS = new Set([
|
||||
"0412_tearful_squadron_supreme.sql",
|
||||
"0413_tired_kronos.sql",
|
||||
"0414_closed_edwin_jarvis.sql",
|
||||
"0419_slippery_impossible_man.sql",
|
||||
|
||||
|
||||
"0384_funny_adam_warlock.sql",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "issue_comments_agent_client_request_uq"
|
||||
ON "issue_comments" ("issue_id", "author_agent_id", "client_request_id");
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'issue_comments_agent_client_request_uq'
|
||||
AND conrelid = 'issue_comments'::regclass
|
||||
) THEN
|
||||
INSERT INTO "issue_read_states" ("company_id", "issue_id", "user_id", "last_read_at")
|
||||
SELECT i."company_id", i."id", i."conversation_user_id", max(c."created_at")
|
||||
FROM "issues" i
|
||||
JOIN "issue_comments" c ON c."company_id" = i."company_id" AND c."issue_id" = i."id"
|
||||
WHERE i."conversation_agent_id" IS NOT NULL AND i."conversation_user_id" IS NOT NULL
|
||||
AND c."deleted_at" IS NULL AND c."author_agent_id" IS NOT NULL
|
||||
AND c."author_type" IS DISTINCT FROM 'system'
|
||||
AND c."presentation" ->> 'kind' IS DISTINCT FROM 'system_notice'
|
||||
AND c."origin" = 'comment'
|
||||
GROUP BY i."company_id", i."id", i."conversation_user_id"
|
||||
ON CONFLICT ("company_id", "issue_id", "user_id") DO NOTHING;
|
||||
ALTER TABLE "issue_comments" ADD CONSTRAINT "issue_comments_agent_client_request_uq"
|
||||
UNIQUE USING INDEX "issue_comments_agent_client_request_uq";
|
||||
END IF;
|
||||
END $$;
|
||||
File renamed without changes.
+10
-7
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"id": "cce00822-b7a4-4acd-9c59-0f6f5822c0cf",
|
||||
"id": "52cf22f3-3bd9-4b0f-8d23-b422bb0d2540",
|
||||
"prevId": "c777fa84-85a6-418f-bea1-0d60b56cf071",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
@@ -37105,6 +37105,15 @@
|
||||
"client_request_id"
|
||||
]
|
||||
},
|
||||
"issue_comments_agent_client_request_uq": {
|
||||
"name": "issue_comments_agent_client_request_uq",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"issue_id",
|
||||
"author_agent_id",
|
||||
"client_request_id"
|
||||
]
|
||||
},
|
||||
"issue_comments_company_id_uq": {
|
||||
"name": "issue_comments_company_id_uq",
|
||||
"nullsNotDistinct": false,
|
||||
@@ -52770,12 +52779,6 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_eval_reason": {
|
||||
"name": "next_eval_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_eval_at": {
|
||||
"name": "next_eval_at",
|
||||
"type": "timestamp with time zone",
|
||||
|
||||
+299
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "dce41524-29a3-4552-ba5d-f0abefbf1ea1",
|
||||
"prevId": "27c9008c-60e3-449b-9630-9e4977fad444",
|
||||
"id": "8f75a1bb-32d9-4aed-9f93-134a6ae71f79",
|
||||
"prevId": "52cf22f3-3bd9-4b0f-8d23-b422bb0d2540",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
@@ -19052,6 +19052,98 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.company_skill_mutation_receipts": {
|
||||
"name": "company_skill_mutation_receipts",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"fingerprint": {
|
||||
"name": "fingerprint",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"skill_id": {
|
||||
"name": "skill_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"version_id": {
|
||||
"name": "version_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"response": {
|
||||
"name": "response",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"company_skill_mutation_receipts_key_idx": {
|
||||
"name": "company_skill_mutation_receipts_key_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "key",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"company_skill_mutation_receipts_company_id_companies_id_fk": {
|
||||
"name": "company_skill_mutation_receipts_company_id_companies_id_fk",
|
||||
"tableFrom": "company_skill_mutation_receipts",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.company_skill_repository_snapshots": {
|
||||
"name": "company_skill_repository_snapshots",
|
||||
"schema": "",
|
||||
@@ -34070,7 +34162,7 @@
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": "\"heartbeat_runs\".\"runtime_mode\" = 'native' AND \"heartbeat_runs\".\"status\" = 'cancelled'\n AND \"heartbeat_runs\".\"runner_profile_json\"->'nativeWorkspaceSync'->>'state' = 'prepared'\n AND \"heartbeat_runs\".\"result_json\"->'nativeCancellation'->>'dispatchState' = 'acknowledged'\n AND (\"heartbeat_runs\".\"result_json\"->'workspaceRestoreRecovery'->>'schema' = 'paperclip.workspace-restore-recovery.v1'\n AND \"heartbeat_runs\".\"result_json\"->>'workspaceRestoreFailure' IN ('restore_permission_denied', 'restore_lock_timeout', 'restore_unsafe_archive', 'restore_failed')) IS NOT TRUE",
|
||||
"where": "\"heartbeat_runs\".\"runtime_mode\" = 'native' AND \"heartbeat_runs\".\"status\" = 'cancelled'\n AND \"heartbeat_runs\".\"runner_profile_json\"->'nativeWorkspaceSync'->>'state' = 'prepared'\n AND \"heartbeat_runs\".\"result_json\"->'nativeCancellation'->>'dispatchState' = 'acknowledged'\n AND (\"heartbeat_runs\".\"result_json\"->'workspaceRestoreRecovery'->>'schema' = 'paperclip.workspace-restore-recovery.v1'\n AND \"heartbeat_runs\".\"result_json\"->>'workspaceRestoreFailure' IN ('restore_permission_denied', 'restore_lock_timeout', 'restore_unsafe_archive', 'restore_invalid_metadata', 'restore_failed')) IS NOT TRUE",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
@@ -37013,6 +37105,15 @@
|
||||
"client_request_id"
|
||||
]
|
||||
},
|
||||
"issue_comments_agent_client_request_uq": {
|
||||
"name": "issue_comments_agent_client_request_uq",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"issue_id",
|
||||
"author_agent_id",
|
||||
"client_request_id"
|
||||
]
|
||||
},
|
||||
"issue_comments_company_id_uq": {
|
||||
"name": "issue_comments_company_id_uq",
|
||||
"nullsNotDistinct": false,
|
||||
@@ -51482,6 +51583,195 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.sandbox_creation_operations": {
|
||||
"name": "sandbox_creation_operations",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"environment_id": {
|
||||
"name": "environment_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"original_environment_id": {
|
||||
"name": "original_environment_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"request_key": {
|
||||
"name": "request_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'run'"
|
||||
},
|
||||
"issue_id": {
|
||||
"name": "issue_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"execution_workspace_id": {
|
||||
"name": "execution_workspace_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"plugin_id": {
|
||||
"name": "plugin_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"driver_key": {
|
||||
"name": "driver_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"lease_id": {
|
||||
"name": "lease_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_attempt_at": {
|
||||
"name": "next_attempt_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"sandbox_creation_operations_pending_deadline_idx": {
|
||||
"name": "sandbox_creation_operations_pending_deadline_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "next_attempt_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": "\"sandbox_creation_operations\".\"state\" = 'pending'",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"sandbox_creation_operations_owner_idx": {
|
||||
"name": "sandbox_creation_operations_owner_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "original_environment_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "run_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "request_key",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"sandbox_creation_operations_environment_id_environments_id_fk": {
|
||||
"name": "sandbox_creation_operations_environment_id_environments_id_fk",
|
||||
"tableFrom": "sandbox_creation_operations",
|
||||
"tableTo": "environments",
|
||||
"columnsFrom": [
|
||||
"environment_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.secret_access_events": {
|
||||
"name": "secret_access_events",
|
||||
"schema": "",
|
||||
@@ -52489,6 +52779,12 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_eval_reason": {
|
||||
"name": "next_eval_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_eval_at": {
|
||||
"name": "next_eval_at",
|
||||
"type": "timestamp with time zone",
|
||||
+9
-2
@@ -2924,8 +2924,15 @@
|
||||
{
|
||||
"idx": 419,
|
||||
"version": "7",
|
||||
"when": 1793402408013,
|
||||
"tag": "0419_moaning_vulture",
|
||||
"when": 1793400383797,
|
||||
"tag": "0419_slippery_impossible_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 420,
|
||||
"version": "7",
|
||||
"when": 1793453735600,
|
||||
"tag": "0420_broad_johnny_storm",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -49,6 +49,7 @@ export const issueComments = pgTable(
|
||||
},
|
||||
(table) => ({
|
||||
clientRequestUq: unique("issue_comments_client_request_uq").on(table.issueId, table.authorUserId, table.clientRequestId),
|
||||
agentClientRequestUq: unique("issue_comments_agent_client_request_uq").on(table.issueId, table.authorAgentId, table.clientRequestId),
|
||||
companyIdUq: unique("issue_comments_company_id_uq").on(table.companyId, table.id),
|
||||
issueIdx: index("issue_comments_issue_idx").on(table.issueId),
|
||||
reviewResponseReceiptIdx: index("issue_comments_review_response_receipt_idx").on(
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import postgres from "postgres";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyPendingMigrations, inspectMigrations } from "./client.js";
|
||||
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./test-embedded-postgres.js";
|
||||
|
||||
const adminUrl = process.env.PAPERCLIP_TEST_POSTGRES_ADMIN_URL;
|
||||
const support = adminUrl ? { supported: true } : await getEmbeddedPostgresTestSupport();
|
||||
const journal = JSON.parse(await readFile(new URL("./migrations/meta/_journal.json", import.meta.url), "utf8")) as {
|
||||
entries: Array<{ idx: number; tag: string }>;
|
||||
};
|
||||
const master = journal.entries.find(entry => entry.tag === "0419_slippery_impossible_man")!;
|
||||
const status = journal.entries.find(entry => entry.tag === "0420_broad_johnny_storm")!;
|
||||
const statusSql = await readFile(new URL(`./migrations/${status.tag}.sql`, import.meta.url), "utf8");
|
||||
const hash = async (tag: string) => createHash("sha256")
|
||||
.update(await readFile(new URL(`./migrations/${tag}.sql`, import.meta.url), "utf8")).digest("hex");
|
||||
|
||||
async function database() {
|
||||
if (!adminUrl) return startEmbeddedPostgresTestDatabase("status-reason-upgrade-");
|
||||
const name = `status_reason_${randomUUID().replaceAll("-", "")}_test`;
|
||||
const admin = postgres(adminUrl, { max: 1, onnotice: () => {} });
|
||||
try { await admin.unsafe(`create database "${name}"`); }
|
||||
finally { await admin.end(); }
|
||||
const url = new URL(adminUrl); url.pathname = `/${name}`;
|
||||
const cleanup = async () => {
|
||||
const admin = postgres(adminUrl, { max: 1, onnotice: () => {} });
|
||||
try { await admin.unsafe(`drop database "${name}"`); }
|
||||
finally { await admin.end(); }
|
||||
};
|
||||
try { await applyPendingMigrations(url.toString()); }
|
||||
catch (error) { await cleanup(); throw error; }
|
||||
return { connectionString: url.toString(), cleanup };
|
||||
}
|
||||
|
||||
async function seed(sql: postgres.Sql) {
|
||||
const companyId = randomUUID(), agentId = randomUUID(), issueId = randomUUID(), userId = randomUUID();
|
||||
const lastMessage = new Date("2026-10-20T12:00:00.000Z");
|
||||
await sql`insert into companies (id, name, issue_prefix) values (${companyId}, 'Migration proof', ${`M${companyId.slice(0, 10)}`})`;
|
||||
await sql`insert into agents (id, company_id, name, adapter_type) values (${agentId}, ${companyId}, 'Agent', 'process')`;
|
||||
await sql`insert into "user" (id, name, email, email_verified, created_at, updated_at)
|
||||
values (${userId}, 'Reader', ${`${userId}@example.test`}, true, now(), now())`;
|
||||
await sql`insert into issues (id, company_id, title, status, assignee_agent_id,
|
||||
conversation_agent_id, conversation_user_id, conversation_state)
|
||||
values (${issueId}, ${companyId}, 'Conversation', 'in_review', ${agentId}, ${agentId}, ${userId}, 'waiting')`;
|
||||
await sql`insert into issue_comments (company_id, issue_id, author_agent_id, author_type, body, origin, client_request_id, created_at)
|
||||
values (${companyId}, ${issueId}, ${agentId}, 'agent', 'Preserved reply', 'comment', 'retry-key', ${lastMessage})`;
|
||||
await sql`insert into issue_comments (company_id, issue_id, author_agent_id, author_type, body, origin, created_at)
|
||||
values (${companyId}, ${issueId}, ${agentId}, 'system', 'Excluded system notice', 'comment', ${new Date(lastMessage.getTime() + 1000)})`;
|
||||
const cardId = randomUUID(), savedDeadline = new Date("2026-10-21T12:00:00.000Z");
|
||||
await sql`insert into status_cards (id, company_id, interest_prompt, refresh_policy, next_eval_at, pending_change_hash)
|
||||
values (${cardId}, ${companyId}, 'Watch preserved work', '{"mode":"manual"}'::jsonb, ${savedDeadline}, 'preserved-change')`;
|
||||
return { companyId, agentId, issueId, userId, lastMessage, cardId, savedDeadline };
|
||||
}
|
||||
|
||||
async function assertFinal(sql: postgres.Sql, row: Awaited<ReturnType<typeof seed>>, expectedRead: Date) {
|
||||
const [column] = await sql`select data_type, is_nullable from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'status_cards' and column_name = 'next_eval_reason'`;
|
||||
expect(column).toMatchObject({ data_type: "text", is_nullable: "YES" });
|
||||
const [constraint] = await sql`select i.indisvalid, i.indisunique from pg_constraint c
|
||||
join pg_index i on i.indexrelid = c.conindid
|
||||
where c.conrelid = 'issue_comments'::regclass and c.conname = 'issue_comments_agent_client_request_uq'`;
|
||||
expect(constraint).toMatchObject({ indisvalid: true, indisunique: true });
|
||||
const [read] = await sql`select last_read_at from issue_read_states
|
||||
where company_id = ${row.companyId} and issue_id = ${row.issueId} and user_id = ${row.userId}`;
|
||||
expect(read.last_read_at).toEqual(expectedRead);
|
||||
const [card] = await sql`select next_eval_at, next_eval_reason, pending_change_hash from status_cards where id = ${row.cardId}`;
|
||||
expect(card).toMatchObject({ next_eval_at: row.savedDeadline, next_eval_reason: null, pending_change_hash: "preserved-change" });
|
||||
await sql`update status_cards set next_eval_reason = 'retry' where id = ${row.cardId}`;
|
||||
expect((await sql`select next_eval_at, next_eval_reason from status_cards where id = ${row.cardId}`)[0])
|
||||
.toMatchObject({ next_eval_at: row.savedDeadline, next_eval_reason: "retry" });
|
||||
await expect(sql`insert into issue_comments (company_id, issue_id, author_agent_id, body, client_request_id)
|
||||
values (${row.companyId}, ${row.issueId}, ${row.agentId}, 'Duplicate', 'retry-key')`)
|
||||
.rejects.toMatchObject({ code: "23505", constraint_name: "issue_comments_agent_client_request_uq" });
|
||||
}
|
||||
|
||||
describe.skipIf(!support.supported)("status-card reason migration after the published comment migration", () => {
|
||||
it.each(["0418", "master0419"] as const)("upgrades from %s without dropping comment uniqueness or read history", async previous => {
|
||||
expect(master.idx).toBe(419);
|
||||
expect(status.idx).toBeGreaterThan(master.idx);
|
||||
expect(statusSql.trim()).toBe('ALTER TABLE "status_cards" ADD COLUMN "next_eval_reason" text;');
|
||||
const db = await database();
|
||||
const sql = postgres(db.connectionString, { max: 1, onnotice: () => {} });
|
||||
try {
|
||||
// Reconstruct exactly the two migrations' preimages; all older schema and
|
||||
// receipts stay intact, as in the repository's other replay regressions.
|
||||
await sql`alter table status_cards drop column next_eval_reason`;
|
||||
await sql`delete from drizzle.__drizzle_migrations where hash = ${await hash(status.tag)}`;
|
||||
if (previous === "0418") {
|
||||
await sql`alter table issue_comments drop constraint issue_comments_agent_client_request_uq`;
|
||||
await sql`delete from drizzle.__drizzle_migrations where hash = ${await hash(master.tag)}`;
|
||||
}
|
||||
const row = await seed(sql);
|
||||
const preservedRead = new Date("2026-10-19T12:00:00.000Z");
|
||||
if (previous === "master0419") await sql`insert into issue_read_states (company_id, issue_id, user_id, last_read_at)
|
||||
values (${row.companyId}, ${row.issueId}, ${row.userId}, ${preservedRead})`;
|
||||
expect(await inspectMigrations(db.connectionString)).toMatchObject({
|
||||
status: "needsMigrations",
|
||||
pendingMigrations: previous === "0418" ? [`${master.tag}.sql`, `${status.tag}.sql`] : [`${status.tag}.sql`],
|
||||
});
|
||||
await applyPendingMigrations(db.connectionString);
|
||||
expect(await inspectMigrations(db.connectionString)).toMatchObject({ status: "upToDate" });
|
||||
await assertFinal(sql, row, previous === "0418" ? row.lastMessage : preservedRead);
|
||||
const receipts = await sql`select hash from drizzle.__drizzle_migrations order by id`;
|
||||
const hashes = receipts.map(receipt => receipt.hash);
|
||||
expect(hashes.indexOf(await hash(master.tag))).toBeLessThan(hashes.indexOf(await hash(status.tag)));
|
||||
await applyPendingMigrations(db.connectionString);
|
||||
expect(await sql`select hash from drizzle.__drizzle_migrations order by id`).toEqual(receipts);
|
||||
} finally { await sql.end(); await db.cleanup(); }
|
||||
}, 90_000);
|
||||
});
|
||||
@@ -311,7 +311,8 @@ installation journey or a paid provider task on ARM64.
|
||||
Automatic creation selects legacy execution when a Linux host has musl, unknown
|
||||
libc, or a measured GNU libc version below the baseline. Existing agents retain
|
||||
their stored runner. Windows, 32-bit ARM, and Alpine/musl native qualification
|
||||
remain deferred.
|
||||
remain deferred. The `Runner Windows` workflow builds and tests the Rust
|
||||
workspace on Windows x64. It does not qualify Windows agent runs.
|
||||
|
||||
Native release jobs cache Cargo dependencies per platform and OS baseline,
|
||||
including compiler, lockfile, and external generated-contract inputs. Workspace
|
||||
@@ -484,6 +485,12 @@ stable free-text option IDs. Legacy callers remain supported. Calls that supply
|
||||
both forms must describe the same complete form; partial forms remain invalid.
|
||||
The native recovery bridge uses the same projection for answer delivery.
|
||||
|
||||
OpenCode 1.18.34 native questions allow a written answer by default when `custom`
|
||||
is omitted. The driver preserves that default and explicit `custom:false`; the
|
||||
native flag takes precedence over the legacy `allowCustom` alias. Choice options
|
||||
remain intact. Questions without options remain text-only, without a separate
|
||||
custom-answer field. See the [pinned native question contract](https://github.com/anomalyco/opencode/blob/v1.18.34/packages/schema/src/v1/question.ts).
|
||||
|
||||
The server validates canonical answer constraints before persistence. Regex
|
||||
matching runs in isolated workers with a one-second deadline and at most four
|
||||
active workers. A timeout or capacity error leaves the question pending. The
|
||||
@@ -980,6 +987,39 @@ while daemon work might still be running. The socket and its parent must be priv
|
||||
canonical paths owned by the same account. Local stdio assignment requires Unix;
|
||||
other platforms retain the HTTP path.
|
||||
|
||||
## Kimi Code
|
||||
|
||||
Kimi Code uses the pinned `@moonshot-ai/kimi-code@2.3.3` distribution through
|
||||
ACPX through the qualified native profile. Ordinary selection and Runtime Test
|
||||
require no candidate grant.
|
||||
The installation targets are macOS ARM64 and Linux x64. Platform startup,
|
||||
authenticated driver checks, and product acceptance are separate evidence levels.
|
||||
|
||||
Run `paperclipai runtime setup kimi` on the execution host to install its complete
|
||||
verified runtime. Setup downloads public pinned dependencies without model calls
|
||||
or provider credentials. An invalid existing installation fails without ambient
|
||||
Kimi fallback. Bind a company secret as `KIMI_API_KEY` and select an exact model.
|
||||
This release uses the global Code endpoint at `https://api.kimi.ai/coding/v1`.
|
||||
Model suggestions are empty; a compatible typed model is decided by the provider.
|
||||
The account needs Kimi Code entitlement. Runtime Test verifies installation and
|
||||
queries the authenticated Code model catalog for the exact selected model. It
|
||||
sends no model prompt; an actual task verifies inference and tool execution.
|
||||
|
||||
The qualified native profile uses ordinary setup, hiring, metadata Test and task
|
||||
startup on these two supported targets without operator admission flags. Protected
|
||||
candidate startup records remain reserved for future pending profiles; saved agent
|
||||
configuration and public requests cannot supply that authority.
|
||||
|
||||
Existing legacy Kimi agents retain their runtime and history. Managed Kimi connections,
|
||||
ambient login borrowing, other regions/platforms, steering, native child work,
|
||||
and per-run token/dollar accounting are outside this release. Read-only Kimi work
|
||||
uses scoped Paperclip tools; native filesystem and shell tools are denied.
|
||||
The provider-owned patch binds native instructions, preserves permission arguments,
|
||||
and removes provider auth from tool subprocesses. See the
|
||||
[protocol dossier](src/providers/kimi/README.md) and
|
||||
[release ledger](../../doc/plans/2026-10-28-kimi-code-runner-integration.md) for
|
||||
current evidence and limits.
|
||||
|
||||
## ACPX controller replacement regression tests
|
||||
|
||||
`src/control-plane/acpx-controller-recovery.test.ts` runs the production Rust
|
||||
|
||||
@@ -67,6 +67,15 @@
|
||||
"agentRuntimeVersion": "0.160.0",
|
||||
"commandDigest": "sha256:443491f4e814501a5dc47347e4510e84faeec9c9382e2d4e53bfe69386d5aabc",
|
||||
"requiresProviderPolicy": false
|
||||
},
|
||||
"kimi": {
|
||||
"agentProfileVersion": 17,
|
||||
"agentServerPackage": "@moonshot-ai/kimi-code",
|
||||
"agentServerVersion": "2.3.3",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:6ee5fb7ac18c1361ac63effe9117d08ed129add8349756a140e73e0c86ba5da7",
|
||||
"requiresProviderPolicy": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ Each provider declares how in `ACPX_RELEASE_ATTESTATIONS`
|
||||
| --- | --- | --- |
|
||||
| `installed-command` | Claude, Codex | the upstream package's launch command, hashed at installation |
|
||||
| `launcher` | Grok | the SHA-256 of the launcher shipped verbatim |
|
||||
| `closure` | Copilot, Pi | upstream versions and the complete platform closure pins |
|
||||
| `closure` | Copilot, Pi, Kimi | upstream versions and the complete platform closure pins |
|
||||
| `declaration` | Cursor, Antigravity | a checked-in release declaration, as canonical JSON |
|
||||
|
||||
`generate:acpx-profiles` writes the computed digests and `check:acpx-profiles`
|
||||
@@ -127,6 +127,12 @@ An administrator-owned read-only Runner can therefore use a user-writable cache,
|
||||
but this provides weaker protection against that same user than hashing every
|
||||
executable on each admission. Removing the cache restores full hashing.
|
||||
|
||||
Kimi retains released revision 17, which separates ordinary admission from
|
||||
pending revision 16. Its upstream-only release attestation replaces the earlier
|
||||
source-bound digest without another revision bump. Historical candidate and
|
||||
released records remain unchanged and readable; exact current digest admission
|
||||
refuses their old launch identities.
|
||||
|
||||
## External controller checkpoints
|
||||
|
||||
The optional controller persistence callback stores recovery control state, not
|
||||
|
||||
@@ -1 +1 @@
|
||||
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
|
||||
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
|
||||
@@ -533,6 +533,45 @@
|
||||
"title": "Report durable progress",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"allowedModes": [
|
||||
"standard"
|
||||
],
|
||||
"description": "Send a Markdown message directly to your own persistent chat with this task's responsible human. The destination is automatic. Use for requested digests and proactive task updates; report_progress writes to this task and comment_on_task writes to a chosen task. This sends no message to other agents, starts no chat run, and changes no task status. Reuse idempotencyKey on retries.",
|
||||
"effect": "write",
|
||||
"inputSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"body": {
|
||||
"description": "The message the human should see.",
|
||||
"maxLength": 20000,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"idempotencyKey": {
|
||||
"description": "Stable key for this message within the source task, including across run retries.",
|
||||
"maxLength": 240,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"body",
|
||||
"idempotencyKey"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"operationId": "send_chat_message",
|
||||
"outputSchema": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"placement": "always",
|
||||
"requiredClaims": [],
|
||||
"schema": "paperclip.semantic-action.v1",
|
||||
"title": "Send message to your chat",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"allowedModes": [
|
||||
"standard",
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
"typecheck:browser": "tsc -p tsconfig.browser.json --noEmit",
|
||||
"test": "pnpm run test:typescript && pnpm run test:rust",
|
||||
"test:typescript": "pnpm run test:typescript:prep && vitest run",
|
||||
"test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node ../../scripts/ci-profile/node-test.mjs test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-model-selection-package-contract.test.mjs test/acpx-message-boundaries-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/materialize-pi-distribution.test.mjs scripts/cursor-provisioner-layout.test.mjs scripts/release-provider-pack.test.mjs scripts/acpx-release-attestations.test.mjs scripts/provider-pack-runtime-tree.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/installed-package-pack.test.mjs scripts/runner-runtime-dependency-roots.test.mjs scripts/runner-package-executable.test.mjs scripts/stage-runner-binary.test.mjs scripts/build-inputs.test.mjs scripts/contracts/contract-fingerprints.test.mjs scripts/provider-pack-executable-shims.test.mjs scripts/controlled-evals/controller.test.mjs scripts/controlled-evals/portable.test.mjs && node --test scripts/build-copilot-distribution.test.mjs scripts/copilot-inner-distribution.test.mjs scripts/copilot-smoke-resources.test.mjs scripts/discover-copilot-acp.test.mjs scripts/materialize-copilot-binary.test.mjs scripts/install-copilot-assets.test.mjs scripts/probe-copilot-acp.test.mjs scripts/qualify-copilot-acp.test.mjs test/acpx-message-boundaries-package-contract.test.mjs",
|
||||
"test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node ../../scripts/ci-profile/node-test.mjs test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-model-selection-package-contract.test.mjs test/acpx-message-boundaries-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/materialize-pi-distribution.test.mjs scripts/cursor-provisioner-layout.test.mjs scripts/release-provider-pack.test.mjs scripts/acpx-release-attestations.test.mjs scripts/provider-pack-runtime-tree.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/materialize-kimi-distribution.test.mjs scripts/provision-kimi-package.test.mjs scripts/kimi-metadata-probe.test.mjs scripts/patch-kimi-runtime.test.mjs test/kimi-installed-runtime.test.mjs scripts/installed-package-pack.test.mjs scripts/runner-runtime-dependency-roots.test.mjs scripts/runner-package-executable.test.mjs scripts/stage-runner-binary.test.mjs scripts/build-inputs.test.mjs scripts/contracts/contract-fingerprints.test.mjs scripts/provider-pack-executable-shims.test.mjs scripts/controlled-evals/controller.test.mjs scripts/controlled-evals/portable.test.mjs && node --test scripts/build-copilot-distribution.test.mjs scripts/copilot-inner-distribution.test.mjs scripts/copilot-smoke-resources.test.mjs scripts/discover-copilot-acp.test.mjs scripts/materialize-copilot-binary.test.mjs scripts/install-copilot-assets.test.mjs scripts/probe-copilot-acp.test.mjs scripts/qualify-copilot-acp.test.mjs test/acpx-message-boundaries-package-contract.test.mjs",
|
||||
"test:typescript:vitest": "node ./scripts/run-pr-vitest-lane.mjs",
|
||||
"test:rust": "cargo test --release --manifest-path runner/Cargo.toml --locked --workspace",
|
||||
"test:codex": "cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --test codex_provider",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"prpVersion": 1,
|
||||
"nativeExecutionVersion": 1,
|
||||
"catalogVersion": 1,
|
||||
"catalogSha256": "sha256:015531b07c9906c6edfa6029890cc7b2c69435c1c0c37432ffe21459e6f15c43",
|
||||
"catalogSha256": "sha256:bb6aec4a12397e82d4f881fbce993e796cb8e8cc6852b29b308fc01ade7ef3d2",
|
||||
"driverContractVersion": 1,
|
||||
"driverKind": "paperclip-deterministic",
|
||||
"driverVersion": "1.0.0"
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
},
|
||||
{
|
||||
"path": "fixtures/evals/native-execution-seeded.json",
|
||||
"sha256": "0b878d30ad4b5514a14f7387c3792662cbfeebc7149188ffe304197e41c20e40",
|
||||
"sha256": "810659b1b73ed62e70598bee5cc693d5632471c0f457788167f9b8eecb0d0022",
|
||||
"expectation": "accept",
|
||||
"compatibilityCase": "canonical"
|
||||
},
|
||||
|
||||
+221
-12
@@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::generated_acpx_profiles::acpx_release_profile;
|
||||
use crate::generated_acpx_profiles::{acpx_release_profile, kimi_native_closure};
|
||||
use crate::local_runner::LocalRunnerError;
|
||||
use crate::stable_identity::{is_stable_id, DURABLE_STABLE_ID_CHARS, SHORT_STABLE_ID_CHARS};
|
||||
|
||||
@@ -63,15 +63,41 @@ impl AcpxCandidateAdmission {
|
||||
}
|
||||
|
||||
fn validate_at(&self, now: u64, os: &str, architecture: &str) -> Result<(), LocalRunnerError> {
|
||||
let profile = acpx_release_profile("antigravity").ok_or_else(invalid)?;
|
||||
let manifest: Value = serde_json::from_str(DISTRIBUTIONS).map_err(|_| invalid())?;
|
||||
let closure = manifest
|
||||
.pointer("/platforms/darwin-arm64/closureSha256")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(invalid)?;
|
||||
let profile = acpx_release_profile(&self.agent).ok_or_else(invalid)?;
|
||||
let (closure, host_matches) = match self.agent.as_str() {
|
||||
"antigravity" => {
|
||||
let manifest: Value = serde_json::from_str(DISTRIBUTIONS).map_err(|_| invalid())?;
|
||||
let closure = manifest
|
||||
.pointer("/platforms/darwin-arm64/closureSha256")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(invalid)?
|
||||
.to_owned();
|
||||
(
|
||||
closure,
|
||||
self.platform == "darwin"
|
||||
&& self.architecture == "arm64"
|
||||
&& os == "macos"
|
||||
&& architecture == "aarch64",
|
||||
)
|
||||
}
|
||||
"kimi" if profile.qualification_pending => {
|
||||
let closure =
|
||||
kimi_native_closure(&self.platform, &self.architecture).ok_or_else(invalid)?;
|
||||
let host_matches = matches!(
|
||||
(
|
||||
self.platform.as_str(),
|
||||
self.architecture.as_str(),
|
||||
os,
|
||||
architecture
|
||||
),
|
||||
("darwin", "arm64", "macos", "aarch64") | ("linux", "x64", "linux", "x86_64")
|
||||
);
|
||||
(closure.to_owned(), host_matches)
|
||||
}
|
||||
_ => return Err(invalid()),
|
||||
};
|
||||
if self.schema != SCHEMA
|
||||
|| !is_stable_id(&self.company_id, DURABLE_STABLE_ID_CHARS)
|
||||
|| self.agent != "antigravity"
|
||||
|| self.model.trim().is_empty()
|
||||
|| self.model != self.model.trim()
|
||||
|| self.model.len() > 512
|
||||
@@ -79,10 +105,7 @@ impl AcpxCandidateAdmission {
|
||||
|| matches!(self.model.to_ascii_lowercase().as_str(), "auto" | "default")
|
||||
|| self.profile_digest != profile.command_digest
|
||||
|| self.native_closure_digest != format!("sha256:{closure}")
|
||||
|| self.platform != "darwin"
|
||||
|| self.architecture != "arm64"
|
||||
|| os != "macos"
|
||||
|| architecture != "aarch64"
|
||||
|| !host_matches
|
||||
|| self.expires_at > MAX_JSON_SAFE_INTEGER
|
||||
|| self.expires_at <= now
|
||||
|| self.expires_at > now.saturating_add(MAX_LIFETIME_MS)
|
||||
@@ -94,6 +117,27 @@ impl AcpxCandidateAdmission {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_next_run(
|
||||
&self,
|
||||
next: &Self,
|
||||
agent: &str,
|
||||
model: &str,
|
||||
run_id: &str,
|
||||
normalized_session_id: &str,
|
||||
) -> Result<(), LocalRunnerError> {
|
||||
if self.agent != "kimi" || self.run_id == next.run_id {
|
||||
return Err(invalid());
|
||||
}
|
||||
self.validate()?;
|
||||
next.validate_binding(agent, model, run_id, normalized_session_id)?;
|
||||
let mut rebound = next.clone();
|
||||
rebound.run_id = self.run_id.clone();
|
||||
if &rebound != self {
|
||||
return Err(invalid());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_binding(
|
||||
&self,
|
||||
agent: &str,
|
||||
@@ -208,4 +252,169 @@ mod tests {
|
||||
let error = AcpxCandidateAdmission::parse_json("private malformed input").unwrap_err();
|
||||
assert!(!error.to_string().contains("private"));
|
||||
}
|
||||
|
||||
fn kimi_admission(platform: &str, architecture: &str) -> AcpxCandidateAdmission {
|
||||
AcpxCandidateAdmission {
|
||||
agent: "kimi".to_owned(),
|
||||
model: "kimi-for-coding".to_owned(),
|
||||
profile_digest: acpx_release_profile("kimi")
|
||||
.unwrap()
|
||||
.command_digest
|
||||
.to_owned(),
|
||||
native_closure_digest: format!(
|
||||
"sha256:{}",
|
||||
kimi_native_closure(platform, architecture).unwrap()
|
||||
),
|
||||
platform: platform.to_owned(),
|
||||
architecture: architecture.to_owned(),
|
||||
..test_admission("kimi-run", "kimi-session")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn released_kimi_rejects_candidate_records_on_both_native_platforms() {
|
||||
assert!(!acpx_release_profile("kimi").unwrap().qualification_pending);
|
||||
for (platform, architecture, os, host_arch) in [
|
||||
("darwin", "arm64", "macos", "aarch64"),
|
||||
("linux", "x64", "linux", "x86_64"),
|
||||
] {
|
||||
let candidate = kimi_admission(platform, architecture);
|
||||
let now = candidate.expires_at - 1;
|
||||
assert!(candidate.validate_at(now, os, host_arch).is_err());
|
||||
for (wrong_os, wrong_arch) in [
|
||||
("windows", "x86_64"),
|
||||
("linux", "aarch64"),
|
||||
("macos", "x86_64"),
|
||||
] {
|
||||
assert!(candidate.validate_at(now, wrong_os, wrong_arch).is_err());
|
||||
}
|
||||
let (other_os, other_arch) = if os == "macos" {
|
||||
("linux", "x86_64")
|
||||
} else {
|
||||
("macos", "aarch64")
|
||||
};
|
||||
assert!(candidate.validate_at(now, other_os, other_arch).is_err());
|
||||
assert!(candidate
|
||||
.validate_at(candidate.expires_at, os, host_arch)
|
||||
.is_err());
|
||||
assert!(candidate
|
||||
.validate_at(candidate.expires_at - MAX_LIFETIME_MS - 1, os, host_arch)
|
||||
.is_err());
|
||||
for (field, value) in [
|
||||
("companyId", json!("bad company")),
|
||||
("agent", json!("copilot")),
|
||||
("model", json!("default")),
|
||||
("profileDigest", json!("sha256:wrong")),
|
||||
("nativeClosureDigest", json!("sha256:wrong")),
|
||||
("platform", json!("windows")),
|
||||
("architecture", json!("s390x")),
|
||||
("runId", json!("bad/run")),
|
||||
("normalizedSessionId", json!("bad/session")),
|
||||
] {
|
||||
let mut changed = serde_json::to_value(&candidate).unwrap();
|
||||
changed[field] = value;
|
||||
let changed: AcpxCandidateAdmission = serde_json::from_value(changed).unwrap();
|
||||
assert!(changed.validate_at(now, os, host_arch).is_err(), "{field}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(
|
||||
all(target_os = "macos", target_arch = "aarch64"),
|
||||
all(target_os = "linux", target_arch = "x86_64")
|
||||
))]
|
||||
fn released_kimi_rejects_candidate_rebinding_and_startup_records() {
|
||||
assert!(!acpx_release_profile("kimi").unwrap().qualification_pending);
|
||||
let (platform, architecture) = if cfg!(target_os = "macos") {
|
||||
("darwin", "arm64")
|
||||
} else {
|
||||
("linux", "x64")
|
||||
};
|
||||
let current = kimi_admission(platform, architecture);
|
||||
let mut next = current.clone();
|
||||
next.run_id = "kimi-next-run".to_owned();
|
||||
assert!(current.validate().is_err());
|
||||
assert!(
|
||||
AcpxCandidateAdmission::parse_json(&serde_json::to_string(¤t).unwrap()).is_err()
|
||||
);
|
||||
assert!(current
|
||||
.validate_next_run(
|
||||
&next,
|
||||
"kimi",
|
||||
"kimi-for-coding",
|
||||
"kimi-next-run",
|
||||
"kimi-session",
|
||||
)
|
||||
.is_err());
|
||||
assert!(current
|
||||
.validate_next_run(
|
||||
¤t,
|
||||
"kimi",
|
||||
"kimi-for-coding",
|
||||
"kimi-run",
|
||||
"kimi-session"
|
||||
)
|
||||
.is_err());
|
||||
for (field, change) in [
|
||||
("companyId", json!("another-company")),
|
||||
("model", json!("another-model")),
|
||||
("profileDigest", json!("sha256:wrong")),
|
||||
("nativeClosureDigest", json!("sha256:wrong")),
|
||||
("normalizedSessionId", json!("another-session")),
|
||||
("expiresAt", json!(next.expires_at + 1)),
|
||||
] {
|
||||
let mut value = serde_json::to_value(&next).unwrap();
|
||||
value[field] = change;
|
||||
let changed = serde_json::from_value(value).unwrap();
|
||||
assert!(current
|
||||
.validate_next_run(
|
||||
&changed,
|
||||
"kimi",
|
||||
"kimi-for-coding",
|
||||
"kimi-next-run",
|
||||
"kimi-session"
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
assert!(current
|
||||
.validate_next_run(
|
||||
&next,
|
||||
"kimi",
|
||||
"kimi-for-coding",
|
||||
"foreign-run",
|
||||
"kimi-session"
|
||||
)
|
||||
.is_err());
|
||||
assert!(current
|
||||
.validate_next_run(
|
||||
&next,
|
||||
"kimi",
|
||||
"kimi-for-coding",
|
||||
"kimi-next-run",
|
||||
"foreign-session"
|
||||
)
|
||||
.is_err());
|
||||
let mut expired = current.clone();
|
||||
expired.expires_at = 0;
|
||||
assert!(expired
|
||||
.validate_next_run(
|
||||
&next,
|
||||
"kimi",
|
||||
"kimi-for-coding",
|
||||
"kimi-next-run",
|
||||
"kimi-session"
|
||||
)
|
||||
.is_err());
|
||||
let antigravity = test_admission("run-1", "session-1");
|
||||
assert!(antigravity
|
||||
.validate_next_run(
|
||||
&next,
|
||||
"kimi",
|
||||
"kimi-for-coding",
|
||||
"kimi-next-run",
|
||||
"kimi-session"
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
@@ -927,6 +927,35 @@ impl AcpxCommandExecutor {
|
||||
.canonical_digest()
|
||||
}
|
||||
|
||||
fn restore_launch_profile_digest(
|
||||
&self,
|
||||
state: &AcpxDurableState,
|
||||
) -> Result<String, DurableRunnerError> {
|
||||
let Some(profile) = self.launch_profile.as_ref() else {
|
||||
return self.launch_profile_digest();
|
||||
};
|
||||
let Some(current) = profile.candidate_admission.as_ref().filter(|grant| {
|
||||
grant.agent == "kimi" && state.descriptor.run_id != self.context.run_id
|
||||
}) else {
|
||||
return profile.canonical_digest();
|
||||
};
|
||||
let mut prior = profile.clone();
|
||||
prior.candidate_admission.as_mut().unwrap().run_id = state.descriptor.run_id.clone();
|
||||
prior
|
||||
.candidate_admission
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.validate_next_run(
|
||||
current,
|
||||
&state.descriptor.agent,
|
||||
&state.descriptor.model,
|
||||
&self.context.run_id,
|
||||
&self.context.normalized_session_id,
|
||||
)
|
||||
.map_err(|error| DurableRunnerError::invalid(error.to_string()))?;
|
||||
prior.canonical_digest()
|
||||
}
|
||||
|
||||
fn restore(&mut self) -> Result<(), DurableRunnerError> {
|
||||
self.ensure_persistence_healthy()?;
|
||||
if self.restore_checked {
|
||||
@@ -958,7 +987,10 @@ impl AcpxCommandExecutor {
|
||||
)))
|
||||
}
|
||||
};
|
||||
if self.candidate_admission().is_some() {
|
||||
if self
|
||||
.candidate_admission()
|
||||
.is_some_and(|admission| admission.agent == "antigravity")
|
||||
{
|
||||
return Err(DurableRunnerError::invalid(
|
||||
"ACPX candidate cannot adopt a prior durable runner activation",
|
||||
));
|
||||
@@ -983,7 +1015,7 @@ impl AcpxCommandExecutor {
|
||||
let state: AcpxDurableState = serde_json::from_slice(&bytes).map_err(|error| {
|
||||
DurableRunnerError::invalid(format!("ACPX provider state is malformed: {error}"))
|
||||
})?;
|
||||
let launch_profile_digest = self.launch_profile_digest()?;
|
||||
let launch_profile_digest = self.restore_launch_profile_digest(&state)?;
|
||||
state.validate(&self.context, &launch_profile_digest)?;
|
||||
self.state = Some(state);
|
||||
self.restore_session_if_needed()
|
||||
@@ -1218,7 +1250,10 @@ impl AcpxCommandExecutor {
|
||||
}
|
||||
|
||||
fn attach_run(&mut self, payload: &Value) -> Result<(), DurableRunnerError> {
|
||||
if self.candidate_admission().is_some() {
|
||||
if self
|
||||
.candidate_admission()
|
||||
.is_some_and(|admission| admission.agent == "antigravity")
|
||||
{
|
||||
return Err(DurableRunnerError::invalid(
|
||||
"ACPX candidate cannot rotate or adopt a live runner authority",
|
||||
));
|
||||
@@ -1258,6 +1293,62 @@ impl AcpxCommandExecutor {
|
||||
attachment_context.run_id = run_id.to_owned();
|
||||
}
|
||||
descriptor.validate(&attachment_context)?;
|
||||
let cold_candidate_attachment = self
|
||||
.candidate_admission()
|
||||
.is_some_and(|grant| grant.agent == "kimi")
|
||||
&& payload.get("paperclipNextAuthority").is_none()
|
||||
&& self
|
||||
.state
|
||||
.as_ref()
|
||||
.is_some_and(|state| state.descriptor.run_id != self.context.run_id);
|
||||
let next_candidate = if let Some(admission) = self
|
||||
.candidate_admission()
|
||||
.filter(|admission| admission.agent == "kimi")
|
||||
{
|
||||
if cold_candidate_attachment {
|
||||
let state = self.state.as_ref().unwrap();
|
||||
let digest = self.restore_launch_profile_digest(state)?;
|
||||
state.validate(&self.context, &digest)?;
|
||||
admission
|
||||
.validate_binding(
|
||||
&descriptor.agent,
|
||||
&descriptor.model,
|
||||
&self.context.run_id,
|
||||
&self.context.normalized_session_id,
|
||||
)
|
||||
.map_err(|error| DurableRunnerError::invalid(error.to_string()))?;
|
||||
Some(admission.clone())
|
||||
} else {
|
||||
let proposed: crate::acpx_candidate_admission::AcpxCandidateAdmission =
|
||||
serde_json::from_value(
|
||||
payload
|
||||
.pointer("/paperclipNextAuthority/candidateAdmission")
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
DurableRunnerError::invalid(
|
||||
"Kimi run.attach requires exact trusted candidate admission",
|
||||
)
|
||||
})?,
|
||||
)
|
||||
.map_err(|_| {
|
||||
DurableRunnerError::invalid(
|
||||
"Kimi run.attach candidate admission is invalid",
|
||||
)
|
||||
})?;
|
||||
admission
|
||||
.validate_next_run(
|
||||
&proposed,
|
||||
&descriptor.agent,
|
||||
&descriptor.model,
|
||||
&descriptor.run_id,
|
||||
&descriptor.normalized_session_id,
|
||||
)
|
||||
.map_err(|error| DurableRunnerError::invalid(error.to_string()))?;
|
||||
Some(proposed)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tool_set = authorized_tool_set(payload)?;
|
||||
let state = self
|
||||
.state
|
||||
@@ -1327,16 +1418,30 @@ impl AcpxCommandExecutor {
|
||||
})?;
|
||||
}
|
||||
self.session = None;
|
||||
let state = self
|
||||
if let Some(admission) = next_candidate {
|
||||
self.launch_profile
|
||||
.as_mut()
|
||||
.expect("candidate launch profile remains available")
|
||||
.candidate_admission = Some(admission);
|
||||
}
|
||||
let launch_profile_digest = self.launch_profile_digest()?;
|
||||
let mut state = self
|
||||
.state
|
||||
.as_mut()
|
||||
.expect("ACPX state remains available while attaching a run");
|
||||
.as_ref()
|
||||
.expect("ACPX state remains available while attaching a run")
|
||||
.clone();
|
||||
state.launch_profile_digest = launch_profile_digest;
|
||||
state.descriptor = descriptor;
|
||||
state.tool_set = tool_set;
|
||||
state.semantic_result = None;
|
||||
state.pending_events.clear();
|
||||
state.lifecycle = "suspended".to_owned();
|
||||
self.save_state()
|
||||
if cold_candidate_attachment {
|
||||
self.commit_staged_state(state)
|
||||
} else {
|
||||
self.state = Some(state);
|
||||
self.save_state()
|
||||
}
|
||||
}
|
||||
|
||||
fn open_session(&mut self) -> Result<CommandExecution, DurableRunnerError> {
|
||||
@@ -2032,7 +2137,10 @@ impl AcpxCommandExecutor {
|
||||
}
|
||||
|
||||
fn suspend(&mut self) -> Result<CommandExecution, DurableRunnerError> {
|
||||
if self.candidate_admission().is_some() {
|
||||
if self
|
||||
.candidate_admission()
|
||||
.is_some_and(|admission| admission.agent == "antigravity")
|
||||
{
|
||||
return self.close_session("candidate runner.suspend retires the owned process");
|
||||
}
|
||||
if self
|
||||
@@ -2270,7 +2378,9 @@ fn goal_terminal_disposition<'a>(
|
||||
impl CommandExecutor for AcpxCommandExecutor {
|
||||
fn execute(&mut self, command: &Command) -> Result<CommandExecution, DurableRunnerError> {
|
||||
self.restore()?;
|
||||
if self.candidate_admission().is_some()
|
||||
if self
|
||||
.candidate_admission()
|
||||
.is_some_and(|admission| admission.agent == "antigravity")
|
||||
&& command.command_type == "run.attach"
|
||||
&& command.payload.get("paperclipNextAuthority").is_some()
|
||||
{
|
||||
@@ -2346,6 +2456,12 @@ impl CommandExecutor for AcpxCommandExecutor {
|
||||
self.context.normalized_session_id = config.normalized_session_id.clone();
|
||||
self.context.turn_id = config.turn_id.clone();
|
||||
self.context.item_id = config.item_id.clone();
|
||||
if self
|
||||
.candidate_admission()
|
||||
.is_some_and(|admission| admission.agent == "kimi")
|
||||
{
|
||||
self.launch_profile = config.acpx_launch_profile.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_events(&mut self) -> Result<Vec<PolledEvent>, DurableRunnerError> {
|
||||
@@ -4824,4 +4940,536 @@ mod tests {
|
||||
drop(listeners.pop());
|
||||
(candidates, listeners)
|
||||
}
|
||||
#[test]
|
||||
#[cfg(any(
|
||||
all(target_os = "macos", target_arch = "aarch64"),
|
||||
all(target_os = "linux", target_arch = "x86_64")
|
||||
))]
|
||||
fn released_kimi_attachment_preserves_ordinary_profile_without_candidate_admission() {
|
||||
assert!(!acpx_release_profile("kimi").unwrap().qualification_pending);
|
||||
let directory = temporary_directory("kimi-candidate-attachment");
|
||||
let mut grant = crate::acpx_candidate_admission::test_admission("run-1", "session-1");
|
||||
grant.agent = "kimi".to_owned();
|
||||
grant.model = "explicit-test-model".to_owned();
|
||||
grant.profile_digest = acpx_release_profile("kimi")
|
||||
.unwrap()
|
||||
.command_digest
|
||||
.to_owned();
|
||||
grant.platform = if cfg!(target_os = "macos") {
|
||||
"darwin"
|
||||
} else {
|
||||
"linux"
|
||||
}
|
||||
.to_owned();
|
||||
grant.architecture = if cfg!(target_os = "macos") {
|
||||
"arm64"
|
||||
} else {
|
||||
"x64"
|
||||
}
|
||||
.to_owned();
|
||||
grant.native_closure_digest = format!(
|
||||
"sha256:{}",
|
||||
crate::generated_acpx_profiles::kimi_native_closure(
|
||||
&grant.platform,
|
||||
&grant.architecture
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
let command = directory.join("owned-sidecar");
|
||||
write_artifact(&command, b"#!/bin/sh\nexit 1\n", true);
|
||||
let profile = AcpxLaunchProfile {
|
||||
candidate_admission: None,
|
||||
authority_digest: format!("sha256:{}", "d".repeat(64)),
|
||||
command: command.clone(),
|
||||
args: vec![],
|
||||
artifacts: vec![artifact(&command)],
|
||||
};
|
||||
let runtime = directory.join("runtime");
|
||||
let workspace = directory.join("workspace");
|
||||
fs::create_dir_all(&runtime).unwrap();
|
||||
fs::create_dir_all(&workspace).unwrap();
|
||||
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
fs::set_permissions(&workspace, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let mut value = descriptor("kimi");
|
||||
value["runtimeDirectory"] = json!(runtime);
|
||||
value["cwd"] = json!(workspace);
|
||||
value["sidecarCommand"] = json!(command);
|
||||
value["sidecarArgs"] = json!([]);
|
||||
let desc: AcpxProviderDescriptor = serde_json::from_value(value.clone()).unwrap();
|
||||
let identity = AcpxProviderSessionIdentity {
|
||||
kind: "acpx".to_owned(),
|
||||
normalized_session_id: "session-1".to_owned(),
|
||||
acpx_record_id: "record-1".to_owned(),
|
||||
backend_session_id: "backend-1".to_owned(),
|
||||
agent_session_id: "agent-1".to_owned(),
|
||||
profile_digest: desc.command_digest.clone(),
|
||||
workspace_digest: format!("sha256:{}", "a".repeat(64)),
|
||||
requested_model: desc.model.clone(),
|
||||
effective_model: desc.model.clone(),
|
||||
permission_mode: Some(desc.permission_mode),
|
||||
mode: None,
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
grok_effort: None,
|
||||
grok_disable_web_search: None,
|
||||
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
|
||||
};
|
||||
let operations = Vec::new();
|
||||
let tools = AuthorizedToolSet {
|
||||
schema: TOOL_SET_SCHEMA.to_owned(),
|
||||
schema_version: 1,
|
||||
catalog_digest: authorized_tool_catalog_digest(&operations).unwrap(),
|
||||
operations,
|
||||
};
|
||||
let previous_digest = profile.canonical_digest().unwrap();
|
||||
let mut state = AcpxDurableState::new(desc, tools, previous_digest.clone());
|
||||
state.lifecycle = "suspended".to_owned();
|
||||
state.identity = Some(identity);
|
||||
let config = test_config(&directory, Some(profile));
|
||||
let mut executor = AcpxCommandExecutor::with_runner_config(&directory, &config);
|
||||
executor.state = Some(state);
|
||||
executor.save_state().unwrap();
|
||||
let before = fs::read(executor.state_path()).unwrap();
|
||||
let mut provider = value;
|
||||
provider["runId"] = json!("run-2");
|
||||
let payload = json!({"provider":provider,"paperclipNextAuthority":{
|
||||
"identity":{"runId":"run-2","normalizedSessionId":"session-1"}
|
||||
}});
|
||||
let mut injected_profile = executor.launch_profile.as_ref().unwrap().clone();
|
||||
injected_profile.candidate_admission = Some(grant.clone());
|
||||
let injected_config = test_config(&directory, Some(injected_profile.clone()));
|
||||
assert!(injected_config.validate().is_err());
|
||||
let prior = executor.state.as_ref().unwrap();
|
||||
assert!(prior
|
||||
.descriptor
|
||||
.session_config(
|
||||
prior.tool_set.clone(),
|
||||
prior.identity.clone(),
|
||||
Some(&injected_profile),
|
||||
)
|
||||
.is_err());
|
||||
assert_eq!(fs::read(executor.state_path()).unwrap(), before);
|
||||
for (field, changed) in [
|
||||
("normalizedSessionId", json!("foreign-session")),
|
||||
("runId", json!("run-1")),
|
||||
] {
|
||||
let mut invalid = payload.clone();
|
||||
invalid["paperclipNextAuthority"]["identity"][field] = changed;
|
||||
assert!(executor.attach_run(&invalid).is_err(), "{field}");
|
||||
assert_eq!(fs::read(executor.state_path()).unwrap(), before);
|
||||
}
|
||||
assert!(executor.attach_run(&json!({"provider":provider})).is_err());
|
||||
executor.attach_run(&payload).unwrap();
|
||||
let updated = executor.state.as_ref().unwrap();
|
||||
assert_eq!(updated.descriptor.run_id, "run-2");
|
||||
assert_eq!(executor.context.run_id, "run-1");
|
||||
let profile = executor.launch_profile.as_ref().unwrap();
|
||||
assert!(profile.candidate_admission.is_none());
|
||||
assert_eq!(updated.launch_profile_digest, previous_digest);
|
||||
assert_eq!(
|
||||
updated.launch_profile_digest,
|
||||
profile.canonical_digest().unwrap()
|
||||
);
|
||||
let launch = updated
|
||||
.descriptor
|
||||
.session_config(
|
||||
updated.tool_set.clone(),
|
||||
updated.identity.clone(),
|
||||
Some(profile),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(launch.candidate_admission.is_none());
|
||||
let saved: AcpxDurableState =
|
||||
serde_json::from_slice(&fs::read(executor.state_path()).unwrap()).unwrap();
|
||||
assert_eq!(saved.descriptor.run_id, "run-2");
|
||||
assert_eq!(saved.launch_profile_digest, updated.launch_profile_digest);
|
||||
assert!(executor.session.is_none());
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
#[cfg(any(
|
||||
all(target_os = "macos", target_arch = "aarch64"),
|
||||
all(target_os = "linux", target_arch = "x86_64")
|
||||
))]
|
||||
fn kimi_cold_restore_fixture(
|
||||
label: &str,
|
||||
) -> (
|
||||
PathBuf,
|
||||
DurableRunnerConfig,
|
||||
Value,
|
||||
Vec<u8>,
|
||||
PathBuf,
|
||||
crate::acpx_candidate_admission::AcpxCandidateAdmission,
|
||||
) {
|
||||
assert!(!acpx_release_profile("kimi").unwrap().qualification_pending);
|
||||
let directory = temporary_directory(label);
|
||||
let mut grant = crate::acpx_candidate_admission::test_admission("run-1", "session-1");
|
||||
grant.agent = "kimi".to_owned();
|
||||
grant.model = "explicit-test-model".to_owned();
|
||||
grant.profile_digest = acpx_release_profile("kimi")
|
||||
.unwrap()
|
||||
.command_digest
|
||||
.to_owned();
|
||||
grant.platform = if cfg!(target_os = "macos") {
|
||||
"darwin"
|
||||
} else {
|
||||
"linux"
|
||||
}
|
||||
.to_owned();
|
||||
grant.architecture = if cfg!(target_os = "macos") {
|
||||
"arm64"
|
||||
} else {
|
||||
"x64"
|
||||
}
|
||||
.to_owned();
|
||||
grant.native_closure_digest = format!(
|
||||
"sha256:{}",
|
||||
crate::generated_acpx_profiles::kimi_native_closure(
|
||||
&grant.platform,
|
||||
&grant.architecture
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
let command = directory.join("owned-sidecar");
|
||||
let sentinel = directory.join("provider-opened");
|
||||
write_artifact(
|
||||
&command,
|
||||
format!("#!/bin/sh\ntouch '{}'\nexit 1\n", sentinel.display()).as_bytes(),
|
||||
true,
|
||||
);
|
||||
let profile = AcpxLaunchProfile {
|
||||
candidate_admission: None,
|
||||
authority_digest: format!("sha256:{}", "d".repeat(64)),
|
||||
command: command.clone(),
|
||||
args: vec![],
|
||||
artifacts: vec![artifact(&command)],
|
||||
};
|
||||
let runtime = directory.join("runtime");
|
||||
let workspace = directory.join("workspace");
|
||||
fs::create_dir_all(&runtime).unwrap();
|
||||
fs::create_dir_all(&workspace).unwrap();
|
||||
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
fs::set_permissions(&workspace, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let mut value = descriptor("kimi");
|
||||
value["runtimeDirectory"] = json!(runtime);
|
||||
value["cwd"] = json!(workspace);
|
||||
value["sidecarCommand"] = json!(command);
|
||||
value["sidecarArgs"] = json!([]);
|
||||
let desc: AcpxProviderDescriptor = serde_json::from_value(value.clone()).unwrap();
|
||||
let identity = AcpxProviderSessionIdentity {
|
||||
kind: "acpx".to_owned(),
|
||||
normalized_session_id: "session-1".to_owned(),
|
||||
acpx_record_id: "record-1".to_owned(),
|
||||
backend_session_id: "backend-1".to_owned(),
|
||||
agent_session_id: "agent-1".to_owned(),
|
||||
profile_digest: desc.command_digest.clone(),
|
||||
workspace_digest: format!("sha256:{}", "a".repeat(64)),
|
||||
requested_model: desc.model.clone(),
|
||||
effective_model: desc.model.clone(),
|
||||
permission_mode: Some(desc.permission_mode),
|
||||
mode: None,
|
||||
pi_thinking_level: None,
|
||||
claude_effort: None,
|
||||
grok_effort: None,
|
||||
grok_disable_web_search: None,
|
||||
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
|
||||
};
|
||||
let operations = Vec::new();
|
||||
let tools = AuthorizedToolSet {
|
||||
schema: TOOL_SET_SCHEMA.to_owned(),
|
||||
schema_version: 1,
|
||||
catalog_digest: authorized_tool_catalog_digest(&operations).unwrap(),
|
||||
operations,
|
||||
};
|
||||
let previous_digest = profile.canonical_digest().unwrap();
|
||||
let mut state = AcpxDurableState::new(desc, tools, previous_digest.clone());
|
||||
state.lifecycle = "suspended".to_owned();
|
||||
state.identity = Some(identity);
|
||||
let config = test_config(&directory, Some(profile));
|
||||
let mut executor = AcpxCommandExecutor::with_runner_config(&directory, &config);
|
||||
executor.state = Some(state);
|
||||
executor.save_state().unwrap();
|
||||
let before = fs::read(executor.state_path()).unwrap();
|
||||
let mut next_config = config.clone();
|
||||
next_config.run_id = "run-2".to_owned();
|
||||
next_config.turn_id = "turn-2".to_owned();
|
||||
next_config.item_id = "item-2".to_owned();
|
||||
grant.run_id = "run-2".to_owned();
|
||||
next_config.validate().unwrap();
|
||||
let mut next_provider = value;
|
||||
next_provider["runId"] = json!("run-2");
|
||||
(
|
||||
directory,
|
||||
next_config,
|
||||
json!({"provider": next_provider}),
|
||||
before,
|
||||
sentinel,
|
||||
grant,
|
||||
)
|
||||
}
|
||||
|
||||
fn cold_attach_command(payload: Value) -> Command {
|
||||
Command {
|
||||
schema: "paperclip.prp.command.v1".into(),
|
||||
command_id: "cold-attach".into(),
|
||||
controller_seq: 1,
|
||||
command_type: "run.attach".into(),
|
||||
issued_at: "2026-10-30T00:00:00.000Z".into(),
|
||||
deadline_at: None,
|
||||
precondition: None,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(
|
||||
all(target_os = "macos", target_arch = "aarch64"),
|
||||
all(target_os = "linux", target_arch = "x86_64")
|
||||
))]
|
||||
fn released_kimi_governed_wait_cold_restore_uses_ordinary_startup_without_grant() {
|
||||
let (directory, next_config, payload, before, sentinel, _) =
|
||||
kimi_cold_restore_fixture("kimi-candidate-cold-governed-wait");
|
||||
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &next_config);
|
||||
recovered.restore().expect("released cold Kimi restore retains its settled prior session under the same full ordinary profile");
|
||||
assert!(recovered.session.is_none());
|
||||
assert!(recovered.poll_events().unwrap().is_empty());
|
||||
assert!(!sentinel.exists());
|
||||
recovered.attach_run(&payload)
|
||||
.expect("released cold replacement attaches with the ordinary current startup and no candidate record");
|
||||
let saved: AcpxDurableState =
|
||||
serde_json::from_slice(&fs::read(recovered.state_path()).unwrap()).unwrap();
|
||||
assert_eq!(saved.descriptor.run_id, "run-2");
|
||||
assert_eq!(
|
||||
saved.launch_profile_digest,
|
||||
next_config
|
||||
.acpx_launch_profile
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.canonical_digest()
|
||||
.unwrap()
|
||||
);
|
||||
assert!(recovered.session.is_none());
|
||||
assert!(!sentinel.exists());
|
||||
assert_ne!(fs::read(recovered.state_path()).unwrap(), before);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(
|
||||
all(target_os = "macos", target_arch = "aarch64"),
|
||||
all(target_os = "linux", target_arch = "x86_64")
|
||||
))]
|
||||
fn released_kimi_cold_restore_rejects_candidate_injection_and_changed_full_profile() {
|
||||
for field in [
|
||||
"company",
|
||||
"model",
|
||||
"expiry",
|
||||
"expired",
|
||||
"profile",
|
||||
"closure",
|
||||
"platform",
|
||||
"architecture",
|
||||
"session",
|
||||
"grant_run",
|
||||
"injected_candidate",
|
||||
"context_run",
|
||||
"authority",
|
||||
"command",
|
||||
"args",
|
||||
"artifact",
|
||||
"missing_profile",
|
||||
"saved_digest",
|
||||
] {
|
||||
let (directory, mut config, payload, before, sentinel, candidate) =
|
||||
kimi_cold_restore_fixture("kimi-released-cold-reject");
|
||||
assert!(config
|
||||
.acpx_launch_profile
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.candidate_admission
|
||||
.is_none());
|
||||
match field {
|
||||
"context_run" => config.run_id = "foreign-run".into(),
|
||||
"missing_profile" => config.acpx_launch_profile = None,
|
||||
"saved_digest" => {
|
||||
let mut state: AcpxDurableState = serde_json::from_slice(&before).unwrap();
|
||||
state.launch_profile_digest = format!("sha256:{}", "0".repeat(64));
|
||||
fs::write(
|
||||
directory.join("acpx-provider-state.json"),
|
||||
serde_json::to_vec(&state).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
_ => {
|
||||
let profile = config.acpx_launch_profile.as_mut().unwrap();
|
||||
match field {
|
||||
"authority" => {
|
||||
profile.authority_digest = format!("sha256:{}", "0".repeat(64))
|
||||
}
|
||||
"command" => profile.command = directory.join("foreign-command"),
|
||||
"args" => profile.args.push("foreign-argument".into()),
|
||||
"artifact" => {
|
||||
profile.artifacts[0].sha256 = format!("sha256:{}", "0".repeat(64))
|
||||
}
|
||||
_ => {
|
||||
let mut grant = candidate;
|
||||
match field {
|
||||
"company" => grant.company_id = "foreign-company".into(),
|
||||
"model" => grant.model = "foreign-model".into(),
|
||||
"expiry" => grant.expires_at += 1,
|
||||
"expired" => grant.expires_at = 1,
|
||||
"profile" => {
|
||||
grant.profile_digest = format!("sha256:{}", "0".repeat(64))
|
||||
}
|
||||
"closure" => {
|
||||
grant.native_closure_digest =
|
||||
format!("sha256:{}", "0".repeat(64))
|
||||
}
|
||||
"platform" => grant.platform = "foreign-platform".into(),
|
||||
"architecture" => {
|
||||
grant.architecture = "foreign-architecture".into()
|
||||
}
|
||||
"session" => grant.normalized_session_id = "foreign-session".into(),
|
||||
"grant_run" => grant.run_id = "foreign-run".into(),
|
||||
"injected_candidate" => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
profile.candidate_admission = Some(grant);
|
||||
assert!(config.validate().is_err(), "{field}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config);
|
||||
let retained = fs::read(recovered.state_path()).unwrap();
|
||||
assert!(
|
||||
recovered.execute(&cold_attach_command(payload)).is_err(),
|
||||
"{field}"
|
||||
);
|
||||
assert!(recovered.session.is_none(), "{field}");
|
||||
assert!(!sentinel.exists(), "{field}");
|
||||
assert_eq!(
|
||||
fs::read(recovered.state_path()).unwrap(),
|
||||
retained,
|
||||
"{field}"
|
||||
);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(
|
||||
all(target_os = "macos", target_arch = "aarch64"),
|
||||
all(target_os = "linux", target_arch = "x86_64")
|
||||
))]
|
||||
fn released_kimi_cold_attach_rejects_unsettled_or_changed_session_before_provider_open() {
|
||||
for field in [
|
||||
"run",
|
||||
"session",
|
||||
"model",
|
||||
"workspace",
|
||||
"active_turn",
|
||||
"pending_event",
|
||||
"exit_unconfirmed",
|
||||
"identity",
|
||||
"closed",
|
||||
] {
|
||||
let (directory, config, mut payload, before, sentinel, _) =
|
||||
kimi_cold_restore_fixture("kimi-candidate-cold-binding");
|
||||
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config);
|
||||
match field {
|
||||
"run" => payload["provider"]["runId"] = json!("foreign-run"),
|
||||
"session" => payload["provider"]["normalizedSessionId"] = json!("foreign-session"),
|
||||
"model" => payload["provider"]["model"] = json!("foreign-model"),
|
||||
"workspace" => {
|
||||
payload["provider"]["cwd"] = json!(directory.join("foreign-workspace"))
|
||||
}
|
||||
_ => {
|
||||
let mut state: AcpxDurableState = serde_json::from_slice(&before).unwrap();
|
||||
match field {
|
||||
"active_turn" => state.active_turn_id = Some("active-turn".into()),
|
||||
"pending_event" => {
|
||||
state
|
||||
.push(NormalizedProviderEvent {
|
||||
event_type: "runtime_request.created".into(),
|
||||
priority: EventPriority::P0,
|
||||
payload: json!({"request":pending_input_request("input-1")}),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
"exit_unconfirmed" => state.provider_exit_unconfirmed = true,
|
||||
"identity" => state.identity = None,
|
||||
"closed" => state.lifecycle = "closed".into(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
fs::write(recovered.state_path(), serde_json::to_vec(&state).unwrap()).unwrap();
|
||||
}
|
||||
}
|
||||
let retained = fs::read(recovered.state_path()).unwrap();
|
||||
assert!(
|
||||
recovered.execute(&cold_attach_command(payload)).is_err(),
|
||||
"{field}"
|
||||
);
|
||||
assert!(recovered.session.is_none(), "{field}");
|
||||
assert!(!sentinel.exists(), "{field}");
|
||||
assert_eq!(
|
||||
fs::read(recovered.state_path()).unwrap(),
|
||||
retained,
|
||||
"{field}"
|
||||
);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(
|
||||
all(target_os = "macos", target_arch = "aarch64"),
|
||||
all(target_os = "linux", target_arch = "x86_64")
|
||||
))]
|
||||
fn released_kimi_cold_attach_post_rename_failure_quarantines_public_execution() {
|
||||
let (directory, config, payload, before, sentinel, _) =
|
||||
kimi_cold_restore_fixture("kimi-candidate-cold-write-failure");
|
||||
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config);
|
||||
recovered.restore().unwrap();
|
||||
let previous_state = recovered.state.clone();
|
||||
recovered.fail_after_state_rename = true;
|
||||
let command = cold_attach_command(payload);
|
||||
let error = recovered.execute(&command).unwrap_err();
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("injected post-rename sync failure"));
|
||||
assert_ne!(recovered.state, previous_state);
|
||||
assert!(recovered.session.is_none());
|
||||
assert!(!sentinel.exists());
|
||||
let disk = fs::read(recovered.state_path()).unwrap();
|
||||
assert_ne!(disk, before);
|
||||
let saved: AcpxDurableState = serde_json::from_slice(&disk).unwrap();
|
||||
assert_eq!(saved.descriptor.run_id, "run-2");
|
||||
assert_eq!(
|
||||
saved.launch_profile_digest,
|
||||
config
|
||||
.acpx_launch_profile
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.canonical_digest()
|
||||
.unwrap()
|
||||
);
|
||||
recovered.fail_after_state_rename = false;
|
||||
for result in [
|
||||
recovered.retained_events().map(|_| ()),
|
||||
recovered.poll_events().map(|_| ()),
|
||||
recovered.acknowledge_events(0),
|
||||
recovered.restore(),
|
||||
recovered.save_state(),
|
||||
recovered.shutdown(),
|
||||
] {
|
||||
assert_eq!(result.unwrap_err().to_string(), error.to_string());
|
||||
}
|
||||
assert_eq!(
|
||||
recovered.execute(&command).unwrap_err().to_string(),
|
||||
error.to_string()
|
||||
);
|
||||
assert_eq!(fs::read(recovered.state_path()).unwrap(), disk);
|
||||
assert!(!sentinel.exists());
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ pub(crate) enum RunAttachmentPolicy {
|
||||
|
||||
pub(crate) fn run_attachment_policy(agent: &str) -> RunAttachmentPolicy {
|
||||
match agent {
|
||||
"cursor" | "copilot" | "pi" => RunAttachmentPolicy::AuthenticatedRunGrants,
|
||||
"cursor" | "copilot" | "pi" | "kimi" => RunAttachmentPolicy::AuthenticatedRunGrants,
|
||||
"claude" => RunAttachmentPolicy::AuthenticatedAssetPaths,
|
||||
_ => RunAttachmentPolicy::ImmutableInstructions,
|
||||
}
|
||||
|
||||
@@ -109,8 +109,8 @@ pub struct AcpxProviderState {
|
||||
plan_revision: u64,
|
||||
assistant_text: String,
|
||||
assistant_message_id: Option<String>,
|
||||
pi_assistant_message: Option<String>,
|
||||
pi_assistant_message_ids: BTreeSet<String>,
|
||||
native_assistant_message: Option<String>,
|
||||
native_assistant_message_ids: BTreeSet<String>,
|
||||
pending_tools: BTreeMap<String, AcpxPendingTool>,
|
||||
pending_tool_input_bytes: usize,
|
||||
pending_permissions: BTreeMap<String, (usize, Value)>,
|
||||
@@ -128,8 +128,8 @@ impl AcpxProviderState {
|
||||
plan_revision: 0,
|
||||
assistant_text: String::new(),
|
||||
assistant_message_id: None,
|
||||
pi_assistant_message: None,
|
||||
pi_assistant_message_ids: BTreeSet::new(),
|
||||
native_assistant_message: None,
|
||||
native_assistant_message_ids: BTreeSet::new(),
|
||||
pending_tools: BTreeMap::new(),
|
||||
pending_tool_input_bytes: 0,
|
||||
pending_permissions: BTreeMap::new(),
|
||||
@@ -222,8 +222,8 @@ impl AcpxProviderState {
|
||||
self.plan_revision = 0;
|
||||
self.assistant_text.clear();
|
||||
self.assistant_message_id = None;
|
||||
self.pi_assistant_message = None;
|
||||
self.pi_assistant_message_ids.clear();
|
||||
self.native_assistant_message = None;
|
||||
self.native_assistant_message_ids.clear();
|
||||
self.semantic_result = None;
|
||||
self.attempted_turn_controls.clear();
|
||||
Ok(())
|
||||
@@ -371,9 +371,9 @@ impl AcpxProviderState {
|
||||
}])
|
||||
}
|
||||
AcpxEventPayload::TurnTerminal { status, error } => {
|
||||
if status == AcpxTurnStatus::Completed && self.pi_assistant_message.is_some() {
|
||||
if status == AcpxTurnStatus::Completed && self.native_assistant_message.is_some() {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Pi assistant message ended without its native boundary",
|
||||
"ACP assistant message ended without its native boundary",
|
||||
));
|
||||
}
|
||||
let turn_id = event
|
||||
@@ -394,8 +394,8 @@ impl AcpxProviderState {
|
||||
self.assistant_text.clear();
|
||||
}
|
||||
self.assistant_message_id = None;
|
||||
self.pi_assistant_message = None;
|
||||
self.pi_assistant_message_ids.clear();
|
||||
self.native_assistant_message = None;
|
||||
self.native_assistant_message_ids.clear();
|
||||
events.push(AcpxProviderStateEvent::TurnTerminal {
|
||||
turn_id,
|
||||
status,
|
||||
@@ -518,6 +518,10 @@ impl AcpxProviderState {
|
||||
};
|
||||
if kind == AcpxRuntimeEventKind::TextDelta
|
||||
&& payload.get("piMessageHistory").and_then(Value::as_bool) != Some(true)
|
||||
&& payload
|
||||
.get("assistantMessageHistory")
|
||||
.and_then(Value::as_bool)
|
||||
!= Some(true)
|
||||
{
|
||||
let provider_message_id = payload
|
||||
.get("messageId")
|
||||
@@ -534,55 +538,67 @@ impl AcpxProviderState {
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let mut native_boundary_clears = false;
|
||||
if let Some(boundary) = payload.get("piMessageBoundary") {
|
||||
if payload.get("assistantMessageBoundary").is_some()
|
||||
&& payload.get("piMessageBoundary").is_some()
|
||||
{
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"ACP assistant boundary has ambiguous representations",
|
||||
));
|
||||
}
|
||||
if let Some(boundary) = payload
|
||||
.get("assistantMessageBoundary")
|
||||
.or_else(|| payload.get("piMessageBoundary"))
|
||||
{
|
||||
let phase = boundary.get("phase").and_then(Value::as_str);
|
||||
let reason = boundary.get("stopReason").and_then(Value::as_str);
|
||||
if !raw_text.is_empty() || provider_message_id.is_none() {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Pi assistant boundary contains invalid content or identity",
|
||||
"ACP assistant boundary contains invalid content or identity",
|
||||
));
|
||||
}
|
||||
match phase {
|
||||
Some("start") if self.pi_assistant_message.is_none() && reason.is_none() => {
|
||||
if self.pi_assistant_message_ids.len() >= 4096
|
||||
Some("start")
|
||||
if self.native_assistant_message.is_none() && reason.is_none() =>
|
||||
{
|
||||
if self.native_assistant_message_ids.len() >= 4096
|
||||
|| !self
|
||||
.pi_assistant_message_ids
|
||||
.native_assistant_message_ids
|
||||
.insert(provider_message_id.clone().unwrap())
|
||||
{
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Pi assistant identity is duplicated or exceeds its bound",
|
||||
"ACP assistant identity is duplicated or exceeds its bound",
|
||||
));
|
||||
}
|
||||
self.pi_assistant_message = provider_message_id.clone();
|
||||
self.native_assistant_message = provider_message_id.clone();
|
||||
native_boundary_clears = true;
|
||||
}
|
||||
Some("end")
|
||||
if self.pi_assistant_message.is_some()
|
||||
&& self.pi_assistant_message == provider_message_id =>
|
||||
if self.native_assistant_message.is_some()
|
||||
&& self.native_assistant_message == provider_message_id =>
|
||||
{
|
||||
if !matches!(
|
||||
reason,
|
||||
Some("stop" | "length" | "toolUse" | "error" | "aborted")
|
||||
) {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Pi assistant stop reason is invalid",
|
||||
"ACP assistant stop reason is invalid",
|
||||
));
|
||||
}
|
||||
self.pi_assistant_message = None;
|
||||
self.native_assistant_message = None;
|
||||
native_boundary_clears = !matches!(reason, Some("stop" | "length"));
|
||||
}
|
||||
_ => {
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Pi assistant boundaries are out of order",
|
||||
"ACP assistant boundaries are out of order",
|
||||
))
|
||||
}
|
||||
}
|
||||
} else if !self.pi_assistant_message_ids.is_empty()
|
||||
&& (self.pi_assistant_message.is_none()
|
||||
|| self.pi_assistant_message != provider_message_id)
|
||||
} else if !self.native_assistant_message_ids.is_empty()
|
||||
&& (self.native_assistant_message.is_none()
|
||||
|| self.native_assistant_message != provider_message_id)
|
||||
{
|
||||
return Err(LocalRunnerError::invalid(
|
||||
"Pi assistant delta has no matching native message",
|
||||
"ACP assistant delta has no matching native message",
|
||||
));
|
||||
}
|
||||
// ACP can produce more than one assistant message in a turn. All
|
||||
|
||||
@@ -216,6 +216,7 @@ impl AcpxSidecarTransport {
|
||||
"pi" => &[],
|
||||
"cursor" => &["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"],
|
||||
"copilot" => &["COPILOT_GITHUB_TOKEN"],
|
||||
"kimi" => &["KIMI_API_KEY"],
|
||||
"antigravity" => &[
|
||||
"GEMINI_API_KEY",
|
||||
"PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET",
|
||||
@@ -254,7 +255,7 @@ impl AcpxSidecarTransport {
|
||||
"PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT",
|
||||
"PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST",
|
||||
];
|
||||
if matches!(agent, "pi" | "cursor" | "copilot" | "antigravity") {
|
||||
if matches!(agent, "pi" | "cursor" | "copilot" | "kimi" | "antigravity") {
|
||||
// Credential values alone are not proof of an explicit task binding.
|
||||
// The sidecar checks this controller-minted provider/session marker.
|
||||
keys.push("PAPERCLIP_ACPX_CREDENTIAL_BINDING");
|
||||
|
||||
+2
-2
@@ -769,8 +769,8 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.map(|value| value.parse::<usize>())
|
||||
.transpose()?
|
||||
.unwrap_or(64);
|
||||
if !(1..=4096).contains(&held_output_count) {
|
||||
return Err("--held-output-count must be between 1 and 4096".into());
|
||||
if !(1..=8192).contains(&held_output_count) {
|
||||
return Err("--held-output-count must be between 1 and 8192".into());
|
||||
}
|
||||
let exit_after_turn_start = args.iter().any(|value| value == "--exit-after-turn-start");
|
||||
let exit_after_turn_completion = args
|
||||
|
||||
@@ -671,10 +671,15 @@ mod tests {
|
||||
args.extend(options.into_iter().map(str::to_owned));
|
||||
assert!(metadata_with_provider_packs(&args).is_err());
|
||||
}
|
||||
let missing_root = if cfg!(windows) {
|
||||
r"C:\missing-provider-pack"
|
||||
} else {
|
||||
"/missing-provider-pack"
|
||||
};
|
||||
let metadata = metadata_with_provider_packs(&[
|
||||
"--build-metadata".to_owned(),
|
||||
"--provider-pack-root".to_owned(),
|
||||
"/missing-provider-pack".to_owned(),
|
||||
missing_root.to_owned(),
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(metadata["binaryContractVersion"], 2);
|
||||
|
||||
@@ -4874,6 +4874,7 @@ done
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn verified_opencode_proxy_executes_the_descriptor_safe_commonjs_bundle() {
|
||||
let nonce = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
|
||||
@@ -52,6 +52,7 @@ const RESERVED: &[&str] = &[
|
||||
"GITHUB_ENTERPRISE_TOKEN",
|
||||
"GITHUB_TOKEN",
|
||||
"HOME",
|
||||
"KIMI_API_KEY",
|
||||
"NODE_OPTIONS",
|
||||
"NODE_PATH",
|
||||
"OPENAI_API_KEY",
|
||||
@@ -209,6 +210,7 @@ mod tests {
|
||||
"[\"PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT\"]",
|
||||
"[\"PAPERCLIP_RUNTIME_NOTIFICATION_HELPER\"]",
|
||||
"[\"OPENAI_API_KEY\"]",
|
||||
"[\"KIMI_API_KEY\"]",
|
||||
"[\"LD_PRELOAD\"]",
|
||||
"[\"bad=name\"]",
|
||||
"[\"CUSTOM\",\"CUSTOM\"]",
|
||||
@@ -226,6 +228,14 @@ mod tests {
|
||||
assert_eq!(command.get_envs().count(), 0);
|
||||
}
|
||||
let mut command = Command::new("unused");
|
||||
assert!(apply_configured_environment(&mut command, |key| match key {
|
||||
MARKER => Some("[\"KIMI_API_KEY\"]".into()),
|
||||
"KIMI_API_KEY" => Some("selected-code-key".into()),
|
||||
_ => None,
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(command.get_envs().count(), 0);
|
||||
let mut command = Command::new("unused");
|
||||
assert!(
|
||||
apply_configured_environment(&mut command, |key| Some(if key == MARKER {
|
||||
"[\"CUSTOM\"]".into()
|
||||
|
||||
@@ -199,7 +199,6 @@ pub(crate) fn next_authority_config(
|
||||
next.normalized_session_id = read_identity("normalizedSessionId")?;
|
||||
next.turn_id = read_identity("turnId")?;
|
||||
next.item_id = read_identity("itemId")?;
|
||||
next.validate()?;
|
||||
if next.runner_instance_id != current.runner_instance_id
|
||||
|| next.environment_lease_id != current.environment_lease_id
|
||||
|| next.normalized_session_id != current.normalized_session_id
|
||||
@@ -209,6 +208,47 @@ pub(crate) fn next_authority_config(
|
||||
"run.attach authority changed an immutable session binding",
|
||||
));
|
||||
}
|
||||
if let Some(admission) = current
|
||||
.acpx_launch_profile
|
||||
.as_ref()
|
||||
.and_then(|profile| profile.candidate_admission.as_ref())
|
||||
{
|
||||
if admission.agent == "kimi" {
|
||||
let proposed: crate::acpx_candidate_admission::AcpxCandidateAdmission =
|
||||
serde_json::from_value(boundary.get("candidateAdmission").cloned().ok_or_else(
|
||||
|| {
|
||||
DurableRunnerError::invalid(
|
||||
"Kimi authority rotation requires exact trusted candidate admission",
|
||||
)
|
||||
},
|
||||
)?)
|
||||
.map_err(|_| {
|
||||
DurableRunnerError::invalid("Kimi authority rotation admission is invalid")
|
||||
})?;
|
||||
admission
|
||||
.validate_next_run(
|
||||
&proposed,
|
||||
"kimi",
|
||||
&admission.model,
|
||||
&next.run_id,
|
||||
&next.normalized_session_id,
|
||||
)
|
||||
.map_err(|error| DurableRunnerError::invalid(error.to_string()))?;
|
||||
next.acpx_launch_profile
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.candidate_admission = Some(proposed);
|
||||
} else if boundary.get("candidateAdmission").is_some() {
|
||||
return Err(DurableRunnerError::invalid(
|
||||
"candidate authority rotation is not admitted",
|
||||
));
|
||||
}
|
||||
} else if boundary.get("candidateAdmission").is_some() {
|
||||
return Err(DurableRunnerError::invalid(
|
||||
"authority rotation cannot create candidate admission",
|
||||
));
|
||||
}
|
||||
next.validate()?;
|
||||
Ok(Some(next))
|
||||
}
|
||||
|
||||
@@ -5104,4 +5144,130 @@ mod tests {
|
||||
assert_eq!(executor.calls, 1);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
#[test]
|
||||
#[cfg(any(
|
||||
all(target_os = "macos", target_arch = "aarch64"),
|
||||
all(target_os = "linux", target_arch = "x86_64")
|
||||
))]
|
||||
fn released_kimi_handoff_uses_ordinary_authority_and_rejects_candidate_injection() {
|
||||
assert!(
|
||||
!crate::generated_acpx_profiles::acpx_release_profile("kimi")
|
||||
.unwrap()
|
||||
.qualification_pending
|
||||
);
|
||||
let directory =
|
||||
std::env::temp_dir().join(format!("kimi-grant-handoff-{}", std::process::id()));
|
||||
let mut current = config(directory);
|
||||
let mut grant = crate::acpx_candidate_admission::test_admission(
|
||||
"run_1",
|
||||
¤t.normalized_session_id,
|
||||
);
|
||||
grant.agent = "kimi".to_owned();
|
||||
grant.profile_digest = crate::generated_acpx_profiles::acpx_release_profile("kimi")
|
||||
.unwrap()
|
||||
.command_digest
|
||||
.to_owned();
|
||||
grant.platform = if cfg!(target_os = "macos") {
|
||||
"darwin"
|
||||
} else {
|
||||
"linux"
|
||||
}
|
||||
.to_owned();
|
||||
grant.architecture = if cfg!(target_os = "macos") {
|
||||
"arm64"
|
||||
} else {
|
||||
"x64"
|
||||
}
|
||||
.to_owned();
|
||||
grant.native_closure_digest = format!(
|
||||
"sha256:{}",
|
||||
crate::generated_acpx_profiles::kimi_native_closure(
|
||||
&grant.platform,
|
||||
&grant.architecture
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
current.acpx_launch_profile = Some(crate::durable::AcpxLaunchProfile {
|
||||
authority_digest: format!("sha256:{}", "a".repeat(64)),
|
||||
command: PathBuf::from("/fixture/node"),
|
||||
args: vec!["/fixture/sidecar.cjs".to_owned()],
|
||||
artifacts: vec![crate::durable::QualifiedLaunchArtifact {
|
||||
path: PathBuf::from("/fixture/node"),
|
||||
sha256: format!("sha256:{}", "b".repeat(64)),
|
||||
}],
|
||||
candidate_admission: None,
|
||||
});
|
||||
current.validate().unwrap();
|
||||
let mut next_grant = grant.clone();
|
||||
next_grant.run_id = "run_2".to_owned();
|
||||
let mut attach = command("run.attach");
|
||||
attach.payload = json!({"paperclipNextAuthority":{
|
||||
"identity":{"runnerInstanceId":current.runner_instance_id,"environmentLeaseId":current.environment_lease_id,
|
||||
"runId":"run_2","normalizedSessionId":current.normalized_session_id,"turnId":"turn_2","itemId":"item_2"},
|
||||
"connection":{"mode":"external"}
|
||||
}});
|
||||
let next = next_authority_config(&attach, ¤t).unwrap().unwrap();
|
||||
assert!(next
|
||||
.acpx_launch_profile
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.candidate_admission
|
||||
.is_none());
|
||||
assert_eq!(next.acpx_launch_profile, current.acpx_launch_profile);
|
||||
assert_eq!(next.run_id, "run_2");
|
||||
assert_eq!(next.turn_id, "turn_2");
|
||||
assert_eq!(next.item_id, "item_2");
|
||||
let mut injected = attach.clone();
|
||||
injected.payload["paperclipNextAuthority"]["candidateAdmission"] = json!(next_grant);
|
||||
assert!(next_authority_config(&injected, ¤t).is_err());
|
||||
assert_eq!(next.runner_instance_id, current.runner_instance_id);
|
||||
assert_eq!(next.environment_lease_id, current.environment_lease_id);
|
||||
assert_eq!(next.normalized_session_id, current.normalized_session_id);
|
||||
for (field, value) in [
|
||||
("companyId", json!("foreign-company")),
|
||||
("agent", json!("antigravity")),
|
||||
("model", json!("different-model")),
|
||||
("profileDigest", json!(format!("sha256:{}", "f".repeat(64)))),
|
||||
(
|
||||
"nativeClosureDigest",
|
||||
json!(format!("sha256:{}", "e".repeat(64))),
|
||||
),
|
||||
("expiresAt", json!(grant.expires_at + 1)),
|
||||
("runId", json!("unbound-run")),
|
||||
("normalizedSessionId", json!("foreign-session")),
|
||||
] {
|
||||
let mut changed = injected.clone();
|
||||
changed.payload["paperclipNextAuthority"]["candidateAdmission"][field] = value;
|
||||
assert!(
|
||||
next_authority_config(&changed, ¤t).is_err(),
|
||||
"{field}"
|
||||
);
|
||||
}
|
||||
for field in [
|
||||
"runnerInstanceId",
|
||||
"environmentLeaseId",
|
||||
"normalizedSessionId",
|
||||
] {
|
||||
let mut changed = attach.clone();
|
||||
changed.payload["paperclipNextAuthority"]["identity"][field] = json!("foreign");
|
||||
assert!(
|
||||
next_authority_config(&changed, ¤t).is_err(),
|
||||
"{field}"
|
||||
);
|
||||
}
|
||||
let mut missing = injected.clone();
|
||||
missing.payload["paperclipNextAuthority"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("candidateAdmission");
|
||||
assert!(next_authority_config(&missing, ¤t).unwrap().is_some());
|
||||
let mut stale_candidate = current.clone();
|
||||
stale_candidate
|
||||
.acpx_launch_profile
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.candidate_admission = Some(grant);
|
||||
assert!(stale_candidate.validate().is_err());
|
||||
assert!(next_authority_config(&injected, &stale_candidate).is_err());
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,18 @@ pub(crate) struct AcpxReleaseProfile {
|
||||
pub qualification_pending: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn kimi_native_closure(platform: &str, architecture: &str) -> Option<&'static str> {
|
||||
match (platform, architecture) {
|
||||
("darwin", "arm64") => {
|
||||
Some("6038325abd0431c30097ff1f572a9b29816e1b4da372d8cb86b2ca5d18ea34e7")
|
||||
}
|
||||
("linux", "x64") => {
|
||||
Some("105d8394adbbc76a953a12cc51b5288eb940ad693291e584f6cdc6407213164d")
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
Some(match agent {
|
||||
"antigravity" => AcpxReleaseProfile {
|
||||
@@ -85,6 +97,16 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
requires_provider_policy: false,
|
||||
qualification_pending: false,
|
||||
},
|
||||
"kimi" => AcpxReleaseProfile {
|
||||
agent_server_package: "@moonshot-ai/kimi-code",
|
||||
agent_server_version: "2.3.3",
|
||||
agent_runtime_package: None,
|
||||
agent_runtime_version: None,
|
||||
command_digest:
|
||||
"sha256:6ee5fb7ac18c1361ac63effe9117d08ed129add8349756a140e73e0c86ba5da7",
|
||||
requires_provider_policy: true,
|
||||
qualification_pending: false,
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -138,7 +138,12 @@ impl NativeProviderCommandExecutor {
|
||||
.config
|
||||
.acpx_launch_profile
|
||||
.as_ref()
|
||||
.is_some_and(|profile| profile.candidate_admission.is_some())
|
||||
.is_some_and(|profile| {
|
||||
profile
|
||||
.candidate_admission
|
||||
.as_ref()
|
||||
.is_some_and(|admission| admission.agent == "antigravity")
|
||||
})
|
||||
&& [
|
||||
CODEX_PROVIDER_STATE_FILE,
|
||||
ACPX_PROVIDER_STATE_FILE,
|
||||
|
||||
@@ -454,6 +454,7 @@ impl VerifiedProcessLaunch {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub(crate) fn spawn_stdio(
|
||||
&self,
|
||||
environment: &std::collections::BTreeMap<String, String>,
|
||||
|
||||
@@ -176,92 +176,103 @@ fn promotes_only_the_latest_provider_message_as_the_terminal_reply() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pi_native_boundaries_keep_progress_but_never_promote_tool_narration_or_history() {
|
||||
for final_reason in ["stop", "length", "toolUse", "error", "aborted", "empty"] {
|
||||
let mut state = AcpxProviderState::new("run-1").unwrap();
|
||||
state.begin_turn("turn-1").unwrap();
|
||||
let mut messages = vec![
|
||||
json!({"type":"text_delta","messageId":"history","text":"Earlier session answer","piMessageHistory":true}),
|
||||
json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"start"}}),
|
||||
json!({"type":"text_delta","messageId":"first","text":"Calling finish."}),
|
||||
json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"end","stopReason":"toolUse"}}),
|
||||
json!({"type":"text_delta","messageId":"last","text":"","piMessageBoundary":{"phase":"start"}}),
|
||||
];
|
||||
if final_reason != "empty" {
|
||||
messages.push(json!({"type":"text_delta","messageId":"last","text":"EXACT_MARKER"}));
|
||||
}
|
||||
messages.push(json!({"type":"text_delta","messageId":"last","text":"","piMessageBoundary":{"phase":"end","stopReason":if final_reason == "empty" { "stop" } else { final_reason }}}));
|
||||
let count = messages.len() as u64;
|
||||
for (index, payload) in messages.into_iter().enumerate() {
|
||||
state
|
||||
fn native_boundaries_keep_progress_but_never_promote_tool_narration_or_history() {
|
||||
for (boundary_field, history_field) in [
|
||||
("piMessageBoundary", "piMessageHistory"),
|
||||
("assistantMessageBoundary", "assistantMessageHistory"),
|
||||
] {
|
||||
for final_reason in ["stop", "length", "toolUse", "error", "aborted", "empty"] {
|
||||
let mut state = AcpxProviderState::new("run-1").unwrap();
|
||||
state.begin_turn("turn-1").unwrap();
|
||||
let mut messages = vec![
|
||||
json!({"type":"text_delta","messageId":"history","text":"Earlier session answer",(history_field):true}),
|
||||
json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"start"}}),
|
||||
json!({"type":"text_delta","messageId":"first","text":"Calling finish."}),
|
||||
json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"end","stopReason":"toolUse"}}),
|
||||
json!({"type":"text_delta","messageId":"last","text":"",(boundary_field):{"phase":"start"}}),
|
||||
];
|
||||
if final_reason != "empty" {
|
||||
messages
|
||||
.push(json!({"type":"text_delta","messageId":"last","text":"EXACT_MARKER"}));
|
||||
}
|
||||
messages.push(json!({"type":"text_delta","messageId":"last","text":"",(boundary_field):{"phase":"end","stopReason":if final_reason == "empty" { "stop" } else { final_reason }}}));
|
||||
let count = messages.len() as u64;
|
||||
for (index, payload) in messages.into_iter().enumerate() {
|
||||
state
|
||||
.accept_event(&event(
|
||||
index as u64 + 1,
|
||||
GeneratedAcpxSidecarEventType::RuntimeEvent,
|
||||
Some("turn-1"),
|
||||
payload,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
let terminal = state
|
||||
.accept_event(&event(
|
||||
index as u64 + 1,
|
||||
GeneratedAcpxSidecarEventType::RuntimeEvent,
|
||||
count + 1,
|
||||
GeneratedAcpxSidecarEventType::RuntimeTurnTerminal,
|
||||
Some("turn-1"),
|
||||
payload,
|
||||
json!({"status":"completed"}),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
let terminal = state
|
||||
.accept_event(&event(
|
||||
count + 1,
|
||||
GeneratedAcpxSidecarEventType::RuntimeTurnTerminal,
|
||||
Some("turn-1"),
|
||||
json!({"status":"completed"}),
|
||||
))
|
||||
.unwrap();
|
||||
let replies: Vec<_> = terminal
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
if let AcpxProviderStateEvent::AssistantMessage { text, .. } = entry {
|
||||
Some(text.as_str())
|
||||
let replies: Vec<_> = terminal
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
if let AcpxProviderStateEvent::AssistantMessage { text, .. } = entry {
|
||||
Some(text.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
replies,
|
||||
if matches!(final_reason, "stop" | "length") {
|
||||
vec!["EXACT_MARKER"]
|
||||
} else {
|
||||
None
|
||||
vec![]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
replies,
|
||||
if matches!(final_reason, "stop" | "length") {
|
||||
vec!["EXACT_MARKER"]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pi_native_boundary_order_and_terminal_completeness_fail_closed() {
|
||||
for malformed in [
|
||||
json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"start"}}),
|
||||
json!({"type":"text_delta","messageId":"other","text":"","piMessageBoundary":{"phase":"end","stopReason":"stop"}}),
|
||||
json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"end","stopReason":"invented"}}),
|
||||
json!({"type":"text_delta","messageId":"other","text":"Out of order"}),
|
||||
fn native_boundary_order_and_terminal_completeness_fail_closed() {
|
||||
for (boundary_field, _history_field) in [
|
||||
("piMessageBoundary", "piMessageHistory"),
|
||||
("assistantMessageBoundary", "assistantMessageHistory"),
|
||||
] {
|
||||
for malformed in [
|
||||
json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"start"}}),
|
||||
json!({"type":"text_delta","messageId":"other","text":"",(boundary_field):{"phase":"end","stopReason":"stop"}}),
|
||||
json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"end","stopReason":"invented"}}),
|
||||
json!({"type":"text_delta","messageId":"other","text":"Out of order"}),
|
||||
] {
|
||||
let mut state = AcpxProviderState::new("run-1").unwrap();
|
||||
state.begin_turn("turn-1").unwrap();
|
||||
state.accept_event(&event(1, GeneratedAcpxSidecarEventType::RuntimeEvent, Some("turn-1"), json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"start"}}))).unwrap();
|
||||
assert!(state
|
||||
.accept_event(&event(
|
||||
2,
|
||||
GeneratedAcpxSidecarEventType::RuntimeEvent,
|
||||
Some("turn-1"),
|
||||
malformed
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
let mut state = AcpxProviderState::new("run-1").unwrap();
|
||||
state.begin_turn("turn-1").unwrap();
|
||||
state.accept_event(&event(1, GeneratedAcpxSidecarEventType::RuntimeEvent, Some("turn-1"), json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"start"}}))).unwrap();
|
||||
state.accept_event(&event(1, GeneratedAcpxSidecarEventType::RuntimeEvent, Some("turn-1"), json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"start"}}))).unwrap();
|
||||
assert!(state
|
||||
.accept_event(&event(
|
||||
2,
|
||||
GeneratedAcpxSidecarEventType::RuntimeEvent,
|
||||
GeneratedAcpxSidecarEventType::RuntimeTurnTerminal,
|
||||
Some("turn-1"),
|
||||
malformed
|
||||
json!({"status":"completed"})
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
let mut state = AcpxProviderState::new("run-1").unwrap();
|
||||
state.begin_turn("turn-1").unwrap();
|
||||
state.accept_event(&event(1, GeneratedAcpxSidecarEventType::RuntimeEvent, Some("turn-1"), json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"start"}}))).unwrap();
|
||||
assert!(state
|
||||
.accept_event(&event(
|
||||
2,
|
||||
GeneratedAcpxSidecarEventType::RuntimeTurnTerminal,
|
||||
Some("turn-1"),
|
||||
json!({"status":"completed"})
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6970,7 +6970,12 @@ fn explicit_skill_input_survives_durable_turn_and_cold_restore() {
|
||||
executor.shutdown().unwrap();
|
||||
drop(executor);
|
||||
let mut restored = CodexCommandExecutor::with_runner_config(&directory, &runner_config);
|
||||
let skill = json!({"type":"skill", "name":"first-task", "path":"/materialized/skills/first-task/SKILL.md"});
|
||||
let skill_path = if cfg!(windows) {
|
||||
r"C:\materialized\skills\first-task\SKILL.md"
|
||||
} else {
|
||||
"/materialized/skills/first-task/SKILL.md"
|
||||
};
|
||||
let skill = json!({"type":"skill", "name":"first-task", "path":skill_path});
|
||||
restored
|
||||
.execute(&command(
|
||||
"turn",
|
||||
|
||||
@@ -558,6 +558,7 @@ fn shutdown_recovered_acpx_fixture(executor: &mut NativeProviderCommandExecutor,
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn acpx_response_delivery_survives_crash_before_journaling_without_replaying_the_response() {
|
||||
for mode in [
|
||||
"permissions-interactive",
|
||||
@@ -633,6 +634,7 @@ fn acpx_response_delivery_survives_crash_before_journaling_without_replaying_the
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn acpx_delivered_response_is_acknowledged_once_before_session_close() {
|
||||
for mode in ["permissions-interactive", "resolutions"] {
|
||||
let directory = temporary_directory("response-ack");
|
||||
@@ -662,6 +664,7 @@ fn acpx_delivered_response_is_acknowledged_once_before_session_close() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn acpx_uncommitted_delivery_expires_after_failed_state_write_without_replay() {
|
||||
for mode in ["permissions-interactive", "resolutions"] {
|
||||
let directory = temporary_directory("response-save-failure");
|
||||
@@ -743,6 +746,7 @@ fn acpx_uncommitted_delivery_expires_after_failed_state_write_without_replay() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn acpx_failed_settlement_ack_replays_the_same_delivered_event_without_expiry() {
|
||||
for mode in ["permissions-interactive", "resolutions"] {
|
||||
let directory = temporary_directory("response-ack-save-failure");
|
||||
@@ -808,6 +812,7 @@ fn acpx_failed_settlement_ack_replays_the_same_delivered_event_without_expiry()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn acpx_uncommitted_expiry_and_cancellation_cannot_escape_through_poll_or_drain() {
|
||||
for (mode, close) in [
|
||||
("permissions-wrong-ack", false),
|
||||
@@ -879,6 +884,7 @@ fn acpx_uncommitted_expiry_and_cancellation_cannot_escape_through_poll_or_drain(
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn acpx_failed_or_cancelled_responses_retain_one_durable_settlement() {
|
||||
for (mode, close) in [
|
||||
("permissions-wrong-ack", false),
|
||||
@@ -937,6 +943,7 @@ fn acpx_failed_or_cancelled_responses_retain_one_durable_settlement() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn preserves_acpx_semantic_disposition_in_the_run_terminal() {
|
||||
let directory = temporary_directory("acpx-blocked");
|
||||
let config = acpx_config(&directory, "turns-reserved-block-terminal");
|
||||
@@ -1072,6 +1079,7 @@ fn preserves_managed_provider_descriptors_through_the_native_selector() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn executes_a_qualified_acpx_profile_through_the_native_selector() {
|
||||
let directory = temporary_directory("acpx");
|
||||
let config = acpx_config(&directory, "turns-reserved-result-terminal");
|
||||
@@ -1131,6 +1139,7 @@ fn executes_a_qualified_acpx_profile_through_the_native_selector() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn routes_reserved_completion_feedback_with_the_full_controller_catalog() {
|
||||
let directory = temporary_directory("acpx-controller-completion");
|
||||
let mode = "turns-reserved-feedback-roundtrip";
|
||||
@@ -1215,6 +1224,7 @@ fn routes_reserved_completion_feedback_with_the_full_controller_catalog() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn acpx_transport_failure_retains_its_error_without_reopening_the_provider() {
|
||||
let directory = temporary_directory("acpx-transport-failure");
|
||||
let mode = "turns-malformed-activity";
|
||||
@@ -1276,6 +1286,7 @@ fn acpx_transport_failure_retains_its_error_without_reopening_the_provider() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn provider_death_expires_input_and_commits_failure_without_querying_the_dead_provider() {
|
||||
let directory = temporary_directory("acpx-provider-death");
|
||||
let mode = "turns-provider-death";
|
||||
@@ -1381,6 +1392,7 @@ fn provider_death_expires_input_and_commits_failure_without_querying_the_dead_pr
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn resumes_an_idle_acpx_session_in_a_cold_replacement_runner() {
|
||||
let directory = temporary_directory("acpx-cold-idle-recovery");
|
||||
let config = acpx_config(&directory, "turns-reserved-result-terminal");
|
||||
@@ -1432,6 +1444,7 @@ fn resumes_an_idle_acpx_session_in_a_cold_replacement_runner() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn keeps_native_acpx_semantic_events_on_the_durable_controller_turn() {
|
||||
let directory = temporary_directory("acpx-durable-turn-correlation");
|
||||
let config = acpx_config(&directory, "turns-tool");
|
||||
@@ -1481,6 +1494,7 @@ fn keeps_native_acpx_semantic_events_on_the_durable_controller_turn() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn starts_a_distinct_acpx_provider_turn_for_same_run_recovery() {
|
||||
let directory = temporary_directory("acpx-same-run-recovery");
|
||||
let config = acpx_config(&directory, "turns-reserved-result-terminal");
|
||||
@@ -1537,6 +1551,7 @@ fn starts_a_distinct_acpx_provider_turn_for_same_run_recovery() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn executes_opencode_through_the_local_facade_without_codex_event_labels() {
|
||||
let directory = temporary_directory("opencode");
|
||||
let config = opencode_config(&directory);
|
||||
@@ -1605,6 +1620,7 @@ fn executes_opencode_through_the_local_facade_without_codex_event_labels() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn replacement_shutdown_restores_the_persisted_provider_before_cleanup() {
|
||||
let directory = temporary_directory("opencode-replacement-shutdown");
|
||||
let config = opencode_config(&directory);
|
||||
@@ -1730,6 +1746,7 @@ fn rejects_pi_without_an_explicit_model_before_starting_a_sidecar() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn pi_thinking_change_cannot_attach_to_an_existing_provider_identity() {
|
||||
let directory = temporary_directory("pi-thinking-attach");
|
||||
let config = pi_acpx_config(&directory, "bootstrap");
|
||||
@@ -1770,6 +1787,7 @@ fn pi_thinking_change_cannot_attach_to_an_existing_provider_identity() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn stops_an_idle_pi_provider_before_suspension_without_waiting_for_its_close_rpc() {
|
||||
let directory = temporary_directory("pi-idle-suspension");
|
||||
let mut config = pi_acpx_config(&directory, "bootstrap");
|
||||
@@ -1818,6 +1836,7 @@ fn stops_an_idle_pi_provider_before_suspension_without_waiting_for_its_close_rpc
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn idle_pi_stop_cannot_publish_a_reusable_identity_while_its_lifetime_fence_is_held() {
|
||||
let directory = temporary_directory("pi-idle-held-lifetime");
|
||||
// Linux can assign port 0 below the identity contract's dynamic-port range.
|
||||
@@ -1886,6 +1905,7 @@ fn idle_pi_stop_cannot_publish_a_reusable_identity_while_its_lifetime_fence_is_h
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(windows, ignore = "verified launch is not supported on Windows yet")]
|
||||
fn publishes_only_changed_pi_controls_before_the_new_turn() {
|
||||
for (mode, initially_available, turn_available) in [
|
||||
("controls-lazy", false, true),
|
||||
|
||||
@@ -28,6 +28,10 @@ export const ACPX_RELEASE_ATTESTATIONS = Object.freeze({
|
||||
nodeVersion: ["pi-node-pins.ts", "PI_NODE_VERSION"],
|
||||
closure: ["pi-closure-pins.ts", "PI_DISTRIBUTION_CLOSURE_SHA256"],
|
||||
} },
|
||||
kimi: { kind: "closure", pins: {
|
||||
nodeVersion: ["kimi-closure-pins.ts", "KIMI_NODE_VERSION"],
|
||||
closure: ["kimi-closure-pins.ts", "KIMI_DISTRIBUTION_CLOSURE_SHA256"],
|
||||
} },
|
||||
cursor: { kind: "declaration", path: "cursor-contract.json", digestField: "commandDigest" },
|
||||
antigravity: { kind: "declaration", path: "antigravity-distributions.json" },
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ const packageFiles = [
|
||||
"scripts/acpx-release-attestations.mjs", "scripts/generate-acpx-profiles.mjs",
|
||||
"src/providers/grok/launcher.cjs", "src/drivers/acpx/copilot-closure-pins.ts",
|
||||
"src/drivers/acpx/pi-closure-pins.ts", "src/drivers/acpx/pi-node-pins.ts",
|
||||
"src/drivers/acpx/kimi-closure-pins.ts",
|
||||
"src/drivers/acpx/generated-profiles.ts", "runner/crates/runner-core/src/generated_acpx_profiles.rs",
|
||||
];
|
||||
|
||||
@@ -37,7 +38,7 @@ const generate = (root, ...args) => spawnSync(process.execPath, [join(root, "scr
|
||||
|
||||
test("every profile declares how its digest attests the upstream release", async () => {
|
||||
assert.deepEqual(Object.keys(ACPX_RELEASE_ATTESTATIONS).sort(), Object.keys(manifest.profiles).sort());
|
||||
assert.deepEqual(COMPUTED.sort(), ["antigravity", "copilot", "cursor", "grok", "pi"]);
|
||||
assert.deepEqual(COMPUTED.sort(), ["antigravity", "copilot", "cursor", "grok", "kimi", "pi"]);
|
||||
for (const agent of COMPUTED) {
|
||||
assert.equal(manifest.profiles[agent].commandDigest, (await acpxReleaseAttestation(agent, packageRoot)).commandDigest, agent);
|
||||
}
|
||||
@@ -66,10 +67,11 @@ test("a relocated copy of the provider inputs computes the same attestations", a
|
||||
});
|
||||
|
||||
test("the attestations bind no Paperclip source beyond the Grok launcher", async () => {
|
||||
for (const agent of ["copilot", "pi"]) {
|
||||
for (const agent of ["copilot", "pi", "kimi"]) {
|
||||
const { declaration } = await acpxReleaseAttestation(agent, packageRoot);
|
||||
assert.deepEqual(Object.keys(declaration).filter(key => /source/iu.test(key)), [], agent);
|
||||
assert.deepEqual(Object.keys(declaration.closure).sort(), ["darwin-arm64", "darwin-x64", "linux-x64"]);
|
||||
assert.deepEqual(Object.keys(declaration.closure).sort(), agent === "kimi"
|
||||
? ["darwin-arm64", "linux-x64"] : ["darwin-arm64", "darwin-x64", "linux-x64"]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,6 +88,10 @@ for (const [name, agents, edit] of [
|
||||
const path = join(root, "src/drivers/acpx/pi-closure-pins.ts");
|
||||
writeFileSync(path, readFileSync(path, "utf8").replace(/"linux-x64": "[a-f0-9]{64}"/u, `"linux-x64": "${"0".repeat(64)}"`));
|
||||
}],
|
||||
["a Kimi platform closure", ["kimi"], root => {
|
||||
const path = join(root, "src/drivers/acpx/kimi-closure-pins.ts");
|
||||
writeFileSync(path, readFileSync(path, "utf8").replace(/"linux-x64": "[a-f0-9]{64}"/u, `"linux-x64": "${"0".repeat(64)}"`));
|
||||
}],
|
||||
["the Grok launcher bytes", ["grok"], root => {
|
||||
const path = join(root, "src/providers/grok/launcher.cjs");
|
||||
writeFileSync(path, `${readFileSync(path, "utf8")}\n// changed\n`);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { dirname, join, relative, resolve } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseProviderPackArguments, materializeCandidateProviderPack, providerPackProviders, providerPackManifestFields } from "./candidate-provider-pack.mjs";
|
||||
import { smokeKimiProviderPack } from "./kimi-provider-pack-smoke.mjs";
|
||||
import { buildNodeStartupTimeout } from "./build-node-startup-timeout.mjs";
|
||||
import { writePortableExecutableShim, writePortableCopilotShims } from "./provider-pack-executable-shims.mjs";
|
||||
import { providerPackLibc, removeControllerArtifactsFromProviderPack, removeForeignLibcPackagesFromProviderPack } from "./provider-pack-runtime-tree.mjs";
|
||||
@@ -183,7 +184,7 @@ try {
|
||||
|| !/^sha256:[a-f0-9]{64}$/.test(metadata.profileDigest)
|
||||
|| !/^sha256:[a-f0-9]{64}$/.test(metadata.closureDigest)) throw new Error("Candidate builder omitted its pinned identity");
|
||||
candidateProviders[provider] = { version: metadata.version, profileDigest: metadata.profileDigest,
|
||||
closureDigest: metadata.closureDigest, qualification: ["pi", "cursor", "copilot"].includes(provider) ? "qualified" : "pending", path: assetPath,
|
||||
closureDigest: metadata.closureDigest, qualification: ["pi", "cursor", "copilot"].includes(provider) || provider === "kimi" && profiles.profiles.kimi.qualificationStatus !== "pending" ? "qualified" : "pending", path: assetPath,
|
||||
sha256: sha256Tree(join(temporaryRoot, assetPath)) };
|
||||
}
|
||||
|
||||
@@ -386,6 +387,11 @@ try {
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
|
||||
if (payload.providers?.kimi && profiles.profiles.kimi.qualificationStatus !== "pending") {
|
||||
const smoke = await smokeKimiProviderPack(temporaryRoot);
|
||||
process.stdout.write(`${JSON.stringify(smoke)}\n`);
|
||||
}
|
||||
|
||||
rmSync(outputRoot, { recursive: true, force: true });
|
||||
renameSync(temporaryRoot, outputRoot);
|
||||
process.stdout.write(`${outputRoot}\n`);
|
||||
|
||||
@@ -8,6 +8,12 @@ import { build } from "esbuild";
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
export const verifiedProviderEntrypoints = Object.freeze([
|
||||
Object.freeze({
|
||||
name: "kimi-metadata-probe",
|
||||
source: resolve(packageRoot, "src/cli/kimi-metadata-probe.ts"),
|
||||
output: resolve(packageRoot, "dist/cli/kimi-metadata-probe.js"),
|
||||
verifiedOutput: resolve(packageRoot, "dist/cli/kimi-metadata-probe.cjs"),
|
||||
}),
|
||||
Object.freeze({
|
||||
name: "provision-antigravity",
|
||||
source: resolve(packageRoot, "scripts/provision-antigravity.mjs"),
|
||||
@@ -80,6 +86,7 @@ export async function bundleVerifiedProviderEntrypoints({ write = true } = {}) {
|
||||
platform: "node",
|
||||
format,
|
||||
target: "node24",
|
||||
absWorkingDir: entrypoint.name === "kimi-metadata-probe" ? packageRoot : undefined,
|
||||
packages: "bundle",
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
@@ -116,6 +123,7 @@ export async function bundleVerifiedProviderEntrypoints({ write = true } = {}) {
|
||||
results.push({ entrypoint, result, verifiedResult });
|
||||
}
|
||||
await bundlePiProvisioner({ write });
|
||||
await bundleKimiProvisioner({ write });
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -146,6 +154,28 @@ export async function bundlePiProvisioner({ write = true, outputRoot = resolve(p
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function bundleKimiProvisioner({ write = true, outputRoot = resolve(packageRoot, "dist/cli") } = {}) {
|
||||
const entrypoint = { name: "provision-kimi", source: resolve(packageRoot, "scripts/provision-kimi.mjs") };
|
||||
const result = await build({
|
||||
entryPoints: [entrypoint.source], outfile: resolve(outputRoot, "provision-kimi.cjs"),
|
||||
bundle: true, platform: "node", format: "cjs", target: "node24", packages: "bundle",
|
||||
splitting: false, sourcemap: false, legalComments: "none", metafile: true,
|
||||
treeShaking: true, write, logLevel: "silent",
|
||||
banner: { js: 'const __paperclipVerifiedEntrypointUrl = require("node:url").pathToFileURL(__filename).href;' },
|
||||
define: { "import.meta.dirname": "__dirname", "import.meta.url": "__paperclipVerifiedEntrypointUrl" },
|
||||
});
|
||||
assertSelfContainedBundle(entrypoint, result);
|
||||
if (write) {
|
||||
const inputs = resolve(outputRoot, "kimi-provision-inputs");
|
||||
await mkdir(inputs, { recursive: true });
|
||||
for (const [source, name] of [
|
||||
["scripts/kimi-distribution/package.json", "package.json"],
|
||||
["scripts/kimi-distribution/package-lock.json", "package-lock.json"],
|
||||
]) await cp(resolve(packageRoot, source), resolve(inputs, name));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1]
|
||||
? pathToFileURL(resolve(process.argv[1])).href
|
||||
: null;
|
||||
|
||||
@@ -41,6 +41,11 @@ test("written provider entrypoints satisfy qualified launch permissions", async
|
||||
|
||||
// Package-layout regression runs in the existing verified-entrypoint test lane.
|
||||
import "./provision-pi-package.test.mjs";
|
||||
|
||||
import "./materialize-kimi-distribution.test.mjs";
|
||||
import "./provision-kimi-package.test.mjs";
|
||||
import "./patch-kimi-runtime.test.mjs";
|
||||
import "./kimi-metadata-probe.test.mjs";
|
||||
import "./antigravity-provisioner-layout.test.mjs";
|
||||
import "./materialize-antigravity-distribution.test.mjs";
|
||||
import "./probe-antigravity-metadata.test.mjs";
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { materializePiDistribution } from "./materialize-pi-distribution.mjs";
|
||||
import profiles from "../acpx-profiles.json" with { type: "json" };
|
||||
import { materializePinnedCursorDistribution } from "./materialize-cursor-distribution.mjs";
|
||||
const CANDIDATES = new Set(["cursor", "copilot", "pi"]);
|
||||
const CANDIDATES = new Set(["cursor", "copilot", "pi", "kimi"]);
|
||||
const releasedKimi = () => profiles.profiles.kimi.qualificationStatus !== "pending";
|
||||
const ordinaryProviders = () => ["pi", "cursor", "copilot", ...(releasedKimi() ? ["kimi"] : [])];
|
||||
|
||||
/** Only materialized providers enter the serialized manifest and its digest. */
|
||||
export function providerPackManifestFields(providers, candidates) {
|
||||
return {
|
||||
...(["pi", "cursor", "copilot"].some(provider => providers[provider]) ? { providers: Object.fromEntries(["pi", "cursor", "copilot"].filter(provider => providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
|
||||
...(["pi", ...candidates].some(provider => provider !== "copilot" && providers[provider]) ? { candidateProviders: Object.fromEntries([...new Set(["pi", ...candidates])].filter(provider => provider !== "copilot" && providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
|
||||
...(ordinaryProviders().some(provider => providers[provider]) ? { providers: Object.fromEntries(ordinaryProviders().filter(provider => providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
|
||||
...(["pi", ...candidates].some(provider => provider !== "copilot" && !(provider === "kimi" && releasedKimi()) && providers[provider]) ? { candidateProviders: Object.fromEntries([...new Set(["pi", ...candidates])].filter(provider => provider !== "copilot" && !(provider === "kimi" && releasedKimi()) && providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function providerPackProviders(platform, architecture, candidates) {
|
||||
const nativeSupported = ["darwin-arm64", "darwin-x64", "linux-x64"].includes(`${platform}-${architecture}`);
|
||||
return [...new Set([...(nativeSupported ? ["pi", "cursor", "copilot"] : []), ...candidates])];
|
||||
const kimiSupported = ["darwin-arm64", "linux-x64"].includes(`${platform}-${architecture}`);
|
||||
return [...new Set([...(nativeSupported ? ["pi", "cursor", "copilot"] : []), ...(releasedKimi() && kimiSupported ? ["kimi"] : []), ...candidates])];
|
||||
}
|
||||
|
||||
export function parseProviderPackArguments(args) {
|
||||
@@ -34,6 +37,11 @@ export function parseProviderPackArguments(args) {
|
||||
/** Closed source-owned builder registry; provider branches add their exact pins. */
|
||||
export async function materializeCandidateProviderPack({ provider, outputRoot }) {
|
||||
if (!CANDIDATES.has(provider)) throw new Error("Unknown candidate provider");
|
||||
if (provider === "kimi") {
|
||||
const { materializeKimiDistribution } = await import("./materialize-kimi-distribution.mjs");
|
||||
const result = await materializeKimiDistribution({ outputRoot });
|
||||
return { version: result.version, profileDigest: profiles.profiles.kimi.commandDigest, closureDigest: result.closureDigest };
|
||||
}
|
||||
if (provider === "pi") return materializePiDistribution({ outputRoot });
|
||||
if (provider === "copilot") {
|
||||
const { buildPinnedCopilotDistribution } = await import("./build-copilot-distribution.mjs");
|
||||
|
||||
@@ -43,11 +43,36 @@ test("provider manifest digest survives disk JSON on supported and unsupported t
|
||||
test("candidate options stay explicit and supported native assets are included by default", () => {
|
||||
assert.deepEqual(parseProviderPackArguments(["--", "/pack"]), { output: "/pack", candidates: [] });
|
||||
assert.deepEqual(parseProviderPackArguments(["/pack", "--candidate-providers=pi,cursor"]), { output: "/pack", candidates: ["pi", "cursor"] });
|
||||
assert.deepEqual(parseProviderPackArguments(["/pack", "--candidate-providers=kimi"]), { output: "/pack", candidates: ["kimi"] });
|
||||
});
|
||||
|
||||
test("released Kimi enters ordinary inventory on only its two supported targets", () => {
|
||||
const kimi = { version: "2.3.3", qualification: "qualified", profileDigest: "sha256:profile", closureDigest: "sha256:closure" };
|
||||
for (const [platform, architecture] of [["darwin", "arm64"], ["linux", "x64"]]) {
|
||||
assert.equal(providerPackProviders(platform, architecture, []).filter(provider => provider === "kimi").length, 1);
|
||||
assert.equal(providerPackProviders(platform, architecture, ["kimi"]).filter(provider => provider === "kimi").length, 1);
|
||||
}
|
||||
for (const [platform, architecture] of [["darwin", "x64"], ["linux", "arm64"], ["win32", "x64"]]) {
|
||||
assert.equal(providerPackProviders(platform, architecture, []).includes("kimi"), false);
|
||||
}
|
||||
for (const candidates of [[], ["kimi"]]) {
|
||||
const fields = providerPackManifestFields({ kimi }, candidates);
|
||||
assert.equal(fields.providers.kimi, kimi);
|
||||
assert.equal(fields.candidateProviders?.kimi, undefined);
|
||||
assert.equal(digest(JSON.parse(JSON.stringify(fields))), digest(fields));
|
||||
}
|
||||
});
|
||||
|
||||
test("Kimi's candidate builder rejects invalid paths before any public download", async t => {
|
||||
let requests = 0;
|
||||
t.mock.method(globalThis, "fetch", async () => { requests++; throw new Error("Unexpected Kimi download"); });
|
||||
await assert.rejects(materializeCandidateProviderPack({ provider: "kimi", outputRoot: "relative-kimi-output" }), /Kimi distribution output must be absolute/);
|
||||
assert.equal(requests, 0);
|
||||
});
|
||||
test("normal packs include Cursor and Copilot on their three pinned targets without breaking other hosts", () => {
|
||||
for (const [platform, architecture] of [["darwin", "arm64"], ["darwin", "x64"], ["linux", "x64"]]) {
|
||||
assert.deepEqual(providerPackProviders(platform, architecture, []), ["pi", "cursor", "copilot"]);
|
||||
assert.deepEqual(providerPackProviders(platform, architecture, ["cursor"]), ["pi", "cursor", "copilot"]);
|
||||
assert.deepEqual(providerPackProviders(platform, architecture, []), ["pi", "cursor", "copilot", ...((platform === "darwin" && architecture === "arm64") || platform === "linux" ? ["kimi"] : [])]);
|
||||
assert.deepEqual(providerPackProviders(platform, architecture, ["cursor"]), providerPackProviders(platform, architecture, []));
|
||||
}
|
||||
assert.deepEqual(providerPackProviders("linux", "arm64", []), []);
|
||||
assert.deepEqual(providerPackProviders("win32", "x64", []), []);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../src/drivers/acpx/kimi-closure-pins.ts";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -90,6 +91,13 @@ pub(crate) struct AcpxReleaseProfile {
|
||||
pub qualification_pending: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn kimi_native_closure(platform: &str, architecture: &str) -> Option<&'static str> {
|
||||
match (platform, architecture) {
|
||||
${Object.entries(KIMI_DISTRIBUTION_CLOSURE_SHA256).map(([target, digest]) => { const [platform, architecture] = target.split("-"); return ` (${quote(platform)}, ${quote(architecture)}) => {\n Some(${quote(digest)})\n }`; }).join("\n")}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
Some(match agent {
|
||||
${entries.map(([agent, p]) => ` ${quote(agent)} => AcpxReleaseProfile {
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
{
|
||||
"name": "paperclip-kimi-runtime-distribution",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paperclip-kimi-runtime-distribution",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@moonshot-ai/kimi-code": "2.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz",
|
||||
"integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@mariozechner/clipboard-darwin-arm64": "0.3.9",
|
||||
"@mariozechner/clipboard-darwin-universal": "0.3.9",
|
||||
"@mariozechner/clipboard-darwin-x64": "0.3.9",
|
||||
"@mariozechner/clipboard-linux-arm64-gnu": "0.3.9",
|
||||
"@mariozechner/clipboard-linux-arm64-musl": "0.3.9",
|
||||
"@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9",
|
||||
"@mariozechner/clipboard-linux-x64-gnu": "0.3.9",
|
||||
"@mariozechner/clipboard-linux-x64-musl": "0.3.9",
|
||||
"@mariozechner/clipboard-win32-arm64-msvc": "0.3.9",
|
||||
"@mariozechner/clipboard-win32-x64-msvc": "0.3.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard-darwin-arm64": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz",
|
||||
"integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard-darwin-universal": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz",
|
||||
"integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard-darwin-x64": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz",
|
||||
"integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard-linux-arm64-gnu": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz",
|
||||
"integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard-linux-arm64-musl": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz",
|
||||
"integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard-linux-riscv64-gnu": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz",
|
||||
"integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard-linux-x64-gnu": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz",
|
||||
"integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard-linux-x64-musl": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz",
|
||||
"integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard-win32-arm64-msvc": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz",
|
||||
"integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/clipboard-win32-x64-msvc": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz",
|
||||
"integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@moonshot-ai/kimi-code": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@moonshot-ai/kimi-code/-/kimi-code-2.3.3.tgz",
|
||||
"integrity": "sha512-3V8bycucf9KgPRH27S8De2u0ip84/Ry3FpNn6gi5z9JmoEHt9vxiYHHZqiMdyDf5nPm4bv+YKGwKEQL4oYpRTw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"bin": {
|
||||
"kimi": "dist/main.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@mariozechner/clipboard": "^0.3.9",
|
||||
"node-pty": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
|
||||
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
|
||||
"integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "paperclip-kimi-runtime-distribution",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@moonshot-ai/kimi-code": "2.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.11.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export const KIMI_INSTRUCTION_RUNTIME_SOURCE = [
|
||||
"function createKimiInstructionRuntime(raw, { createHash, byteLength }) {",
|
||||
" const schema = \"paperclip.kimi.instructions.v1\";",
|
||||
" const fail = () => { throw new Error(\"Kimi instruction binding is missing or invalid\"); };",
|
||||
" if (typeof raw !== \"string\" || byteLength(raw) > 200_000) fail();",
|
||||
" let value;",
|
||||
" try { value = JSON.parse(raw); } catch { fail(); }",
|
||||
" if (!value || typeof value !== \"object\" || Array.isArray(value)",
|
||||
" || Object.keys(value).sort().join(\",\") !== \"byteLength,content,digest,schema\"",
|
||||
" || value.schema !== schema || typeof value.content !== \"string\"",
|
||||
" || value.content.includes(\"\\0\") || !value.content.isWellFormed()",
|
||||
" || byteLength(value.content) > 32_768 || value.byteLength !== byteLength(value.content)",
|
||||
" || typeof value.digest !== \"string\") fail();",
|
||||
" const digest = text => `sha256:${createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\")}`;",
|
||||
" const content = value.content;",
|
||||
" if (value.digest !== digest(content)) fail();",
|
||||
" const suffix = content.length === 0 ? \"\" : `\\n\\n${content}`;",
|
||||
" const append = base => {",
|
||||
" if (typeof base !== \"string\" || byteLength(base) > 2_097_152) fail();",
|
||||
" return suffix.length === 0 || base.endsWith(suffix) ? base : `${base}${suffix}`;",
|
||||
" };",
|
||||
" return Object.freeze({",
|
||||
" append,",
|
||||
" receipt(sessionId, effectivePrompt) {",
|
||||
" if (typeof sessionId !== \"string\" || !sessionId.trim() || append(effectivePrompt) !== effectivePrompt) fail();",
|
||||
" return Object.freeze({ schema, sessionId, digest: value.digest,",
|
||||
" byteLength: byteLength(content), effectiveDigest: digest(effectivePrompt),",
|
||||
" effectiveByteLength: byteLength(effectivePrompt) });",
|
||||
" },",
|
||||
" });",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
export const createKimiInstructionRuntime = new Function(`return (${KIMI_INSTRUCTION_RUNTIME_SOURCE});`)();
|
||||
@@ -0,0 +1,127 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, mkdir, readFile, realpath, rm, stat, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { build } from "esbuild";
|
||||
import { bundleVerifiedProviderEntrypoints } from "./build-verified-provider-entrypoints.mjs";
|
||||
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const sha = bytes => `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
||||
|
||||
test("Kimi metadata CLI is self-contained and rejects incomplete assets without inheriting credentials or making requests", { timeout: 20_000 }, async t => {
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-metadata-cli-")));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const bundles = await bundleVerifiedProviderEntrypoints({ write: false });
|
||||
const bundle = bundles.find(value => value.entrypoint.name === "kimi-metadata-probe");
|
||||
assert.ok(bundle, "The ordinary provider pack build must include the metadata CLI");
|
||||
const bytes = bundle.verifiedResult.outputFiles[0].contents;
|
||||
const relocatedBuild = spawnSync(process.execPath, ["--input-type=module", "-e", `
|
||||
import { createHash } from "node:crypto";
|
||||
import { bundleVerifiedProviderEntrypoints } from ${JSON.stringify(new URL("./build-verified-provider-entrypoints.mjs", import.meta.url).href)};
|
||||
const bundles = await bundleVerifiedProviderEntrypoints({ write: false });
|
||||
const bytes = bundles.find(value => value.entrypoint.name === "kimi-metadata-probe").verifiedResult.outputFiles[0].contents;
|
||||
process.stdout.write("sha256:" + createHash("sha256").update(bytes).digest("hex"));
|
||||
`], { cwd: root, env: { PATH: "/usr/bin:/bin" }, encoding: "utf8", timeout: 10_000 });
|
||||
assert.ifError(relocatedBuild.error);
|
||||
assert.equal(relocatedBuild.status, 0, relocatedBuild.stderr);
|
||||
assert.equal(relocatedBuild.stdout, sha(bytes), "Metadata CLI admission bytes must be independent of the build cwd");
|
||||
const entry = join(root, "kimi-metadata-probe.cjs");
|
||||
await writeFile(entry, bytes);
|
||||
assert.doesNotMatch(Buffer.from(bytes).toString(), /\bimport\.meta\b/);
|
||||
for (const output of Object.values(bundle.verifiedResult.metafile.outputs)) {
|
||||
for (const dependency of output.imports) assert.match(dependency.path, /^node:/);
|
||||
}
|
||||
const manifest = join(root, "package.json");
|
||||
await writeFile(manifest, '{"name":"@paperclipai/paperclip-runner","type":"module"}');
|
||||
const preload = join(root, "deny-external-work.cjs");
|
||||
await writeFile(preload, `
|
||||
const assert = require("node:assert/strict");
|
||||
process.nextTick(() => {
|
||||
for (const name of ["KIMI_API_KEY", "PAPERCLIP_KIMI_PROBE_MODEL", "KIMI_MODEL_DEFAULT", "HOME", "HTTPS_PROXY", "NODE_OPTIONS", "NODE_PATH", "ANTHROPIC_API_KEY", "PAPERCLIP_ACPX_CANDIDATE_ADMISSION"]) {
|
||||
assert.equal(process.env[name], undefined, name);
|
||||
}
|
||||
assert.equal(process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT, ${JSON.stringify(root)});
|
||||
});
|
||||
const fail = () => { throw Error("UNEXPECTED_NETWORK_OR_CHILD"); };
|
||||
globalThis.fetch = fail;
|
||||
for (const name of ["node:net", "node:tls", "node:http", "node:https"]) {
|
||||
const dependency = require(name);
|
||||
for (const key of ["connect", "createConnection", "request", "get"]) if (key in dependency) dependency[key] = fail;
|
||||
}
|
||||
const child = require("node:child_process");
|
||||
for (const key of ["spawn", "exec", "execFile"]) child[key] = fail;
|
||||
`);
|
||||
const pinnedNode = join(packageRoot, "provider-assets/kimi", `${process.platform}-${process.arch}`, "runtime/node/bin/node");
|
||||
const node = await stat(pinnedNode).then(() => pinnedNode, () => process.execPath);
|
||||
const profile = JSON.parse(await readFile(join(packageRoot, "acpx-profiles.json"), "utf8")).profiles.kimi;
|
||||
const { KIMI_DISTRIBUTION_CLOSURE_SHA256 } = await import(new URL("../src/drivers/acpx/kimi-closure-pins.ts", import.meta.url));
|
||||
const admission = { schema: "paperclip.acpx-candidate-admission.v1", companyId: "metadata-cli-company", agent: "kimi", model: "selected-model",
|
||||
profileDigest: profile.commandDigest, nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[`${process.platform}-${process.arch}`]}`,
|
||||
platform: process.platform, architecture: process.arch, expiresAt: Date.now() + 60_000,
|
||||
runId: "metadata-operation", normalizedSessionId: "metadata-session" };
|
||||
const child = spawnSync(node, ["--require", preload, entry], {
|
||||
env: { KIMI_API_KEY: "selected-secret-canary", PAPERCLIP_KIMI_PROBE_MODEL: "selected-model",
|
||||
...(profile.qualificationStatus === "pending" ? { PAPERCLIP_ACPX_CANDIDATE_ADMISSION: JSON.stringify(admission) } : {}),
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: root, PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: manifest,
|
||||
KIMI_MODEL_DEFAULT: "unselected-model", HOME: "/private/unselected-home", HTTPS_PROXY: "http://127.0.0.1:9",
|
||||
ANTHROPIC_API_KEY: "unselected-secret-canary", NODE_OPTIONS: "", NODE_PATH: "/unselected-module-path" },
|
||||
encoding: "utf8", timeout: 10_000,
|
||||
});
|
||||
assert.ifError(child.error);
|
||||
assert.equal(child.status, 1);
|
||||
assert.equal(child.signal, null);
|
||||
assert.doesNotMatch(child.stdout + child.stderr, /UNEXPECTED_NETWORK_OR_CHILD|selected-secret-canary|unselected-secret-canary/);
|
||||
assert.deepEqual(JSON.parse(child.stdout), { schema: "paperclip.kimi.metadata.v1", status: "failed",
|
||||
code: "KIMI_INSTALLATION_INVALID", message: process.versions.node === "24.21.0" || node === pinnedNode
|
||||
? "Install the current verified Kimi Code runtime in this execution environment."
|
||||
: "Kimi metadata verification requires the pinned Node runtime.", promptSent: false });
|
||||
|
||||
const withoutAdmission = spawnSync(node, ["--require", preload, entry], {
|
||||
env: { KIMI_API_KEY: "selected-secret-canary", PAPERCLIP_KIMI_PROBE_MODEL: "selected-model",
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: root, PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: manifest },
|
||||
encoding: "utf8", timeout: 10_000,
|
||||
});
|
||||
assert.ifError(withoutAdmission.error); assert.equal(withoutAdmission.status, 1);
|
||||
if (process.versions.node === "24.21.0" || node === pinnedNode) {
|
||||
assert.equal(JSON.parse(withoutAdmission.stdout).code,
|
||||
profile.qualificationStatus === "pending" ? "KIMI_QUALIFICATION_REQUIRED" : "KIMI_INSTALLATION_INVALID");
|
||||
}
|
||||
assert.doesNotMatch(withoutAdmission.stdout + withoutAdmission.stderr, /UNEXPECTED_NETWORK_OR_CHILD|selected-secret-canary/);
|
||||
|
||||
if (profile.qualificationStatus !== "pending") {
|
||||
const formerAdmission = spawnSync(node, ["--require", preload, entry], {
|
||||
env: { KIMI_API_KEY: "selected-secret-canary", PAPERCLIP_KIMI_PROBE_MODEL: "selected-model",
|
||||
PAPERCLIP_ACPX_CANDIDATE_ADMISSION: JSON.stringify(admission),
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: root, PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: manifest,
|
||||
HOME: "/private/unselected-home", HTTPS_PROXY: "http://127.0.0.1:9", ANTHROPIC_API_KEY: "unselected-secret-canary" },
|
||||
encoding: "utf8", timeout: 10_000,
|
||||
});
|
||||
assert.ifError(formerAdmission.error); assert.equal(formerAdmission.status, 1); assert.equal(formerAdmission.signal, null);
|
||||
assert.deepEqual(JSON.parse(formerAdmission.stdout), { schema: "paperclip.kimi.metadata.v1", status: "failed",
|
||||
code: "KIMI_REQUEST_FAILED", message: "Kimi Code metadata verification failed.", promptSent: false });
|
||||
assert.doesNotMatch(formerAdmission.stdout + formerAdmission.stderr, /UNEXPECTED_NETWORK_OR_CHILD|selected-secret-canary|unselected-secret-canary/);
|
||||
}
|
||||
|
||||
for (const layout of ["runner", "server"]) {
|
||||
const packageDirectory = join(root, layout);
|
||||
const outputRoot = layout === "server" ? join(packageDirectory, "dist/vendor/paperclip-runner") : join(packageDirectory, "dist");
|
||||
const helper = join(outputRoot, "drivers/acpx/kimi-metadata-probe.js");
|
||||
const cli = join(outputRoot, "cli/kimi-metadata-probe.cjs");
|
||||
await mkdir(dirname(cli), { recursive: true });
|
||||
await writeFile(join(packageDirectory, "package.json"), '{"type":"module"}');
|
||||
await writeFile(cli, bytes);
|
||||
await build({ entryPoints: [join(packageRoot, "src/drivers/acpx/kimi-metadata-probe.ts")], outfile: helper,
|
||||
bundle: true, platform: "node", format: "esm", target: "node24", logLevel: "silent" });
|
||||
const api = await import(pathToFileURL(helper).href);
|
||||
assert.equal(await api.kimiMetadataProbeDigest(), sha(bytes), `${layout} fixed artifact`);
|
||||
await writeFile(cli, "different artifact bytes");
|
||||
assert.equal(await api.kimiMetadataProbeDigest(), sha("different artifact bytes"), `${layout} digest is fresh`);
|
||||
await rm(cli);
|
||||
await symlink(entry, cli);
|
||||
await assert.rejects(api.kimiMetadataProbeDigest(), /ELOOP|symbolic/i);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { once } from "node:events";
|
||||
import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
export function assertKimiProviderPackInventory(manifest, profile, closures, platform = process.platform, architecture = process.arch) {
|
||||
assert.equal(manifest.schema, "paperclip-runner/remote-provider-pack/v1");
|
||||
assert.deepEqual(manifest.payload.target, { platform, architecture });
|
||||
const target = `${platform}-${architecture}`;
|
||||
assert.ok(["darwin-arm64", "linux-x64"].includes(target));
|
||||
const pending = profile.qualificationStatus === "pending";
|
||||
const identity = pending ? manifest.payload.candidateProviders?.kimi : manifest.payload.providers?.kimi;
|
||||
assert.equal(pending ? manifest.payload.providers?.kimi : manifest.payload.candidateProviders?.kimi, undefined);
|
||||
assert.equal(identity?.qualification, pending ? "pending" : "qualified");
|
||||
assert.equal(identity.version, profile.agentServerVersion);
|
||||
assert.equal(identity.path, `provider-assets/kimi/${target}`);
|
||||
assert.equal(identity.profileDigest, profile.commandDigest);
|
||||
assert.match(closures[target] ?? "", /^[a-f0-9]{64}$/);
|
||||
assert.equal(identity.closureDigest, `sha256:${closures[target]}`);
|
||||
assert.match(identity.sha256, /^sha256:[a-f0-9]{64}$/);
|
||||
return identity;
|
||||
}
|
||||
|
||||
export async function smokeKimiProviderPack(directory) {
|
||||
const pack = await realpath(resolve(directory));
|
||||
const manifest = JSON.parse(await readFile(join(pack, "provider-pack.json"), "utf8"));
|
||||
assert.equal(manifest.schema, "paperclip-runner/remote-provider-pack/v1");
|
||||
assert.equal(manifest.payload.target.platform, process.platform);
|
||||
assert.equal(manifest.payload.target.architecture, process.arch);
|
||||
if (process.env.PAPERCLIP_EXPECTED_SOURCE_REVISION) assert.equal(manifest.payload.runnerSourceRevision, process.env.PAPERCLIP_EXPECTED_SOURCE_REVISION);
|
||||
const load = path => import(pathToFileURL(join(pack, "dist/drivers/acpx", path)));
|
||||
const [{ verifyKimiInstallation }, { QUALIFIED_ACPX_PROFILES }, { kimiManagedConfiguration }, { kimiInstructionBinding }, { KIMI_DISTRIBUTION_CLOSURE_SHA256 }] = await Promise.all([
|
||||
load("kimi-installation.js"), load("qualified-profiles.js"), load("kimi-config.js"), load("kimi-instructions.js"), load("kimi-closure-pins.js"),
|
||||
]);
|
||||
const provider = assertKimiProviderPackInventory(manifest, QUALIFIED_ACPX_PROFILES.kimi, KIMI_DISTRIBUTION_CLOSURE_SHA256);
|
||||
const keys = ["PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"];
|
||||
const previous = keys.map(key => process.env[key]);
|
||||
process.env[keys[0]] = pack; process.env[keys[1]] = join(pack, "package.json");
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-pack-smoke-")));
|
||||
const home = join(root, "home"), workspace = join(root, "workspace");
|
||||
await mkdir(home); await mkdir(workspace);
|
||||
const model = "kimi-for-coding";
|
||||
const binding = kimiInstructionBinding("Do not call a model during this installed-package smoke. Exact suffix: café.\n");
|
||||
await writeFile(join(home, "config.toml"), kimiManagedConfiguration(model), { mode: 0o600 });
|
||||
const active = new Set(); let sequence = 0;
|
||||
try {
|
||||
const installation = await verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi);
|
||||
const connect = async () => {
|
||||
const lease = await installation.openCommand();
|
||||
const child = lease.spawn([], { cwd: workspace, stdio: "pipe", env: {
|
||||
PATH: "/usr/bin:/bin", LANG: "C.UTF-8", KIMI_CODE_HOME: home,
|
||||
KIMI_CODE_NO_AUTO_UPDATE: "1", KIMI_API_KEY: "paperclip-offline-placeholder",
|
||||
PAPERCLIP_KIMI_INSTRUCTIONS: binding.payload, CI: "1", NO_COLOR: "1", TERM: "dumb",
|
||||
} });
|
||||
const closed = once(child, "close"), pending = new Map();
|
||||
let buffer = "", bytes = 0;
|
||||
const fail = error => { for (const request of pending.values()) request.reject(error); pending.clear(); };
|
||||
child.on("error", fail); child.on("close", () => fail(new Error("Kimi exited with a pending ACP request")));
|
||||
child.stderr.resume();
|
||||
child.stdout.on("data", chunk => {
|
||||
bytes += chunk.length;
|
||||
if (bytes > 1024 * 1024) { fail(new Error("Kimi ACP output exceeded its package smoke bound")); return; }
|
||||
buffer += chunk.toString("utf8");
|
||||
let newline;
|
||||
while ((newline = buffer.indexOf("\n")) >= 0) {
|
||||
const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
|
||||
if (!line.trim()) continue;
|
||||
let value;
|
||||
try { value = JSON.parse(line); } catch { fail(new Error("Kimi emitted malformed ACP JSON")); return; }
|
||||
const request = pending.get(value.id);
|
||||
if (request && value.method === undefined) {
|
||||
pending.delete(value.id);
|
||||
if (value.error) request.reject(new Error(`Kimi rejected ACP request (${value.error.code})`));
|
||||
else request.resolve(value.result);
|
||||
}
|
||||
}
|
||||
});
|
||||
const request = (method, params) => new Promise((accept, reject) => {
|
||||
const id = sequence++;
|
||||
const timeout = setTimeout(() => { pending.delete(id); reject(new Error(`Kimi ${method} timed out`)); }, 15_000);
|
||||
const settle = callback => value => { clearTimeout(timeout); callback(value); };
|
||||
pending.set(id, { resolve: settle(accept), reject: settle(reject) });
|
||||
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
||||
});
|
||||
let closing;
|
||||
const close = () => closing ??= (async () => {
|
||||
const started = Date.now(); child.stdin.end();
|
||||
const term = setTimeout(() => child.kill("SIGTERM"), 4_000), kill = setTimeout(() => child.kill("SIGKILL"), 5_000);
|
||||
try {
|
||||
const [code, signal] = await closed;
|
||||
assert.equal(code, 0, "Kimi did not exit cleanly after EOF"); assert.equal(signal, null);
|
||||
return Date.now() - started;
|
||||
} finally { clearTimeout(term); clearTimeout(kill); await lease.close(); active.delete(close); }
|
||||
})();
|
||||
active.add(close);
|
||||
const initialized = await request("initialize", { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: "paperclip-provider-pack-smoke", version: "1" } });
|
||||
assert.equal(initialized.agentInfo?.version, "2.3.3");
|
||||
return { request, close };
|
||||
};
|
||||
const assertBinding = (result, sessionId) => {
|
||||
const ack = result._meta?.paperclipKimiInstructions;
|
||||
assert.equal(ack?.sessionId, sessionId); assert.equal(ack.digest, binding.digest); assert.equal(ack.byteLength, binding.byteLength);
|
||||
assert.match(ack.effectiveDigest, /^sha256:[a-f0-9]{64}$/);
|
||||
};
|
||||
const first = await connect();
|
||||
const created = await first.request("session/new", { cwd: workspace, mcpServers: [] });
|
||||
assertBinding(created, created.sessionId);
|
||||
const selected = await first.request("session/set_config_option", { sessionId: created.sessionId, configId: "model", value: model });
|
||||
assert.equal(selected.configOptions.find(option => option.id === "model").currentValue, model);
|
||||
const createExitMs = await first.close();
|
||||
const second = await connect();
|
||||
const loaded = await second.request("session/load", { sessionId: created.sessionId, cwd: workspace, mcpServers: [] });
|
||||
assertBinding(loaded, created.sessionId);
|
||||
assert.equal(loaded.configOptions.find(option => option.id === "model").currentValue, model);
|
||||
const loadExitMs = await second.close();
|
||||
return { schema: "paperclip.kimi-provider-pack-smoke.v1", sourceRevision: manifest.payload.runnerSourceRevision, providerPackDigest: manifest.digest,
|
||||
platform: process.platform, architecture: process.arch, runtimeVersion: "2.3.3", profileDigest: provider.profileDigest, closureDigest: provider.closureDigest,
|
||||
createAcknowledged: true, loadAcknowledged: true, model, createExitMs, loadExitMs, providerPrompts: 0, providerCredentials: false,
|
||||
qualification: "installed runtime only; no model entitlement or task completion" };
|
||||
} finally {
|
||||
try { await Promise.all([...active].map(close => close())); }
|
||||
finally { keys.forEach((key, index) => { if (previous[index] === undefined) delete process.env[key]; else process.env[key] = previous[index]; }); await rm(root, { recursive: true, force: true }); }
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
||||
if (process.argv.length !== 3) throw new Error("Usage: node scripts/kimi-provider-pack-smoke.mjs /absolute/installed-provider-pack");
|
||||
smokeKimiProviderPack(process.argv[2]).then(result => console.log(JSON.stringify(result))).catch(error => { console.error(error.message); process.exitCode = 1; });
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { assertKimiProviderPackInventory } from "./kimi-provider-pack-smoke.mjs";
|
||||
import profiles from "../acpx-profiles.json" with { type: "json" };
|
||||
import historical from "../test/fixtures/kimi-profile-v14-identity.json" with { type: "json" };
|
||||
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 as closures } from "../src/drivers/acpx/kimi-closure-pins.ts";
|
||||
|
||||
function fixture(profile, target) {
|
||||
const [platform, architecture] = target.split("-");
|
||||
const pending = profile.qualificationStatus === "pending";
|
||||
return { schema: "paperclip-runner/remote-provider-pack/v1", payload: { target: { platform, architecture },
|
||||
[pending ? "candidateProviders" : "providers"]: { kimi: { version: profile.agentServerVersion,
|
||||
qualification: pending ? "pending" : "qualified", profileDigest: profile.commandDigest,
|
||||
closureDigest: `sha256:${closures[target]}`, path: `provider-assets/kimi/${target}`, sha256: `sha256:${"a".repeat(64)}` } } } };
|
||||
}
|
||||
for (const target of ["darwin-arm64", "linux-x64"]) {
|
||||
test(`ordinary ${target} smoke needs released current17 and no candidate entry`, () => {
|
||||
const profile = profiles.profiles.kimi;
|
||||
assert.equal(profile.agentProfileVersion, 17);
|
||||
assert.notEqual(profile.qualificationStatus, "pending");
|
||||
const manifest = fixture(profile, target), [platform, architecture] = target.split("-");
|
||||
assert.equal(assertKimiProviderPackInventory(manifest, profile, closures, platform, architecture), manifest.payload.providers.kimi);
|
||||
for (const change of [{ profileDigest: historical.commandDigest }, { closureDigest: "sha256:" + "b".repeat(64) },
|
||||
{ qualification: "pending" }, { path: "provider-assets/kimi/foreign" }]) {
|
||||
const invalid = structuredClone(manifest); Object.assign(invalid.payload.providers.kimi, change);
|
||||
assert.throws(() => assertKimiProviderPackInventory(invalid, profile, closures, platform, architecture));
|
||||
}
|
||||
const duplicate = structuredClone(manifest); duplicate.payload.candidateProviders = { kimi: duplicate.payload.providers.kimi };
|
||||
assert.throws(() => assertKimiProviderPackInventory(duplicate, profile, closures, platform, architecture));
|
||||
assert.throws(() => assertKimiProviderPackInventory(manifest, profile, closures, "linux", "arm64"));
|
||||
});
|
||||
test(`historical pending14 ${target} remains a distinct inventory contract`, () => {
|
||||
const profile = { ...historical.declaration, commandDigest: historical.commandDigest };
|
||||
const manifest = fixture(profile, target), [platform, architecture] = target.split("-");
|
||||
assert.equal(profile.qualificationStatus, "pending");
|
||||
assert.equal(assertKimiProviderPackInventory(manifest, profile, closures, platform, architecture), manifest.payload.candidateProviders.kimi);
|
||||
assert.throws(() => assertKimiProviderPackInventory(manifest, profiles.profiles.kimi, closures, platform, architecture));
|
||||
});
|
||||
}
|
||||
|
||||
test("the actual builder verifies ordinary Kimi before replacing the retained pack", async () => {
|
||||
const source = readFileSync(new URL("./build-provider-pack.mjs", import.meta.url), "utf8");
|
||||
const begin = source.indexOf(' if (payload.providers?.kimi && profiles.profiles.kimi.qualificationStatus !== "pending") {');
|
||||
const end = source.indexOf('\n} finally {', begin);
|
||||
assert.ok(begin > 0 && end > begin);
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
const run = new AsyncFunction("payload", "profiles", "smokeKimiProviderPack", "temporaryRoot", "outputRoot", "process", "rmSync", "renameSync", source.slice(begin, end));
|
||||
for (const failed of [false, true]) {
|
||||
const order = [], smoke = async root => { assert.equal(root, "/owned/temporary"); order.push("smoke"); if (failed) throw Error("smoke failed"); return { providerPrompts: 0 }; };
|
||||
const work = run({ providers: { kimi: {} } }, profiles, smoke, "/owned/temporary", "/owned/output",
|
||||
{ stdout: { write() {} } }, () => order.push("remove-old"), () => order.push("publish"));
|
||||
if (failed) { await assert.rejects(work, /smoke failed/); assert.deepEqual(order, ["smoke"]); }
|
||||
else { await work; assert.deepEqual(order, ["smoke", "remove-old", "publish"]); }
|
||||
}
|
||||
for (const [payload, status] of [[{ candidateProviders: { kimi: {} } }, "pending"], [{ providers: {} }, undefined]]) {
|
||||
let calls = 0;
|
||||
await run(payload, { profiles: { kimi: { qualificationStatus: status } } }, () => { calls++; }, "/owned/temporary", "/owned/output",
|
||||
{ stdout: { write() {} } }, () => {}, () => {});
|
||||
assert.equal(calls, 0);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { KIMI_DISTRIBUTION_CLOSURE_SHA256, KIMI_DISTRIBUTION_LOCK_SHA256, KIMI_NODE_DISTRIBUTIONS, KIMI_NODE_VERSION, KIMI_RUNTIME_VERSION } from "../src/drivers/acpx/kimi-closure-pins.ts";
|
||||
import { inventoryKimiRuntimeFiles, kimiRuntimeClosureDigest } from "../src/drivers/acpx/kimi-runtime-files.ts";
|
||||
import { patchKimiRuntime } from "./patch-kimi-runtime.mjs";
|
||||
|
||||
const run = promisify(execFile);
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repositoryRoot = resolve(packageRoot, "../..");
|
||||
const hash = bytes => createHash("sha256").update(bytes).digest("hex");
|
||||
export const KIMI_DISTRIBUTION_ENTRYPOINT = "node_modules/@moonshot-ai/kimi-code/dist/main.mjs";
|
||||
|
||||
export async function runKimiBuildCommand(command, args, options, checkCancelled = () => {}, signal) {
|
||||
checkCancelled(); signal?.throwIfAborted();
|
||||
const pending = run(command, args, { ...options, signal, killSignal: "SIGKILL" });
|
||||
const closed = new Promise(resolve => pending.child.once("close", resolve));
|
||||
try {
|
||||
const result = await pending;
|
||||
checkCancelled(); signal?.throwIfAborted();
|
||||
return result;
|
||||
} finally { await closed; }
|
||||
}
|
||||
|
||||
export function kimiDistributionInstallCommand() {
|
||||
return ["ci", "--ignore-scripts", "--omit=optional", "--omit=dev", "--no-audit", "--no-fund", "--registry=https://registry.npmjs.org", "--userconfig=.npmrc", "--globalconfig=.npmrc-global"];
|
||||
}
|
||||
|
||||
export function kimiDistributionEnvironment(source = process.env) {
|
||||
return Object.fromEntries(["PATH", "LANG", "LC_ALL", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "https_proxy", "http_proxy", "no_proxy", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"].flatMap(key => typeof source[key] === "string" ? [[key, source[key]]] : []));
|
||||
}
|
||||
|
||||
export async function verifyLockedKimiPackageGraph(root, lock) {
|
||||
if (lock.lockfileVersion !== 3 || !lock.packages || lock.packages[""]?.dependencies?.["@moonshot-ai/kimi-code"] !== KIMI_RUNTIME_VERSION) throw new Error("Kimi distribution lock is invalid");
|
||||
let packages = 0;
|
||||
for (const [path, entry] of Object.entries(lock.packages)) {
|
||||
if (!path) continue;
|
||||
if (!path.startsWith("node_modules/") || /[\\\u0000-\u001f\u007f]/.test(path) || path.split("/").some(part => !part || part === "." || part === "..") || entry.link || typeof entry.version !== "string" || !/^https:\/\/registry\.npmjs\.org\//.test(entry.resolved ?? "") || !/^sha512-[A-Za-z0-9+/]+=*$/.test(entry.integrity ?? "")) throw new Error("Kimi distribution lock contains an unpinned package");
|
||||
if (entry.optional === true) {
|
||||
try { await lstat(join(root, path)); } catch (error) { if (error.code === "ENOENT") continue; throw error; }
|
||||
throw new Error("Kimi headless distribution unexpectedly installed an optional TUI dependency");
|
||||
}
|
||||
const file = join(root, path, "package.json");
|
||||
if (await realpath(file) !== file || !(await lstat(file)).isFile()) throw new Error("Kimi distribution package metadata is not a regular file");
|
||||
const actual = JSON.parse(await readFile(file, "utf8"));
|
||||
if (actual.version !== entry.version) throw new Error(`Kimi distribution package differs from its lock: ${path}`);
|
||||
packages++;
|
||||
}
|
||||
return packages;
|
||||
}
|
||||
|
||||
export function assertKimiNodeSystemDependencies(listing, platform) {
|
||||
for (const line of listing.split("\n").map(value => value.trim()).filter(Boolean)) {
|
||||
if (platform === "darwin") {
|
||||
if (line.endsWith(":")) continue;
|
||||
if (!line.startsWith("/usr/lib/") && !line.startsWith("/System/Library/")) throw new Error("Kimi Node depends on an unbundled library");
|
||||
} else if (!line.startsWith("linux-vdso.so.") && !/^(?:[^\s]+ => )?\/(?:usr\/)?lib(?:64)?\//.test(line)) throw new Error("Kimi Node depends on an unbundled library");
|
||||
}
|
||||
}
|
||||
|
||||
async function nodeDistribution(staging, target, archivePath, environment, runOwned, signal) {
|
||||
const pin = KIMI_NODE_DISTRIBUTIONS[target];
|
||||
if (!pin) throw new Error(`Kimi distribution target is unsupported: ${target}`);
|
||||
let bytes;
|
||||
if (archivePath) bytes = await readFile(archivePath);
|
||||
else {
|
||||
const timeout = AbortSignal.timeout(60_000);
|
||||
const response = await fetch(`https://nodejs.org/dist/v${KIMI_NODE_VERSION}/node-v${KIMI_NODE_VERSION}-${target}.tar.gz`, { redirect: "error", signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
|
||||
if (!response.ok || !response.body) throw new Error("Pinned Kimi Node download failed");
|
||||
const chunks = []; let length = 0;
|
||||
for await (const chunk of response.body) {
|
||||
length += chunk.length;
|
||||
if (length > 128 * 1024 * 1024) throw new Error("Kimi Node archive exceeds its bound");
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
bytes = Buffer.concat(chunks);
|
||||
}
|
||||
if (bytes.length > 128 * 1024 * 1024 || hash(bytes) !== pin.archiveSha256) throw new Error("Kimi Node archive does not match its release pin");
|
||||
const archive = join(staging, `${target}.tar.gz`); await writeFile(archive, bytes);
|
||||
const output = join(staging, `node-${target}`); await mkdir(output);
|
||||
await runOwned("tar", ["-xzf", archive, "-C", output, "--strip-components=1", `node-v${KIMI_NODE_VERSION}-${target}/bin/node`, `node-v${KIMI_NODE_VERSION}-${target}/lib/node_modules/npm`], { env: environment, timeout: 30_000 });
|
||||
const executable = join(output, "bin/node");
|
||||
if (hash(await readFile(executable)) !== pin.executableSha256 || (await lstat(executable)).size !== pin.executableSize) throw new Error("Kimi Node executable does not match its target pin");
|
||||
const npm = join(output, "lib/node_modules/npm/bin/npm-cli.js");
|
||||
if (JSON.parse(await readFile(join(output, "lib/node_modules/npm/package.json"), "utf8")).version !== "11.19.0") throw new Error("Kimi Node archive has an unexpected npm version");
|
||||
return { executable, npm };
|
||||
}
|
||||
|
||||
async function removePackageManagerLinks(root) {
|
||||
const visit = async directory => {
|
||||
for (const name of await readdir(directory)) {
|
||||
const path = join(directory, name), stat = await lstat(path);
|
||||
if (stat.isSymbolicLink()) {
|
||||
if (dirname(path).endsWith("/node_modules/.bin")) await unlink(path);
|
||||
else throw new Error("Kimi package payload contains an unsupported symbolic link");
|
||||
} else if (stat.isDirectory()) await visit(path);
|
||||
else if (!stat.isFile()) throw new Error("Kimi package payload contains a non-file resource");
|
||||
}
|
||||
};
|
||||
await visit(root);
|
||||
await rm(join(root, "node_modules/.package-lock.json"), { force: true });
|
||||
}
|
||||
|
||||
export async function buildKimiDistribution({ outputRoot, target = `${process.platform}-${process.arch}`, lockDirectory = join(packageRoot, "scripts/kimi-distribution"), nodeArchives = {}, checkCancelled = () => {}, signal }) {
|
||||
checkCancelled(); signal?.throwIfAborted();
|
||||
if (typeof outputRoot !== "string" || !isAbsolute(outputRoot)) throw new Error("Kimi distribution output must be absolute");
|
||||
const output = resolve(outputRoot);
|
||||
if (["/", packageRoot, repositoryRoot].includes(output)) throw new Error("Refusing unsafe Kimi distribution output");
|
||||
if (!Object.hasOwn(KIMI_NODE_DISTRIBUTIONS, target)) throw new Error("Kimi distribution target is unsupported");
|
||||
const host = `${process.platform}-${process.arch}`;
|
||||
if (!Object.hasOwn(KIMI_NODE_DISTRIBUTIONS, host)) throw new Error("Kimi distribution build host is unsupported");
|
||||
try { await lstat(output); throw new Error("Kimi distribution output already exists"); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
||||
const lockBytes = await readFile(join(lockDirectory, "package-lock.json"));
|
||||
if (hash(lockBytes) !== KIMI_DISTRIBUTION_LOCK_SHA256) throw new Error("Kimi distribution lock differs from its source pin");
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
const staging = await mkdtemp(join(await realpath(dirname(output)), ".paperclip-kimi-distribution-"));
|
||||
const environment = kimiDistributionEnvironment();
|
||||
const runOwned = (command, args, options) => runKimiBuildCommand(command, args, options, checkCancelled, signal);
|
||||
const runtime = join(staging, "runtime");
|
||||
try {
|
||||
const buildHome = join(staging, "build-home"); await mkdir(buildHome); environment.HOME = buildHome;
|
||||
const installedNode = await nodeDistribution(staging, target, nodeArchives[target], environment, runOwned, signal);
|
||||
const buildNode = host === target ? installedNode : await nodeDistribution(staging, host, nodeArchives[host], environment, runOwned, signal);
|
||||
if ((await runOwned(buildNode.executable, ["--version"], { env: {}, timeout: 10_000 })).stdout.trim() !== `v${KIMI_NODE_VERSION}`) throw new Error("Kimi distribution build Node cannot execute");
|
||||
await mkdir(runtime);
|
||||
for (const name of ["package.json", "package-lock.json"]) await copyFile(join(lockDirectory, name), join(runtime, name));
|
||||
await writeFile(join(runtime, ".npmrc"), "registry=https://registry.npmjs.org/\nignore-scripts=true\naudit=false\nfund=false\n");
|
||||
await writeFile(join(runtime, ".npmrc-global"), "");
|
||||
await runOwned(buildNode.executable, [buildNode.npm, ...kimiDistributionInstallCommand()], { cwd: runtime, env: environment, timeout: 300_000, maxBuffer: 4 * 1024 * 1024 });
|
||||
if (!(await readFile(join(runtime, "package-lock.json"))).equals(lockBytes)) throw new Error("Kimi installation changed its committed lock");
|
||||
const packageCount = await verifyLockedKimiPackageGraph(runtime, JSON.parse(lockBytes.toString("utf8")));
|
||||
const patch = await patchKimiRuntime(join(runtime, KIMI_DISTRIBUTION_ENTRYPOINT));
|
||||
await mkdir(join(runtime, "node/bin"), { recursive: true });
|
||||
await copyFile(installedNode.executable, join(runtime, "node/bin/node")); await chmod(join(runtime, "node/bin/node"), 0o755);
|
||||
if (host === target) {
|
||||
const listing = process.platform === "darwin"
|
||||
? await runOwned("/usr/bin/otool", ["-L", join(runtime, "node/bin/node")], { env: {}, timeout: 10_000 })
|
||||
: await runOwned("ldd", [join(runtime, "node/bin/node")], { env: { PATH: "/usr/bin:/bin" }, timeout: 10_000 });
|
||||
assertKimiNodeSystemDependencies(listing.stdout, process.platform);
|
||||
if ((await runOwned(join(runtime, "node/bin/node"), ["--version"], { env: {}, timeout: 10_000 })).stdout.trim() !== `v${KIMI_NODE_VERSION}`) throw new Error("Kimi Node cannot execute after relocation");
|
||||
}
|
||||
await removePackageManagerLinks(runtime);
|
||||
const entries = await inventoryKimiRuntimeFiles(runtime);
|
||||
const closure = kimiRuntimeClosureDigest(entries);
|
||||
const metadata = { schema: "paperclip.kimi-distribution.v1", version: KIMI_RUNTIME_VERSION, target, nodeVersion: KIMI_NODE_VERSION, runtimeRoot: "runtime", entrypoint: KIMI_DISTRIBUTION_ENTRYPOINT, lockSha256: KIMI_DISTRIBUTION_LOCK_SHA256, patch, packageCount, nativeClosureSha256: closure };
|
||||
await writeFile(join(staging, "native-closure.json"), `${JSON.stringify({ entries })}\n`);
|
||||
await writeFile(join(staging, "kimi-distribution.json"), `${JSON.stringify(metadata, null, 2)}\n`);
|
||||
for (const name of await readdir(staging)) if (!["runtime", "native-closure.json", "kimi-distribution.json"].includes(name)) await rm(join(staging, name), { recursive: true, force: true });
|
||||
checkCancelled();
|
||||
await rename(staging, output);
|
||||
return { outputRoot: output, runtimeRoot: join(output, "runtime"), version: KIMI_RUNTIME_VERSION, closureDigest: `sha256:${closure}`, metadata };
|
||||
} catch (error) { await rm(staging, { recursive: true, force: true }); throw error; }
|
||||
}
|
||||
|
||||
export async function materializeKimiDistribution(options) {
|
||||
if (typeof options.outputRoot !== "string" || !isAbsolute(options.outputRoot)) throw new Error("Kimi distribution output must be absolute");
|
||||
const output = resolve(options.outputRoot);
|
||||
if (["/", packageRoot, repositoryRoot].includes(output)) throw new Error("Refusing unsafe Kimi distribution output");
|
||||
const target = options.target ?? `${process.platform}-${process.arch}`;
|
||||
if (!Object.hasOwn(KIMI_DISTRIBUTION_CLOSURE_SHA256, target)) throw new Error("Kimi distribution target is unsupported");
|
||||
try { await lstat(output); throw new Error("Kimi distribution output already exists"); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
const parent = await realpath(dirname(output));
|
||||
const temporary = await mkdtemp(join(parent, ".paperclip-kimi-admission-"));
|
||||
const candidate = join(temporary, "candidate");
|
||||
try {
|
||||
const result = await buildKimiDistribution({ ...options, outputRoot: candidate });
|
||||
if (result.metadata.nativeClosureSha256 !== KIMI_DISTRIBUTION_CLOSURE_SHA256[target]) throw new Error("Kimi native closure differs from its trusted target pin");
|
||||
try { await lstat(options.outputRoot); throw new Error("Kimi distribution output already exists"); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
||||
await rename(candidate, options.outputRoot);
|
||||
return { ...result, outputRoot: options.outputRoot, runtimeRoot: join(options.outputRoot, "runtime") };
|
||||
} finally { await rm(temporary, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
if (import.meta.url.endsWith("/materialize-kimi-distribution.mjs") && process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length !== 1) throw new Error("Usage: node scripts/materialize-kimi-distribution.mjs /absolute/output-directory");
|
||||
materializeKimiDistribution({ outputRoot: args[0] }).then(result => console.log(JSON.stringify(result))).catch(error => { console.error(error.message); process.exitCode = 1; });
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmod, link, mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { assertKimiNodeSystemDependencies, buildKimiDistribution, kimiDistributionEnvironment, kimiDistributionInstallCommand, materializeKimiDistribution, runKimiBuildCommand, verifyLockedKimiPackageGraph } from "./materialize-kimi-distribution.mjs";
|
||||
import { KIMI_DISTRIBUTION_LOCK_SHA256, KIMI_NODE_DISTRIBUTIONS } from "../src/drivers/acpx/kimi-closure-pins.ts";
|
||||
import { inventoryKimiRuntimeFiles, kimiRuntimeClosureDigest } from "../src/drivers/acpx/kimi-runtime-files.ts";
|
||||
|
||||
async function fixture(t) {
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-distribution-test-")));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const write = async (path, content) => { const file = join(root, path); await mkdir(dirname(file), { recursive: true }); await writeFile(file, content); };
|
||||
return { root, write };
|
||||
}
|
||||
|
||||
test("Kimi's portable graph pins public integrity and excludes lifecycle and optional TUI dependencies", async () => {
|
||||
const bytes = await readFile(new URL("./kimi-distribution/package-lock.json", import.meta.url));
|
||||
assert.equal(createHash("sha256").update(bytes).digest("hex"), KIMI_DISTRIBUTION_LOCK_SHA256);
|
||||
const lock = JSON.parse(bytes.toString("utf8"));
|
||||
const pkg = JSON.parse(await readFile(new URL("./kimi-distribution/package.json", import.meta.url), "utf8"));
|
||||
assert.deepEqual(pkg.dependencies, { "@moonshot-ai/kimi-code": "2.3.3" });
|
||||
assert.deepEqual(lock.packages[""].dependencies, pkg.dependencies);
|
||||
assert.equal(lock.packages["node_modules/@moonshot-ai/kimi-code"].integrity, "sha512-3V8bycucf9KgPRH27S8De2u0ip84/Ry3FpNn6gi5z9JmoEHt9vxiYHHZqiMdyDf5nPm4bv+YKGwKEQL4oYpRTw==");
|
||||
assert.equal(kimiDistributionInstallCommand()[0], "ci");
|
||||
assert.ok(kimiDistributionInstallCommand().includes("--ignore-scripts"));
|
||||
assert.ok(kimiDistributionInstallCommand().includes("--omit=optional"));
|
||||
assert.ok(kimiDistributionInstallCommand().includes("--registry=https://registry.npmjs.org"));
|
||||
assert.deepEqual(Object.keys(KIMI_NODE_DISTRIBUTIONS).sort(), ["darwin-arm64", "linux-x64"]);
|
||||
});
|
||||
|
||||
test("distribution build admits network settings without provider keys or Node injection", () => {
|
||||
const network = { PATH: "/usr/bin:/bin", LANG: "C.UTF-8", HTTPS_PROXY: "http://proxy.invalid", NODE_EXTRA_CA_CERTS: "/public/ca.pem" };
|
||||
assert.deepEqual(kimiDistributionEnvironment({ ...network, HOME: "/private/home", KIMI_MODEL_API_KEY: "canary-secret", NPM_TOKEN: "canary-secret", NODE_OPTIONS: "--require /foreign.js", NODE_PATH: "/foreign", NODE_TLS_REJECT_UNAUTHORIZED: "0" }), network);
|
||||
});
|
||||
|
||||
test("setup cancellation physically stops an in-flight owned child", { timeout: 5_000 }, async t => {
|
||||
const { root } = await fixture(t), pidFile = join(root, "pid");
|
||||
const controller = new AbortController();
|
||||
const result = runKimiBuildCommand(process.execPath, ["-e", "require('node:fs').writeFileSync(process.argv[1],String(process.pid));setInterval(()=>{},1000)", pidFile], { env: {}, timeout: 3_000 }, undefined, controller.signal).then(() => null, error => error);
|
||||
let pid;
|
||||
try {
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
try { pid = Number(await readFile(pidFile, "utf8")); break; }
|
||||
catch (error) { if (error.code !== "ENOENT") throw error; await delay(10); }
|
||||
}
|
||||
assert.ok(Number.isSafeInteger(pid) && pid > 0, "The real child did not start");
|
||||
controller.abort();
|
||||
assert.equal((await result).code, "ABORT_ERR");
|
||||
assert.throws(() => process.kill(pid, 0), error => error.code === "ESRCH");
|
||||
} finally { controller.abort(); await result; }
|
||||
});
|
||||
|
||||
test("setup cancellation aborts the pending download and removes its staging tree", { timeout: 5_000 }, async t => {
|
||||
const { root } = await fixture(t), controller = new AbortController();
|
||||
let started;
|
||||
const downloading = new Promise(resolve => { started = resolve; });
|
||||
t.mock.method(globalThis, "fetch", (_url, options) => new Promise((_resolve, reject) => {
|
||||
started(); options.signal.addEventListener("abort", () => reject(options.signal.reason), { once: true });
|
||||
}));
|
||||
const result = buildKimiDistribution({ outputRoot: join(root, "output"), signal: controller.signal });
|
||||
await downloading; controller.abort();
|
||||
await assert.rejects(result, error => error.name === "AbortError");
|
||||
assert.deepEqual(await readdir(root), []);
|
||||
});
|
||||
|
||||
test("the installed graph rejects version drift and optional packages", async t => {
|
||||
const { root, write } = await fixture(t);
|
||||
const entry = { version: "2.3.3", resolved: "https://registry.npmjs.org/@moonshot-ai/kimi-code/-/kimi-code-2.3.3.tgz", integrity: "sha512-YWJjZA==" };
|
||||
const lock = { lockfileVersion: 3, packages: { "": { dependencies: { "@moonshot-ai/kimi-code": "2.3.3" } }, "node_modules/@moonshot-ai/kimi-code": entry, "node_modules/optional-native": { ...entry, optional: true } } };
|
||||
await write("node_modules/@moonshot-ai/kimi-code/package.json", '{"version":"2.3.3"}');
|
||||
assert.equal(await verifyLockedKimiPackageGraph(root, lock), 1);
|
||||
await write("node_modules/@moonshot-ai/kimi-code/package.json", '{"version":"2.3.4"}');
|
||||
await assert.rejects(verifyLockedKimiPackageGraph(root, lock), /differs from its lock/);
|
||||
await write("node_modules/@moonshot-ai/kimi-code/package.json", '{"version":"2.3.3"}');
|
||||
await write("node_modules/optional-native/package.json", '{"version":"2.3.3"}');
|
||||
await assert.rejects(verifyLockedKimiPackageGraph(root, lock), /optional TUI dependency/);
|
||||
});
|
||||
|
||||
test("inventory binds native assets, workers, permissions and bytes in stable path order", async t => {
|
||||
const { root, write } = await fixture(t);
|
||||
await write("node_modules/provider/worker.mjs", "worker");
|
||||
await write("node_modules/provider/native/addon.node", Buffer.from([0, 1, 2, 3]));
|
||||
await write("node/bin/node", "node");
|
||||
const first = await inventoryKimiRuntimeFiles(root);
|
||||
assert.deepEqual(first.map(entry => entry.path), ["node/bin/node", "node_modules/provider/native/addon.node", "node_modules/provider/worker.mjs"]);
|
||||
const digest = kimiRuntimeClosureDigest(first);
|
||||
await write("node_modules/provider/worker.mjs", "worker changed");
|
||||
assert.notEqual(kimiRuntimeClosureDigest(await inventoryKimiRuntimeFiles(root)), digest);
|
||||
await chmod(join(root, "node/bin/node"), 0o755);
|
||||
assert.equal((await inventoryKimiRuntimeFiles(root))[0].executable, true);
|
||||
});
|
||||
|
||||
test("inventory rejects external links, hardlinks and special files", async t => {
|
||||
const { root, write } = await fixture(t);
|
||||
await write("original", "content");
|
||||
await symlink("/outside", join(root, "escape"));
|
||||
await assert.rejects(inventoryKimiRuntimeFiles(root), /symbolic link/);
|
||||
await rm(join(root, "escape"));
|
||||
await link(join(root, "original"), join(root, "alias"));
|
||||
await assert.rejects(inventoryKimiRuntimeFiles(root), /invalid file/);
|
||||
});
|
||||
|
||||
test("setup refuses relative, root, unsupported and existing outputs before network work", async t => {
|
||||
const { root } = await fixture(t);
|
||||
for (const operation of [buildKimiDistribution, materializeKimiDistribution]) {
|
||||
await assert.rejects(operation({ outputRoot: "relative" }), /must be absolute/);
|
||||
await assert.rejects(operation({ outputRoot: "/" }), /unsafe/);
|
||||
await assert.rejects(operation({ outputRoot: join(root, "output"), target: "linux-arm64" }), /unsupported/);
|
||||
await assert.rejects(operation({ outputRoot: root }), /already exists/);
|
||||
}
|
||||
});
|
||||
|
||||
test("the bundled Node may depend only on platform system libraries", () => {
|
||||
assert.doesNotThrow(() => assertKimiNodeSystemDependencies("node:\n /usr/lib/libSystem.B.dylib (version 0)\n", "darwin"));
|
||||
assert.throws(() => assertKimiNodeSystemDependencies("node:\n /opt/homebrew/lib/libnode.dylib (version 0)\n", "darwin"), /unbundled/);
|
||||
assert.doesNotThrow(() => assertKimiNodeSystemDependencies("linux-vdso.so.1 (0x0)\nlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0)\n/lib64/ld-linux-x86-64.so.2 (0x0)\n", "linux"));
|
||||
assert.throws(() => assertKimiNodeSystemDependencies("libcustom.so => /opt/private/libcustom.so (0x0)\n", "linux"), /unbundled/);
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { KIMI_INSTRUCTION_RUNTIME_SOURCE } from "./kimi-instructions-runtime.mjs";
|
||||
|
||||
export const KIMI_RUNTIME_PATCH = Object.freeze({
|
||||
version: "paperclip-kimi-2.3.3-instructions-v6",
|
||||
vendorSha256: "f18b711e22947d473175126d0c2da437d641187963a3bc300d66e183060703e3",
|
||||
patchedSha256: "79980aa95c54f325a05cbdf883f74443705db28f3e412a1b5ae22929d939eb66",
|
||||
});
|
||||
|
||||
const sha256 = value => createHash("sha256").update(value).digest("hex");
|
||||
|
||||
export function replaceKimiPatchAnchor(source, before, after) {
|
||||
if (source.split(before).length !== 2) throw new Error("Kimi runtime patch requires exactly one source anchor");
|
||||
return source.replace(before, () => after);
|
||||
}
|
||||
|
||||
export function patchKimiRuntimeSource(input) {
|
||||
if (sha256(input) !== KIMI_RUNTIME_PATCH.vendorSha256) throw new Error("Kimi runtime patch input digest mismatch");
|
||||
let source = input;
|
||||
const replace = (before, after) => { source = replaceKimiPatchAnchor(source, before, after); };
|
||||
replace("#!/usr/bin/env node\n", `#!/usr/bin/env node
|
||||
import { createHash as paperclipKimiCreateHash } from "node:crypto";
|
||||
let paperclipKimiInstructionSnapshot;
|
||||
function paperclipKimiInstructionState() {
|
||||
if (paperclipKimiInstructionSnapshot) return paperclipKimiInstructionSnapshot;
|
||||
const raw = process.env.PAPERCLIP_KIMI_INSTRUCTIONS;
|
||||
delete process.env.PAPERCLIP_KIMI_INSTRUCTIONS;
|
||||
return paperclipKimiInstructionSnapshot = (${KIMI_INSTRUCTION_RUNTIME_SOURCE})(raw, {
|
||||
createHash: paperclipKimiCreateHash, byteLength: value => Buffer.byteLength(value, "utf8")
|
||||
});
|
||||
}
|
||||
function paperclipKimiChildEnvironment(overrides) {
|
||||
const environment = { ...process.env, ...overrides };
|
||||
delete environment.KIMI_API_KEY;
|
||||
delete environment.PAPERCLIP_KIMI_INSTRUCTIONS;
|
||||
return environment;
|
||||
}
|
||||
`);
|
||||
replace('function buildEnv(overrides) {\n\tif (overrides === void 0) return;\n\treturn {\n\t\t...process.env,\n\t\t...overrides\n\t};\n}',
|
||||
'function buildEnv(overrides) {\n\treturn paperclipKimiChildEnvironment(overrides);\n}');
|
||||
replace('env: globalThis.process.env', 'env: paperclipKimiChildEnvironment()');
|
||||
replace('\t\tbindTerminalCapability(terminal) {\n\t\t\tthis._terminalEnabled = terminal === true;\n\t\t}',
|
||||
'\t\tbindTerminalCapability(terminal) {\n\t\t\tthis._terminalEnabled = false;\n\t\t}');
|
||||
replace("\t\tget systemPrompt() {\n\t\t\treturn this.profileState.systemPrompt;\n\t\t}",
|
||||
"\t\tget systemPrompt() {\n\t\t\treturn paperclipKimiInstructionState().append(this.profileState.systemPrompt);\n\t\t}");
|
||||
replace("async function runAcpServerWithStream(stream, opts = {}) {\n",
|
||||
"async function runAcpServerWithStream(stream, opts = {}) {\n\tpaperclipKimiInstructionState();\n");
|
||||
replace('\t\tmerged() {\n\t\t\treturn {\n\t\t\t\t...Object.fromEntries(this.pluginServers),\n\t\t\t\t...Object.fromEntries(this.fileServers)\n\t\t\t};\n\t\t}',
|
||||
'\t\tmerged() {\n\t\t\treturn {};\n\t\t}');
|
||||
replace('\tawait pushFirstExisting$1(roots, USER_GENERIC_DIRS$1, osHomeDir, "user");', '');
|
||||
replace('async function projectRoots(workDir, options = {}) {\n', 'async function projectRoots(workDir, options = {}) {\n\treturn [];\n');
|
||||
replace('async function projectAgentRoots(fs, workDir, warn) {\n', 'async function projectAgentRoots(fs, workDir, warn) {\n\treturn [];\n');
|
||||
replace('async function userAgentRoots(fs, homeDir, osHomeDir, warn) {\n', 'async function userAgentRoots(fs, homeDir, osHomeDir, warn) {\n\treturn [];\n');
|
||||
replace('this.byEvent = indexHooks([...configured ?? [], ...pluginHooks]);', 'this.byEvent = new Map();');
|
||||
replace('\t\tasync evaluate(context) {\n\t\t\tfor (const policy of this.policies) {',
|
||||
'\t\tasync evaluate(context) {\n\t\t\treturn { policyName: "paperclip-runner", result: { kind: "ask" } };\n\t\t\tfor (const policy of this.policies) {');
|
||||
replace('if (req.display.kind !== "plan_review") return CANONICAL_OPTIONS;',
|
||||
'if (req.display.kind !== "plan_review") return CANONICAL_OPTIONS.filter(option => option.kind !== "allow_always");');
|
||||
replace('\t\t\t\ttoolName: name,\n\t\t\t\taction,\n\t\t\t\tdisplay\n\t\t\t};\n\t\t\tconst approvalContext = {',
|
||||
'\t\t\t\ttoolName: name,\n\t\t\t\ttoolInput: context.args,\n\t\t\t\taction,\n\t\t\t\tdisplay\n\t\t\t};\n\t\t\tconst approvalContext = {');
|
||||
replace('\t\ttitle: req.toolName,\n\t\tcontent\n', '\t\ttitle: req.toolName,\n\t\tkind: inferToolKind(req.toolName),\n\t\trawInput: req.toolInput,\n\t\tcontent\n');
|
||||
const bindRuntime = '\t\t\thandle.accessor.get(IAgentLifecycleService).handleOf(agentContext.agentId).accessor.get(IAgentRuntimeBindingService).switch(runtimeId);';
|
||||
replace(bindRuntime, `${bindRuntime}
|
||||
\t\t\tconst paperclipProfile = handle.accessor.get(IAgentLifecycleService).handleOf(agentContext.agentId).accessor.get(IAgentProfileService);
|
||||
\t\t\treturn paperclipKimiInstructionState().receipt(sessionId, paperclipProfile.getSystemPrompt());`);
|
||||
replace("\t\t\tawait this.bindSessionRuntime?.(sessionId);\n",
|
||||
"\t\t\tconst paperclipReceipt = await this.bindSessionRuntime?.(sessionId);\n\t\t\tif (!paperclipReceipt || paperclipReceipt.sessionId !== sessionId) throw new Error(\"Kimi native instruction receipt is missing\");\n\t\t\tthis.paperclipInstructionReceipts ??= new Map();\n\t\t\tthis.paperclipInstructionReceipts.set(sessionId, paperclipReceipt);\n");
|
||||
replace("\t\t\t\t...await this.activateSession(meta.id)\n",
|
||||
"\t\t\t\t...await this.activateSession(meta.id),\n\t\t\t\t_meta: { paperclipKimiInstructions: this.paperclipInstructionReceipts.get(meta.id) }\n");
|
||||
const lifecycleState = "\t\t\t\tconfigOptions: await acpSession.configOptions(),\n\t\t\t\tmodes: acpSession.modeState()\n\t\t\t};\n\t\t}\n\t\tasync ";
|
||||
replace(`${lifecycleState}resumeSession(params)`,
|
||||
`${lifecycleState.replace("modes: acpSession.modeState()", "modes: acpSession.modeState(),\n\t\t\t\t_meta: { paperclipKimiInstructions: this.paperclipInstructionReceipts.get(params.sessionId) }")}resumeSession(params)`);
|
||||
replace(`${lifecycleState}listSessions(params)`,
|
||||
`${lifecycleState.replace("modes: acpSession.modeState()", "modes: acpSession.modeState(),\n\t\t\t\t_meta: { paperclipKimiInstructions: this.paperclipInstructionReceipts.get(params.sessionId) }")}listSessions(params)`);
|
||||
replace('\t\t\t\tdriver.resolve({ stopReason: turnEndReasonToStopReason(event.reason, error) });',
|
||||
'\t\t\t\tif (event.reason === "failed") {\n\t\t\t\t\tdriver.reject(RequestError$1.internalError({ schema: "paperclip.kimi.provider-error.v1", kind: "provider_failed" }, "Kimi provider turn failed"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tdriver.resolve({ stopReason: turnEndReasonToStopReason(event.reason, error) });');
|
||||
const stepSchema = 'object({ type: string(), turnId: number$1().int().nonnegative(), step: number$1().int().positive(), stepId: string().min(1).max(256).optional(), finishReason: string().max(32).optional(), reason: string().max(32).optional() })';
|
||||
replace('\tagentEvents = {\n\t\t"turn.started": {', `\tagentEvents = {
|
||||
\t\t"turn.step.started": { kind: "stream", name: "events", type: "turn.step.started", schema: ${stepSchema} },
|
||||
\t\t"turn.step.completed": { kind: "stream", name: "events", type: "turn.step.completed", schema: ${stepSchema} },
|
||||
\t\t"turn.step.interrupted": { kind: "stream", name: "events", type: "turn.step.interrupted", schema: ${stepSchema} },
|
||||
\t\t"turn.started": {`);
|
||||
replace('\t\t\tthis.subscriptions.push(events.on("assistant.delta", (event) => {', `\t\t\tthis.subscriptions.push(
|
||||
\t\t\t\tevents.on("turn.step.started", event => this.dispatchTurnEvent(event.turnId, () => this.paperclipKimiStep(event, "start"))),
|
||||
\t\t\t\tevents.on("turn.step.completed", event => this.dispatchTurnEvent(event.turnId, () => this.paperclipKimiStep(event, "end"))),
|
||||
\t\t\t\tevents.on("turn.step.interrupted", event => this.dispatchTurnEvent(event.turnId, () => this.paperclipKimiStep(event, "interrupted"))),
|
||||
\t\t\t\tevents.on("assistant.delta", (event) => {`);
|
||||
replace('\t\tonAssistantDelta(event) {\n\t\t\tif (this.driverFor(event.turnId) === void 0) return;\n\t\t\tthis.emit(assistantDeltaToSessionUpdate(this.sessionId, event));\n\t\t}', `\t\tpaperclipKimiMessageFailure(driver) {
|
||||
\t\t\tthis.settleDriver(driver, () => driver.reject(RequestError$1.internalError({ schema: "paperclip.kimi.provider-error.v1", kind: "message_boundary_invalid" }, "Kimi native message boundary is invalid")));
|
||||
\t\t}
|
||||
\t\tpaperclipKimiChunk(driver, text, kind, thought = false) {
|
||||
\t\t\tthis.emit({ sessionId: this.sessionId, update: {
|
||||
\t\t\t\tsessionUpdate: thought ? "agent_thought_chunk" : "agent_message_chunk",
|
||||
\t\t\t\tmessageId: driver.paperclipKimiMessage.id, content: { type: "text", text },
|
||||
\t\t\t\t_meta: { origin: "kimi-native-assistant", source: "kimi-turn-step-v1", kind }
|
||||
\t\t\t} });
|
||||
\t\t}
|
||||
\t\tpaperclipKimiStep(event, phase) {
|
||||
\t\t\tconst driver = this.driverFor(event.turnId);
|
||||
\t\t\tif (driver === void 0) return;
|
||||
\t\t\tconst current = driver.paperclipKimiMessage;
|
||||
\t\t\tif (!Number.isSafeInteger(event.step) || event.step < 1 || !Number.isSafeInteger(event.turnId) || event.turnId < 0) return this.paperclipKimiMessageFailure(driver);
|
||||
\t\t\tif (phase === "start") {
|
||||
\t\t\t\tif (current?.active || typeof event.stepId !== "string" || !/^[A-Za-z0-9_-]{1,256}$/.test(event.stepId) || current && event.step <= current.step) return this.paperclipKimiMessageFailure(driver);
|
||||
\t\t\t\tdriver.paperclipKimiMessageIds ??= new Set();
|
||||
\t\t\t\tconst id = "kimi-message-" + paperclipKimiCreateHash("sha256").update(JSON.stringify([this.sessionId, event.turnId, event.stepId])).digest("hex");
|
||||
\t\t\t\tif (driver.paperclipKimiMessageIds.size >= 4096 || driver.paperclipKimiMessageIds.has(id)) return this.paperclipKimiMessageFailure(driver);
|
||||
\t\t\t\tdriver.paperclipKimiMessageIds.add(id);
|
||||
\t\t\t\tdriver.paperclipKimiMessage = { id, step: event.step, stepId: event.stepId, active: true };
|
||||
\t\t\t\tthis.paperclipKimiChunk(driver, "", "start");
|
||||
\t\t\t\treturn;
|
||||
\t\t\t}
|
||||
\t\t\tif (!current || current.step !== event.step || event.stepId !== void 0 && event.stepId !== current.stepId) return this.paperclipKimiMessageFailure(driver);
|
||||
\t\t\tconst reason = phase === "interrupted" ? event.reason : event.finishReason;
|
||||
\t\t\tconst reasons = phase === "interrupted" ? ["aborted", "max_steps", "error"] : ["end_turn", "max_tokens", "tool_use", "filtered", "paused", "other"];
|
||||
\t\t\tif (!reasons.includes(reason)) return this.paperclipKimiMessageFailure(driver);
|
||||
\t\t\tif (!current.active) {
|
||||
\t\t\t\tif (phase === "interrupted") return;
|
||||
\t\t\t\treturn this.paperclipKimiMessageFailure(driver);
|
||||
\t\t\t}
|
||||
\t\t\tthis.paperclipKimiChunk(driver, "", "end:" + reason);
|
||||
\t\t\tcurrent.active = false;
|
||||
\t\t}
|
||||
\t\tonAssistantDelta(event) {
|
||||
\t\t\tconst driver = this.driverFor(event.turnId);
|
||||
\t\t\tif (driver === void 0) return;
|
||||
\t\t\tif (!driver.paperclipKimiMessage?.active || typeof event.delta !== "string") return this.paperclipKimiMessageFailure(driver);
|
||||
\t\t\tthis.paperclipKimiChunk(driver, event.delta, "delta");
|
||||
\t\t}`);
|
||||
replace('\t\tonThinkingDelta(event) {\n\t\t\tif (this.driverFor(event.turnId) === void 0) return;\n\t\t\tthis.emit(thinkingDeltaToSessionUpdate(this.sessionId, event));\n\t\t}', `\t\tonThinkingDelta(event) {
|
||||
\t\t\tconst driver = this.driverFor(event.turnId);
|
||||
\t\t\tif (driver === void 0) return;
|
||||
\t\t\tif (!driver.paperclipKimiMessage?.active || typeof event.delta !== "string") return this.paperclipKimiMessageFailure(driver);
|
||||
\t\t\tthis.paperclipKimiChunk(driver, event.delta, "delta", true);
|
||||
\t\t}`);
|
||||
return source;
|
||||
}
|
||||
|
||||
export async function patchKimiRuntime(path) {
|
||||
const input = await readFile(path, "utf8");
|
||||
const digest = sha256(input);
|
||||
if (digest === KIMI_RUNTIME_PATCH.patchedSha256) return { ...KIMI_RUNTIME_PATCH };
|
||||
const patched = patchKimiRuntimeSource(input);
|
||||
if (sha256(patched) !== KIMI_RUNTIME_PATCH.patchedSha256) throw new Error("Kimi runtime patch output digest mismatch");
|
||||
await writeFile(path, patched);
|
||||
return { ...KIMI_RUNTIME_PATCH };
|
||||
}
|
||||
|
||||
if (import.meta.url?.endsWith("/patch-kimi-runtime.mjs") && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const path = process.argv[2];
|
||||
if (!path || process.argv.length !== 3) throw new Error("Usage: node patch-kimi-runtime.mjs <dist/main.mjs>");
|
||||
patchKimiRuntime(path).then(result => {
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
}).catch(error => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : "Kimi runtime patch failed"}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { access, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import test from "node:test";
|
||||
import { runInNewContext } from "node:vm";
|
||||
import { createKimiInstructionRuntime } from "./kimi-instructions-runtime.mjs";
|
||||
import { KIMI_RUNTIME_PATCH, patchKimiRuntime, patchKimiRuntimeSource, replaceKimiPatchAnchor } from "./patch-kimi-runtime.mjs";
|
||||
|
||||
const sha = text => createHash("sha256").update(text, "utf8").digest("hex");
|
||||
const binding = content => JSON.stringify({ schema: "paperclip.kimi.instructions.v1", content,
|
||||
digest: `sha256:${sha(content)}`, byteLength: Buffer.byteLength(content) });
|
||||
const runtime = raw => createKimiInstructionRuntime(raw, { createHash, byteLength: text => Buffer.byteLength(text, "utf8") });
|
||||
|
||||
test("native instruction binding preserves exact literals and rejects false readback", () => {
|
||||
const content = "\nGovernance: résumé 日本語 ${cwd} ${base_prompt}\n\n";
|
||||
const state = runtime(binding(content));
|
||||
const native = "Original native system prompt";
|
||||
const effective = `${native}\n\n${content}`;
|
||||
assert.equal(state.append(native), effective);
|
||||
assert.equal(state.append(effective), effective);
|
||||
const receipt = state.receipt("native-session", effective);
|
||||
assert.deepEqual(receipt, { schema: "paperclip.kimi.instructions.v1", sessionId: "native-session",
|
||||
digest: `sha256:${sha(content)}`, byteLength: Buffer.byteLength(content),
|
||||
effectiveDigest: `sha256:${sha(effective)}`, effectiveByteLength: Buffer.byteLength(effective) });
|
||||
assert.throws(() => state.receipt("native-session", native), /binding/);
|
||||
assert.throws(() => state.receipt("", effective), /binding/);
|
||||
assert.throws(() => state.append("x".repeat(2_097_153)), /binding/);
|
||||
assert.ok(Object.isFrozen(receipt));
|
||||
});
|
||||
|
||||
test("explicit empty instructions retain the native prompt and acknowledge zero bytes", () => {
|
||||
const state = runtime(binding(""));
|
||||
assert.equal(state.append("native"), "native");
|
||||
assert.equal(state.receipt("s", "native").byteLength, 0);
|
||||
});
|
||||
|
||||
test("malformed, changed, oversized, and ill-formed instruction bindings fail closed", () => {
|
||||
const valid = JSON.parse(binding("test"));
|
||||
for (const raw of [undefined, "{", "[]", "null", " ".repeat(200_001),
|
||||
binding("x\0y"), binding("x".repeat(32_769)), binding("\ud800"),
|
||||
JSON.stringify({ ...valid, byteLength: 1 }), JSON.stringify({ ...valid, digest: "sha256:" + "0".repeat(64) }),
|
||||
JSON.stringify({ ...valid, extra: true }), JSON.stringify({ schema: valid.schema, content: "test", digest: valid.digest })]) {
|
||||
assert.throws(() => runtime(raw), /binding/);
|
||||
}
|
||||
});
|
||||
|
||||
test("patch rejects unknown source and ambiguous anchors before writing", async () => {
|
||||
assert.throws(() => patchKimiRuntimeSource("unverified runtime"), /input digest/);
|
||||
assert.throws(() => replaceKimiPatchAnchor("absent", "needle", "replacement"), /exactly one/);
|
||||
assert.throws(() => replaceKimiPatchAnchor("needle needle", "needle", "replacement"), /exactly one/);
|
||||
const root = await mkdtemp(join(tmpdir(), "kimi-patch-test-"));
|
||||
const path = join(root, "main.mjs");
|
||||
try {
|
||||
await writeFile(path, "unverified runtime");
|
||||
await assert.rejects(patchKimiRuntime(path), /input digest/);
|
||||
assert.equal(await readFile(path, "utf8"), "unverified runtime");
|
||||
} finally { await rm(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
function segment(source, start, end) {
|
||||
const first = source.indexOf(start);
|
||||
assert.ok(first >= 0, `Missing native start: ${start}`);
|
||||
const last = source.indexOf(end, first + start.length);
|
||||
assert.ok(last > first, `Missing native end: ${end}`);
|
||||
return source.slice(first, last);
|
||||
}
|
||||
|
||||
test("pinned native package binds new/load and rejects failed turns", {
|
||||
skip: !process.env.PAPERCLIP_KIMI_VENDOR_MAIN,
|
||||
}, async () => {
|
||||
const input = await readFile(process.env.PAPERCLIP_KIMI_VENDOR_MAIN, "utf8");
|
||||
const patched = patchKimiRuntimeSource(input);
|
||||
assert.equal(sha(patched), KIMI_RUNTIME_PATCH.patchedSha256);
|
||||
const { build } = await import("esbuild");
|
||||
const bundledRoot = await mkdtemp(join(tmpdir(), "kimi-bundled-patch-test-"));
|
||||
try {
|
||||
const bundledPath = join(bundledRoot, "patch-kimi-runtime.bundle.cjs");
|
||||
await build({ entryPoints: [fileURLToPath(new URL("./patch-kimi-runtime.mjs", import.meta.url))],
|
||||
outfile: bundledPath, bundle: true, platform: "node", format: "cjs", target: "node24", logLevel: "silent",
|
||||
define: { "import.meta.url": JSON.stringify(pathToFileURL(bundledPath).href) } });
|
||||
const bundled = (await import(pathToFileURL(bundledPath))).default;
|
||||
assert.equal(sha(bundled.patchKimiRuntimeSource(input)), KIMI_RUNTIME_PATCH.patchedSha256,
|
||||
"installed bundled patcher produces exactly the same pinned native bytes");
|
||||
} finally { await rm(bundledRoot, { recursive: true, force: true }); }
|
||||
const content = " Exact ${cwd} instructions\n日本語\n";
|
||||
const env = { PAPERCLIP_KIMI_INSTRUCTIONS: binding(content) };
|
||||
const helper = segment(patched, "let paperclipKimiInstructionSnapshot;", "import { fileURLToPath");
|
||||
const context = { process: { env }, Buffer, paperclipKimiCreateHash: createHash };
|
||||
const state = runInNewContext(`${helper};paperclipKimiInstructionState()`, context);
|
||||
assert.equal(env.PAPERCLIP_KIMI_INSTRUCTIONS, undefined);
|
||||
env.PAPERCLIP_KIMI_INSTRUCTIONS = binding("replacement");
|
||||
assert.equal(runInNewContext("paperclipKimiInstructionState()", context), state);
|
||||
env.KIMI_API_KEY = "fake-provider-key";
|
||||
env.PAPERCLIP_TASK_SCOPED = "assigned-task-value";
|
||||
const childEnvironment = segment(patched, "function buildEnv(overrides) {", "function waitForSpawn(child)");
|
||||
const buildEnvironment = runInNewContext(`${childEnvironment};buildEnv`, context);
|
||||
for (const overrides of [undefined, { KIMI_API_KEY: "override-key", PAPERCLIP_KIMI_INSTRUCTIONS: "private", EXTRA: "assigned" }]) {
|
||||
const child = buildEnvironment(overrides);
|
||||
assert.equal(child.KIMI_API_KEY, undefined);
|
||||
assert.equal(child.PAPERCLIP_KIMI_INSTRUCTIONS, undefined);
|
||||
assert.equal(child.PAPERCLIP_TASK_SCOPED, "assigned-task-value");
|
||||
assert.equal(child.EXTRA, overrides?.EXTRA);
|
||||
}
|
||||
assert.equal(env.KIMI_API_KEY, "fake-provider-key");
|
||||
const terminalBinding = segment(patched, "\t\tbindTerminalCapability(terminal) {", "\n\t\tget terminalEnabled()");
|
||||
const bindTerminal = runInNewContext(`({${terminalBinding}}).bindTerminalCapability`);
|
||||
const terminalHolder = {};
|
||||
bindTerminal.call(terminalHolder, true);
|
||||
assert.equal(terminalHolder._terminalEnabled, false);
|
||||
const getter = segment(patched, "\t\tget systemPrompt() {", "\t\tget thinkingLevel()");
|
||||
const native = runInNewContext(`({profileState:{systemPrompt:"native"},${getter}})`, context);
|
||||
assert.equal(native.systemPrompt, `native\n\n${content}`);
|
||||
assert.equal(native.profileState.systemPrompt, "native");
|
||||
const receipt = state.receipt("s", native.systemPrompt);
|
||||
const baseline = segment(patched, "\t\tmerged() {\n\t\t\treturn {};", "\n\t\tasync watchConfigFiles()");
|
||||
const merged = runInNewContext(`({${baseline}}).merged`, context);
|
||||
assert.deepEqual(Object.keys(merged.call(new Proxy({}, { get: () => assert.fail("ambient MCP was read") }))), []);
|
||||
const policy = segment(patched, '\t\tasync evaluate(context) {\n\t\t\treturn { policyName: "paperclip-runner"', '\n\t};');
|
||||
const evaluate = runInNewContext(`({${policy}}).evaluate`, context);
|
||||
assert.equal((await evaluate.call(new Proxy({}, { get: () => assert.fail("native heuristic was read") }), {})).result.kind, "ask");
|
||||
const userRoots = segment(patched, "async function userRoots(homeDir, osHomeDir, options = {}) {", "async function projectRoots(");
|
||||
const discovered = [];
|
||||
const roots = await runInNewContext(`(${userRoots})`, { USER_BRAND_DIRS$1: ["skills"],
|
||||
pushBrandGroup: async (_roots, _dirs, home) => { discovered.push(home); },
|
||||
pushFirstExisting$1: () => assert.fail("global skills were read") })("/owned-kimi", "/ambient-home");
|
||||
assert.deepEqual(discovered, ["/owned-kimi"]);
|
||||
assert.equal(roots.length, 0);
|
||||
const host = { klient: { session: () => ({ agent: () => ({}) }) },
|
||||
bindDefaultModel: async () => {}, bindSessionRuntime: async () => undefined };
|
||||
const wire = segment(patched, "\t\tasync wireSession(sessionId) {", "\n\t\t/**");
|
||||
await assert.rejects(runInNewContext(`({${wire}}).wireSession`, context).call(host, "s"), /native instruction receipt/);
|
||||
const admitted = { ensureAuthed: async () => {}, klient: { global: { sessions: { create: async () => ({ id: "s" }) } } },
|
||||
activateSession: async () => ({}), paperclipInstructionReceipts: new Map([["s", receipt]]) };
|
||||
const create = segment(patched, "\t\tasync newSession(params) {", "\n\t\t/**");
|
||||
const createContext = { ...context, acpMcpServersToConfigRecord: value => value };
|
||||
const created = await runInNewContext(`({${create}}).newSession`, createContext).call(admitted, { cwd: "/workspace", mcpServers: [] });
|
||||
assert.equal(created._meta.paperclipKimiInstructions, receipt);
|
||||
for (const [name, next] of [["loadSession", "resumeSession"], ["resumeSession", "listSessions"]]) {
|
||||
const method = segment(patched, `\t\tasync ${name}(params) {`, `\t\tasync ${next}(params)`);
|
||||
const response = await runInNewContext(`({${method}}).${name}`, createContext).call({ ...admitted,
|
||||
warnIgnoredAdditionalDirs() {}, scheduleAvailableCommandsUpdate() {},
|
||||
resumeAcpSession: async () => ({ replayHistory: async () => {}, configOptions: async () => [], modeState: () => ({}) }) },
|
||||
{ sessionId: "s", mcpServers: [] });
|
||||
assert.equal(response._meta.paperclipKimiInstructions, receipt);
|
||||
}
|
||||
const terminal = segment(patched, "\t\tonTurnEnded(event) {", "\n\t\t/**");
|
||||
const rejection = [];
|
||||
const RequestError = { authRequired: () => ({ kind: "auth" }), internalError: data => data };
|
||||
const end = runInNewContext(`({${terminal}}).onTurnEnded`, { RequestError$1: RequestError,
|
||||
isAuthError: error => error?.code === "provider.auth_error", turnEndReasonToStopReason: () => "end_turn" });
|
||||
for (const code of ["provider.auth_error", "provider.server_error"]) {
|
||||
end.call({ driverFor: () => ({ reject: error => rejection.push(error), resolve: () => assert.fail("failed turn resolved") }),
|
||||
settleDriver: (_driver, action) => action(), emitUsageUpdate() {} }, { turnId: 1, reason: "failed", error: { code } });
|
||||
}
|
||||
assert.equal(rejection[0].kind, "auth");
|
||||
assert.equal(rejection[1].kind, "provider_failed");
|
||||
const infer = segment(patched, "function inferToolKind(name) {", "\n/**");
|
||||
const approval = segment(patched, "function buildPermissionToolCallUpdate(req) {", "\n/**");
|
||||
const permission = runInNewContext(`${infer}\n${approval}\nbuildPermissionToolCallUpdate`, {
|
||||
displayBlockToAcpContent: () => null, acpToolCallId: (turn, id) => `${turn}:${id}`,
|
||||
});
|
||||
for (const [toolName, kind] of [["Write", "edit"], ["Edit", "edit"], ["Read", "read"], ["Bash", "execute"], ["mcp__paperclip__read_workspace_file", "other"]]) {
|
||||
const args = { path: "/workspace/exact.txt", content: "body" };
|
||||
const request = permission({ toolName, toolInput: args, toolCallId: "native-tool", turnId: 1, display: {}, action: "test" });
|
||||
assert.equal(request.kind, kind);
|
||||
assert.equal(request.rawInput, args);
|
||||
assert.equal(request.toolCallId, "1:native-tool");
|
||||
}
|
||||
});
|
||||
|
||||
test("native step identity separates identical commentary/final text and retains empty boundaries", {
|
||||
skip: !process.env.PAPERCLIP_KIMI_VENDOR_MAIN,
|
||||
}, async () => {
|
||||
const patched = patchKimiRuntimeSource(await readFile(process.env.PAPERCLIP_KIMI_VENDOR_MAIN, "utf8"));
|
||||
const methods = segment(patched, "\t\tpaperclipKimiMessageFailure(driver) {", "\n\t\tonToolCallStarted(event)");
|
||||
const host = () => {
|
||||
const events = [], rejected = [];
|
||||
const driver = { reject: value => rejected.push(value) };
|
||||
const session = runInNewContext(`new class {${methods}}`, { paperclipKimiCreateHash: createHash,
|
||||
RequestError$1: { internalError: value => value } });
|
||||
Object.assign(session, { sessionId: "native-session", emit: value => events.push(value),
|
||||
driverFor: turnId => turnId === 7 && !driver.settled ? driver : undefined,
|
||||
settleDriver: (value, action) => { value.settled = true; action(); } });
|
||||
return { session, events, rejected, driver };
|
||||
};
|
||||
const h = host();
|
||||
h.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "native-step-uuid-one" }, "start");
|
||||
h.session.onAssistantDelta({ turnId: 7, delta: "SAME café" });
|
||||
h.session.onThinkingDelta({ turnId: 7, delta: "reasoning" });
|
||||
h.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "native-step-uuid-one", finishReason: "tool_use" }, "end");
|
||||
h.session.paperclipKimiStep({ turnId: 7, step: 2, stepId: "native-step-uuid-two" }, "start");
|
||||
h.session.onAssistantDelta({ turnId: 7, delta: "SAME café" });
|
||||
h.session.paperclipKimiStep({ turnId: 7, step: 2, stepId: "native-step-uuid-two", finishReason: "end_turn" }, "end");
|
||||
h.session.paperclipKimiStep({ turnId: 7, step: 3, stepId: "native-step-uuid-empty" }, "start");
|
||||
h.session.paperclipKimiStep({ turnId: 7, step: 3, stepId: "native-step-uuid-empty", finishReason: "end_turn" }, "end");
|
||||
const chunks = h.events.map(event => event.update);
|
||||
assert.equal(h.rejected.length, 0);
|
||||
assert.deepEqual(chunks.map(chunk => chunk._meta.kind), ["start", "delta", "delta", "end:tool_use", "start", "delta", "end:end_turn", "start", "end:end_turn"]);
|
||||
assert.equal(chunks[0].messageId, `kimi-message-${sha(JSON.stringify(["native-session", 7, "native-step-uuid-one"]))}`);
|
||||
assert.equal(new Set(chunks.map(chunk => chunk.messageId)).size, 3);
|
||||
assert.equal(chunks[2].sessionUpdate, "agent_thought_chunk");
|
||||
assert.ok(chunks.every(chunk => chunk._meta.origin === "kimi-native-assistant" && chunk._meta.source === "kimi-turn-step-v1"));
|
||||
assert.ok(chunks.filter(chunk => chunk._meta.kind !== "delta").every(chunk => chunk.content.text === ""));
|
||||
h.session.onAssistantDelta({ turnId: 8, delta: "foreign turn" });
|
||||
assert.equal(h.events.length, 9);
|
||||
for (const mutate of [
|
||||
value => value.session.onAssistantDelta({ turnId: 7, delta: "orphan" }),
|
||||
value => { value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "uuid" }, "start"); value.session.paperclipKimiStep({ turnId: 7, step: 2, stepId: "other" }, "start"); },
|
||||
value => { value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "uuid" }, "start"); value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "foreign", finishReason: "end_turn" }, "end"); },
|
||||
value => { value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "uuid" }, "start"); value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "uuid", finishReason: "invented" }, "end"); },
|
||||
value => { value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "uuid" }, "start"); value.session.paperclipKimiStep({ turnId: 7, step: 1, reason: "aborted" }, "interrupted"); value.session.paperclipKimiStep({ turnId: 7, step: 2, stepId: "uuid" }, "start"); },
|
||||
]) {
|
||||
const invalid = host(); mutate(invalid);
|
||||
assert.equal(invalid.rejected.length, 1);
|
||||
assert.equal(invalid.rejected[0].kind, "message_boundary_invalid");
|
||||
const count = invalid.events.length;
|
||||
invalid.session.onAssistantDelta({ turnId: 7, delta: "late after failure" });
|
||||
assert.equal(invalid.events.length, count);
|
||||
}
|
||||
});
|
||||
|
||||
test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a simulated local model service", {
|
||||
skip: process.env.PAPERCLIP_TEST_KIMI_MODEL_PEER !== "1",
|
||||
timeout: 60_000,
|
||||
}, async t => {
|
||||
const packageRoot = await realpath(fileURLToPath(new URL("../", import.meta.url)));
|
||||
const assets = join(packageRoot, "provider-assets/kimi", `${process.platform}-${process.arch}`);
|
||||
try { await access(join(assets, "native-closure.json")); }
|
||||
catch (error) {
|
||||
if (error.code !== "ENOENT") throw error;
|
||||
t.skip("Install the pinned Kimi runtime with paperclipai runtime setup kimi first");
|
||||
return;
|
||||
}
|
||||
const { tsImport } = await import("tsx/esm/api");
|
||||
const selectorPeer = process.env.PAPERCLIP_TEST_KIMI_FAULT_SELECTOR_PEER === "1";
|
||||
let compiledFactory;
|
||||
if (selectorPeer) {
|
||||
assert.equal(process.platform, "darwin"); assert.equal(process.arch, "arm64");
|
||||
const cli = join(packageRoot, "dist/cli/acpx-runtime-sidecar.cjs");
|
||||
const compiled = await readFile(cli, "utf8");
|
||||
const boundary = "var lines = (0, import_node_readline.createInterface)({";
|
||||
const at = compiled.indexOf(boundary);
|
||||
assert.ok(at > 0 && compiled.lastIndexOf(boundary) === at && Buffer.byteLength(compiled) <= 16 * 1024 * 1024);
|
||||
const module = { exports: {} };
|
||||
new Function("require", "module", "exports", "__filename", "__dirname", compiled.slice(0, at).replace(/^#![^\n]*\n/, "\n")
|
||||
+ "\nmodule.exports={verifyKimiInstallation,QUALIFIED_ACPX_PROFILES,acquireAcpxProviderLifetimeLease,createAcpxSidecarHostEnvironment,createSanitizedAcpxSpawnInput};")(
|
||||
createRequire(cli), module, module.exports, cli, dirname(cli));
|
||||
compiledFactory = module.exports;
|
||||
}
|
||||
const [{ verifyKimiInstallation }, { QUALIFIED_ACPX_PROFILES }, { kimiManagedConfiguration }, instructionModule, { createKimiMessageProjection }, { normalizeAcpxPermission }, environmentModule, { configuredEnvironmentProjection, CONFIGURED_ENVIRONMENT_KEYS }] = await Promise.all([
|
||||
tsImport("../src/drivers/acpx/kimi-installation.ts", import.meta.url),
|
||||
tsImport("../src/drivers/acpx/qualified-profiles.ts", import.meta.url),
|
||||
tsImport("../src/drivers/acpx/kimi-config.ts", import.meta.url),
|
||||
tsImport("../src/drivers/acpx/kimi-instructions.ts", import.meta.url),
|
||||
tsImport("../src/drivers/acpx/kimi-message-boundary.ts", import.meta.url),
|
||||
tsImport("../src/drivers/acpx/acp-permission-adapter.ts", import.meta.url),
|
||||
tsImport("../src/drivers/acpx/environment.ts", import.meta.url),
|
||||
tsImport("../src/configured-environment.ts", import.meta.url),
|
||||
]);
|
||||
const { createAcpRuntime, createFileSessionStore } = await import(pathToFileURL(join(packageRoot, "node_modules/acpx/dist/runtime.js")));
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "kimi-simulated-model-peer-")));
|
||||
const home = join(root, "home"), workspace = join(root, "workspace");
|
||||
await mkdir(home, { mode: 0o700 }); await mkdir(workspace);
|
||||
const instructions = "\nExact governance café 日本語 ${cwd} ${base_prompt}\nKeep the final LF.\n";
|
||||
const selectedModel = "kimi-for-coding";
|
||||
const binding = instructionModule.kimiInstructionBinding(instructions);
|
||||
const argumentsObject = { payload: "café 🧪\n${literal}", nested: { count: 0, empty: {}, flag: false } };
|
||||
const toolReceipt = sha(JSON.stringify(argumentsObject));
|
||||
const deniedPath = join(workspace, "denied-peer.txt");
|
||||
const notificationEnvironment = {
|
||||
PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT: join(root, "controlled-notification-context.json"),
|
||||
PAPERCLIP_RUNTIME_NOTIFICATION_HELPER: join(root, "controlled-notification-helper"),
|
||||
};
|
||||
await writeFile(notificationEnvironment.PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT, '{"controlledEnvironmentOnly":true}\n', { flag: "wx", mode: 0o600 });
|
||||
await writeFile(notificationEnvironment.PAPERCLIP_RUNTIME_NOTIFICATION_HELPER, "controlled path fixture\n", { flag: "wx", mode: 0o700 });
|
||||
const shellQuote = value => "'" + value.replaceAll("'", "'\\''") + "'";
|
||||
const notificationChecks = Object.entries(notificationEnvironment).map(([key, value]) => `if test "$${key}" != ${shellQuote(value)}; then printf "NOTIFICATION_DISCOVERY_CHANGED"; exit 1; fi; `).join("");
|
||||
const bashArguments = { command: 'if test -n "${KIMI_API_KEY+x}"; then printf "PROVIDER_KEY_PRESENT"; exit 1; fi; if test -n "${PAPERCLIP_KIMI_INSTRUCTIONS+x}"; then printf "PRIVATE_BINDING_PRESENT"; exit 1; fi; if test "$PAPERCLIP_TASK_SCOPED" != "assigned-task-canary"; then printf "TASK_VARIABLE_MISSING"; exit 1; fi; ' + notificationChecks + 'printf "KIMI_CHILD_CREDENTIAL_FENCE_OK"', timeout: 5 };
|
||||
const modelRequests = [], mcpCalls = [], permissions = [], deliveries = [], nativeMessages = [], owners = [];
|
||||
let scenario = "echo", modelStep = 0, peerFailure, permissionGate;
|
||||
const complete = (response, chunks) => {
|
||||
response.writeHead(200, { "content-type": "text/event-stream" });
|
||||
const bytes = Buffer.from(chunks.map(chunk => `data: ${JSON.stringify({ id: "simulated-peer", ...chunk })}\n\n`).join("") + "data: [DONE]\n\n");
|
||||
const index = bytes.indexOf(Buffer.from("é"));
|
||||
if (index >= 0) { response.write(bytes.subarray(0, index + 1)); response.end(bytes.subarray(index + 1)); }
|
||||
else response.end(bytes);
|
||||
};
|
||||
const server = createServer(async (request, response) => {
|
||||
try {
|
||||
if (request.method !== "POST") { response.writeHead(405); response.end(); return; }
|
||||
let body = "";
|
||||
for await (const chunk of request) { body += chunk; assert.ok(Buffer.byteLength(body) < 2_097_152); }
|
||||
const value = JSON.parse(body);
|
||||
if (request.url === "/v1/chat/completions") {
|
||||
assert.equal(request.headers.authorization, "Bearer simulated-peer-not-a-credential");
|
||||
assert.equal(value.model, selectedModel);
|
||||
const system = value.messages.find(message => message.role === "system")?.content;
|
||||
assert.equal(typeof system, "string");
|
||||
assert.ok(system.endsWith(`\n\n${instructions}`), "exact literal instructions reached native HTTP request");
|
||||
const ack = nativeMessages.map(entry => entry.message.result?._meta?.paperclipKimiInstructions).filter(Boolean).at(-1);
|
||||
assert.equal(ack?.digest, binding.digest);
|
||||
assert.equal(ack?.effectiveDigest, `sha256:${sha(system)}`, "native instruction receipt matches the full actual model-request prompt");
|
||||
modelRequests.push({ scenario, value });
|
||||
assert.ok(modelRequests.length <= 9, "bounded simulated model requests");
|
||||
modelStep++;
|
||||
if (scenario === "error") {
|
||||
response.writeHead(418, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ error: { type: "simulated_provider_error", message: "private-peer-error-canary" } }));
|
||||
return;
|
||||
}
|
||||
if (modelStep === 1) {
|
||||
const tools = scenario === "echo"
|
||||
? [{ id: "peer-echo", name: "mcp__peer__echo_peer", args: argumentsObject }, { id: "peer-empty", name: "mcp__peer__empty_peer", args: {} }]
|
||||
: scenario === "bash" ? [{ id: "peer-bash", name: "Bash", args: bashArguments }]
|
||||
: [{ id: `peer-${scenario}`, name: "Write", args: { path: permissionGate?.path ?? deniedPath, content: "must never be written" } }];
|
||||
for (const tool of tools) assert.ok(value.tools.some(declared => declared.function?.name === tool.name), `native request discloses ${tool.name}`);
|
||||
complete(response, [
|
||||
{ choices: [{ index: 0, delta: { content: "Before tool café." } }] },
|
||||
{ choices: [{ index: 0, delta: { tool_calls: tools.map((tool, index) => ({ index, id: tool.id, type: "function", function: { name: tool.name, arguments: JSON.stringify(tool.args).slice(0, 1) } })) } }] },
|
||||
...tools.map((tool, index) => ({ choices: [{ index: 0, delta: { tool_calls: [{ index, function: { arguments: JSON.stringify(tool.args).slice(1) } }] } }] })),
|
||||
{ choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }] },
|
||||
]);
|
||||
} else {
|
||||
const result = value.messages.find(message => message.role === "tool" && message.tool_call_id === `peer-${scenario}`);
|
||||
assert.ok(result, "the exact native tool result returned to the model");
|
||||
if (scenario === "echo") {
|
||||
assert.ok(JSON.stringify(result).includes(toolReceipt), `MCP receiver result reached the next native request: ${JSON.stringify({ result, mcpCalls: mcpCalls.length, permissions })}`);
|
||||
assert.ok(value.messages.some(message => message.role === "tool" && message.tool_call_id === "peer-empty" && JSON.stringify(message).includes("EMPTY_TOOL_ACCEPTED")), "zero-argument MCP result returned to the native model");
|
||||
}
|
||||
else if (scenario === "bash") assert.match(result.content, /^(?:Wall time: [0-9.]+ seconds\n)?KIMI_CHILD_CREDENTIAL_FENCE_OK\n?$/, "native Bash excluded provider/private credentials and retained the assigned task variable plus owned Runner notification discovery");
|
||||
else assert.match(JSON.stringify(result), /denied|rejected|approval/i);
|
||||
complete(response, [{ choices: [{ index: 0, delta: { content: scenario === "echo" ? "PEER_ECHO_OK café" : scenario === "bash" ? "PEER_BASH_OK" : "PEER_DENIED_OK" } }] },
|
||||
{ choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 12, completion_tokens: 3, total_tokens: 15 } }]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
assert.equal(request.url, "/mcp");
|
||||
if (value.id === undefined) { response.writeHead(202); response.end(); return; }
|
||||
if (value.method === "tools/call") {
|
||||
assert.ok(["echo_peer", "empty_peer"].includes(value.params.name));
|
||||
assert.deepEqual(value.params.arguments, value.params.name === "echo_peer" ? argumentsObject : {});
|
||||
mcpCalls.push(value.params);
|
||||
}
|
||||
const result = value.method === "initialize"
|
||||
? { protocolVersion: value.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "simulated-peer", version: "1" } }
|
||||
: value.method === "tools/list" ? { tools: [{ name: "echo_peer", description: "Echo the supplied JSON through the controlled peer.",
|
||||
inputSchema: { type: "object", properties: { payload: { type: "string" }, nested: { type: "object", additionalProperties: true } }, required: ["payload", "nested"] } },
|
||||
{ name: "empty_peer", description: "Return a receipt from an empty object argument.", inputSchema: { type: "object", properties: {}, additionalProperties: false } }] }
|
||||
: { content: [{ type: "text", text: value.params.name === "empty_peer" ? "EMPTY_TOOL_ACCEPTED" : JSON.stringify({ echoed: argumentsObject, receipt: toolReceipt }) }] };
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ jsonrpc: "2.0", id: value.id, result }));
|
||||
} catch (error) {
|
||||
peerFailure ??= error;
|
||||
if (!response.headersSent) response.writeHead(400, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ error: { type: "peer_assertion_failed", message: "simulated peer assertion failed" } }));
|
||||
}
|
||||
});
|
||||
const keys = ["PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"];
|
||||
const previous = keys.map(key => process.env[key]);
|
||||
const closeOwner = async owner => {
|
||||
if (owner.closed) return;
|
||||
owner.closed = true;
|
||||
const started = Date.now();
|
||||
const terminate = setTimeout(() => { for (const child of owner.children) if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM"); }, 2_000);
|
||||
const kill = setTimeout(() => { for (const child of owner.children) if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL"); }, 3_000);
|
||||
try {
|
||||
if (owner.handle) await owner.runtime.close({ handle: owner.handle, reason: "simulated peer cleanup" });
|
||||
} finally {
|
||||
try {
|
||||
await Promise.all(owner.completions);
|
||||
assert.ok(Date.now() - started < 5_000, "native process cleanup is bounded");
|
||||
} finally { clearTimeout(terminate); clearTimeout(kill); await owner.lease.close(); await owner.fence?.close(); }
|
||||
}
|
||||
};
|
||||
try {
|
||||
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); });
|
||||
const endpoint = `http://127.0.0.1:${server.address().port}`;
|
||||
const config = kimiManagedConfiguration(selectedModel).replace(/^base_url = .+$/m, `base_url = ${JSON.stringify(`${endpoint}/v1`)}`)
|
||||
+ "\n[loop_control]\nmax_attempts_per_step = 1\n";
|
||||
await writeFile(join(home, "config.toml"), config, { mode: 0o600 });
|
||||
process.env[keys[0]] = packageRoot; process.env[keys[1]] = join(packageRoot, "package.json");
|
||||
const installation = compiledFactory
|
||||
? await compiledFactory.verifyKimiInstallation(compiledFactory.QUALIFIED_ACPX_PROFILES.kimi)
|
||||
: await verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi);
|
||||
const store = createFileSessionStore({ stateDir: join(root, "acpx-state") });
|
||||
const taskEnvironment = configuredEnvironmentProjection({ PAPERCLIP_TASK_SCOPED: "assigned-task-canary",
|
||||
...Object.fromEntries(Object.keys(notificationEnvironment).map(key => [key, "forged-task-override"])) });
|
||||
for (const key of Object.keys(notificationEnvironment)) assert.ok(!Object.hasOwn(taskEnvironment, key));
|
||||
const explicitEnvironment = { ...taskEnvironment, ...notificationEnvironment, PATH: "/usr/bin:/bin", LANG: "C.UTF-8", KIMI_API_KEY: "simulated-peer-not-a-credential" };
|
||||
const sessionId = "controlled-native-environment";
|
||||
const compose = compiledFactory ?? environmentModule;
|
||||
const sidecarEnvironment = compose.createAcpxSidecarHostEnvironment({ ...explicitEnvironment,
|
||||
[environmentModule.ACPX_CREDENTIAL_BINDING_ENV]: environmentModule.createAcpxCredentialBinding(explicitEnvironment, "kimi", sessionId) }, "kimi", sessionId);
|
||||
const launchEnvironment = compose.createSanitizedAcpxSpawnInput(sidecarEnvironment, "kimi").env;
|
||||
for (const [key, value] of Object.entries(notificationEnvironment)) {
|
||||
assert.equal(launchEnvironment[key], value);
|
||||
assert.throws(() => compose.createSanitizedAcpxSpawnInput({ ...sidecarEnvironment,
|
||||
[CONFIGURED_ENVIRONMENT_KEYS]: JSON.stringify([key]), [key]: "forged-task-override" }, "kimi"), /configured environment projection/);
|
||||
}
|
||||
assert.equal(launchEnvironment.PAPERCLIP_TASK_SCOPED, "assigned-task-canary");
|
||||
assert.equal(launchEnvironment.KIMI_API_KEY, "simulated-peer-not-a-credential");
|
||||
const spawnNative = (lease, owner) => {
|
||||
const child = lease.spawn([], { cwd: workspace, stdio: "pipe", env: { ...launchEnvironment, HOME: home,
|
||||
CI: "1", NO_COLOR: "1", TERM: "dumb", KIMI_CODE_HOME: home,
|
||||
KIMI_CODE_NO_AUTO_UPDATE: "1", KIMI_DISABLE_TELEMETRY: "1",
|
||||
KIMI_CODE_WATCH: "0", KIMI_DISABLE_CRON: "1", PAPERCLIP_KIMI_INSTRUCTIONS: binding.payload,
|
||||
...(selectorPeer ? { PAPERCLIP_FAULT_ENV_CANARY: "must-not-appear-in-observed-argv" } : {}) } },
|
||||
owner.fence ? { credentialFenceFds: owner.fence.lifetimeFenceFds, activateCredentialFenceOwner: pid => owner.fence.activateLifetimeOwner(pid) } : undefined);
|
||||
owner.children.push(child);
|
||||
owner.completions.push(new Promise((resolve, reject) => { child.once("close", resolve); child.once("error", reject); }));
|
||||
return child;
|
||||
};
|
||||
const connect = async (resumeSessionId, sessionKey = "scripted-kimi-peer") => {
|
||||
const lease = await installation.openCommand();
|
||||
const admission = instructionModule.createKimiInstructionAdmission(instructions);
|
||||
const fence = compiledFactory ? await compiledFactory.acquireAcpxProviderLifetimeLease({ agentHomeDirectory: home }) : undefined;
|
||||
const owner = { lease, fence, children: [], completions: [], handle: undefined, runtime: undefined, closed: false };
|
||||
owners.push(owner);
|
||||
owner.runtime = createAcpRuntime({ cwd: workspace, sessionStore: store,
|
||||
agentRegistry: { resolve: () => "paperclip-verified-kimi-command", list: () => ["kimi"] },
|
||||
mcpServers: [{ type: "http", name: "peer", url: `${endpoint}/mcp`, headers: [] }],
|
||||
permissionMode: "approve-all", permissionPolicy: { defaultAction: "escalate" }, timeoutMs: 15_000,
|
||||
protocolGuardFactory: () => admission.createGuard(),
|
||||
onAcpMessage: (direction, message) => nativeMessages.push({ owner, direction, message }),
|
||||
onPermissionRequest: async (request, context) => {
|
||||
permissions.push(request.raw);
|
||||
assert.ok(context.responseDelivery instanceof Promise);
|
||||
assert.ok(request.raw.options.every(option => option.kind !== "allow_always"));
|
||||
const normalized = normalizeAcpxPermission(request, { provider: "kimi", workingDirectory: workspace });
|
||||
if (request.raw.toolCall.title === "Write") {
|
||||
assert.equal(request.raw.toolCall.kind, "edit", "native approval carries the actual tool's canonical kind");
|
||||
const path = request.raw.toolCall.rawInput.path;
|
||||
assert.equal(normalized.title, `Change file: ${path.slice(workspace.length + 1)}`);
|
||||
assert.equal(normalized.kind, "edit");
|
||||
assert.deepEqual(normalized.choices.map(choice => choice.key), ["accept", "decline", "cancel"]);
|
||||
for (const workingDirectory of [undefined, home]) {
|
||||
const unsafe = normalizeAcpxPermission(request, { provider: "kimi", workingDirectory });
|
||||
assert.equal(unsafe.title, "File change requested; target unavailable. Deny or cancel this request.");
|
||||
assert.deepEqual(unsafe.choices.map(choice => choice.key), ["decline", "cancel"]);
|
||||
assert.throws(() => unsafe.resolve({ action: "accept" }), /not an offered choice/);
|
||||
}
|
||||
}
|
||||
if (permissionGate) {
|
||||
assert.equal(request.raw.toolCall.title, "Write");
|
||||
assert.deepEqual(request.raw.toolCall.rawInput, { path: permissionGate.path, content: "must never be written" });
|
||||
permissionGate.context = context;
|
||||
permissionGate.delivery = context.responseDelivery.then(() => "delivered", () => "suppressed");
|
||||
permissionGate.requestId = nativeMessages.findLast(entry => entry.owner === owner && entry.direction === "inbound"
|
||||
&& entry.message.method === "session/request_permission"
|
||||
&& entry.message.params.toolCall.rawInput?.path === permissionGate.path)?.message.id;
|
||||
assert.ok(permissionGate.requestId !== undefined);
|
||||
permissionGate.markSeen();
|
||||
return permissionGate.decision;
|
||||
}
|
||||
deliveries.push(context.responseDelivery);
|
||||
if (request.raw.toolCall.title === "Write") {
|
||||
assert.deepEqual(request.raw.toolCall.rawInput, { path: deniedPath, content: "must never be written" });
|
||||
await assert.rejects(access(deniedPath), { code: "ENOENT" });
|
||||
return { outcome: "reject_once" };
|
||||
}
|
||||
if (request.raw.toolCall.title === "Bash") {
|
||||
assert.deepEqual(request.raw.toolCall.rawInput, bashArguments);
|
||||
return { outcome: "allow_once" };
|
||||
}
|
||||
assert.ok(["mcp__peer__echo_peer", "mcp__peer__empty_peer"].includes(request.raw.toolCall.title));
|
||||
assert.deepEqual(request.raw.toolCall.rawInput, request.raw.toolCall.title === "mcp__peer__echo_peer" ? argumentsObject : {});
|
||||
return { outcome: "allow_once" };
|
||||
},
|
||||
spawnAgent: () => spawnNative(lease, owner),
|
||||
});
|
||||
owner.handle = await owner.runtime.ensureSession({ sessionKey, agent: "kimi", mode: "persistent", cwd: workspace,
|
||||
...(resumeSessionId ? { resumeSessionId } : {}),
|
||||
sessionOptions: { model: selectedModel } });
|
||||
return owner;
|
||||
};
|
||||
const prompt = async (owner, name) => {
|
||||
scenario = name; modelStep = 0;
|
||||
const turn = owner.runtime.startTurn({ handle: owner.handle, mode: "prompt", text: `Controlled ${name} boundary.`, requestId: `peer-${name}` });
|
||||
const events = [];
|
||||
const drained = (async () => { for await (const event of turn.events) events.push(event); })();
|
||||
const result = await turn.result;
|
||||
await drained;
|
||||
if (peerFailure) throw peerFailure;
|
||||
if (name !== "error") {
|
||||
const chunks = events.filter(event => event.type === "text_delta" && event.stream === "output");
|
||||
assert.deepEqual(chunks.map(event => event.meta?.kind), ["start", "delta", "end:tool_use", "start", "delta", "end:end_turn"]);
|
||||
assert.ok(chunks.every(event => /^kimi-message-[a-f0-9]{64}$/.test(event.messageId)
|
||||
&& event.meta.origin === "kimi-native-assistant" && event.meta.source === "kimi-turn-step-v1"));
|
||||
assert.equal(new Set(chunks.map(event => event.messageId)).size, 2);
|
||||
assert.equal(chunks[0].messageId, chunks[2].messageId);
|
||||
assert.equal(chunks[3].messageId, chunks[5].messageId);
|
||||
assert.notEqual(chunks[0].messageId, chunks[3].messageId);
|
||||
assert.equal(chunks[1].text, "Before tool café.");
|
||||
assert.ok(chunks.filter(event => event.meta.kind !== "delta").every(event => event.text === ""));
|
||||
const projection = createKimiMessageProjection();
|
||||
const projected = events.map(event => projection.normalize(event));
|
||||
projection.settle();
|
||||
assert.deepEqual(projected.filter(event => event.assistantMessageBoundary).map(event => event.assistantMessageBoundary), [
|
||||
{ phase: "start" }, { phase: "end", stopReason: "toolUse" },
|
||||
{ phase: "start" }, { phase: "end", stopReason: "stop" },
|
||||
]);
|
||||
}
|
||||
return { result, events };
|
||||
};
|
||||
const first = await connect();
|
||||
const echoed = await prompt(first, "echo");
|
||||
assert.equal(echoed.result.status, "completed");
|
||||
assert.equal(mcpCalls.length, 2);
|
||||
assert.ok(echoed.events.some(event => event.type === "text_delta" && event.text.includes("PEER_ECHO_OK")));
|
||||
const shell = await prompt(first, "bash");
|
||||
assert.equal(shell.result.status, "completed");
|
||||
assert.ok(shell.events.some(event => event.type === "text_delta" && event.text.includes("PEER_BASH_OK")));
|
||||
assert.ok(nativeMessages.some(entry => entry.direction === "outbound" && entry.message.method === "initialize" && entry.message.params.clientCapabilities.terminal === true), "actual ACPX offered its mandatory client terminal");
|
||||
assert.ok(!nativeMessages.some(entry => entry.direction === "inbound" && entry.message.method === "terminal/create"), "Kimi kept Bash in its native fenced process service despite ACPX client terminal capability");
|
||||
await closeOwner(first);
|
||||
const second = await connect(first.handle.backendSessionId);
|
||||
const denied = await prompt(second, "deny");
|
||||
assert.equal(denied.result.status, "completed");
|
||||
await assert.rejects(access(deniedPath), { code: "ENOENT" });
|
||||
const failed = await prompt(second, "error");
|
||||
assert.equal(failed.result.status, "failed");
|
||||
assert.match(JSON.stringify(failed.result), /Kimi provider turn failed/);
|
||||
assert.ok(!JSON.stringify(failed.result).includes("private-peer-error-canary"));
|
||||
assert.ok(nativeMessages.some(entry => entry.direction === "outbound" && ["session/load", "session/resume"].includes(entry.message.method)), "ACPX restored the native session before the second model turn");
|
||||
assert.ok(permissions.some(request => request.toolCall.title === "Write"));
|
||||
assert.equal(permissions.length, 4);
|
||||
await Promise.all(deliveries);
|
||||
assert.equal(modelRequests.length, 7);
|
||||
await closeOwner(second);
|
||||
assert.ok(owners.every(owner => owner.children.every(child => child.exitCode !== null || child.signalCode !== null)));
|
||||
await assert.rejects(access(deniedPath), { code: "ENOENT" });
|
||||
t.diagnostic("SIMULATED provider service; real pinned Kimi 2.3.3 + ACPX manager: 7 native model requests with provider auth intact, exact create/resume instruction suffix, native UUID start/end separates pre-tool commentary from final text despite reused model response IDs, actual provider projection yields toolUse/stop boundaries, two MCP JSON/result round trips including terminal {}, native Bash excludes provider/private credentials and retains the assigned task variable and exact owned Runner notification paths after actual configured-environment projection rejected forged task overrides, rejected Write with no file, typed provider failure, owned process closure; zero external model calls");
|
||||
if (process.env.PAPERCLIP_TEST_KIMI_LIFECYCLE_PEER === "1") {
|
||||
const { readProcessTable, observeDescendantProcessTree } = await tsImport("../../../tests/runner-e2e/process-tree.ts", import.meta.url);
|
||||
const bounded = async (promise, label, milliseconds = 5_000) => {
|
||||
let timer;
|
||||
try {
|
||||
return await Promise.race([promise, new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error(`${label} exceeded its bound`)), milliseconds);
|
||||
})]);
|
||||
} finally { clearTimeout(timer); }
|
||||
};
|
||||
const nativeWorker = async owner => {
|
||||
assert.equal(owner.children.length, 1);
|
||||
if (selectorPeer) {
|
||||
const { readKimiLocalProcesses, selectKimiLocalTarget } = await tsImport("../../../tests/runner-e2e/kimi-local-provider-fault.ts", import.meta.url);
|
||||
const { kimiFaultRuntimeContract } = await tsImport("../../../tests/runner-e2e/kimi-provider-fault-contract.ts", import.meta.url);
|
||||
const table = await readProcessTable(); assert.ok(table);
|
||||
const root = table.find(row => row.pid === process.pid); assert.ok(root && root.processGroupId === process.pid);
|
||||
const tree = observeDescendantProcessTree(table, process.pid);
|
||||
const observed = tree.members.map(row => row.process);
|
||||
const processes = readKimiLocalProcesses(observed.map(row => row.pid));
|
||||
assert.ok(!JSON.stringify(processes).includes("must-not-appear-in-observed-argv"));
|
||||
const runId = process.argv[process.argv.indexOf("--run-id") + 1];
|
||||
const input = { authority: { pid: root.pid, groupId: root.processGroupId, startedAt: root.started, runId },
|
||||
journal: observed.map(row => ({ pid: row.pid, parent: row.parentPid, start: row.started })), processes,
|
||||
workspacePath: workspace, guardLoaderSha256: kimiFaultRuntimeContract("darwin-arm64").guardLoaderSha256 };
|
||||
const selected = selectKimiLocalTarget(input);
|
||||
assert.equal(selected.target.command, "kimi-code");
|
||||
assert.equal(selected.target.argv[0], "kimi-code");
|
||||
assert.ok(selected.ancestry.some(row => row.pid === owner.children[0].pid));
|
||||
for (const kind of ["missing", "foreign", "ambiguous"]) {
|
||||
const wrong = structuredClone(input);
|
||||
if (kind === "missing") wrong.processes = wrong.processes.filter(row => row.pid !== selected.target.pid);
|
||||
if (kind === "foreign") wrong.processes.find(row => row.pid === selected.target.ppid).argv[2] = "foreign loader";
|
||||
if (kind === "ambiguous") { const extra = { ...selected.target, pid: 2147483000 }; wrong.processes.push(extra); wrong.journal.push({ pid: extra.pid, parent: extra.ppid, start: new Date(Number(extra.startUs) / 1000).toISOString() }); }
|
||||
assert.throws(() => selectKimiLocalTarget(wrong), /kimi_local_provider_fault:/);
|
||||
}
|
||||
owner.selectorInput = input;
|
||||
return table.find(row => row.pid === selected.target.pid);
|
||||
}
|
||||
const rootPid = owner.children[0].pid;
|
||||
const table = await readProcessTable();
|
||||
assert.ok(table);
|
||||
const tree = observeDescendantProcessTree(table, rootPid);
|
||||
const candidates = tree.members.filter(member => member.depth === 1);
|
||||
assert.equal(candidates.length, 1, `one native Kimi worker belongs directly to the verified command root: ${JSON.stringify(tree.members.map(member => ({ depth: member.depth, kind: member.process.kind })))}`);
|
||||
const rootProcess = table.find(row => row.pid === rootPid);
|
||||
const worker = candidates[0].process;
|
||||
assert.ok(rootProcess && worker.pid > 1 && worker.pid !== rootPid && worker.pid !== process.pid);
|
||||
const fresh = await readProcessTable();
|
||||
assert.ok(fresh?.some(row => row.pid === rootPid && row.started === rootProcess.started && row.processGroupId === rootProcess.processGroupId));
|
||||
assert.ok(fresh?.some(row => row.pid === worker.pid && row.started === worker.started
|
||||
&& row.parentPid === rootPid && row.processGroupId === worker.processGroupId));
|
||||
return worker;
|
||||
};
|
||||
const assertRetired = async worker => {
|
||||
const deadline = Date.now() + 5_000;
|
||||
while (Date.now() < deadline) {
|
||||
const table = await readProcessTable();
|
||||
assert.ok(table);
|
||||
if (!table.some(row => row.pid === worker.pid && row.started === worker.started)) return;
|
||||
await new Promise(resolve => setTimeout(resolve, 25));
|
||||
}
|
||||
assert.fail("the observed native worker survived owned cleanup");
|
||||
};
|
||||
for (const boundary of ["stop", "loss"]) {
|
||||
const path = join(workspace, `${boundary}-must-not-exist.txt`);
|
||||
let markSeen, decide;
|
||||
const seen = new Promise(resolve => { markSeen = resolve; });
|
||||
const decision = new Promise(resolve => { decide = resolve; });
|
||||
permissionGate = { path, markSeen, decision, decide };
|
||||
const owner = await connect(undefined, `scripted-kimi-peer-${boundary}`);
|
||||
scenario = boundary; modelStep = 0;
|
||||
const turn = owner.runtime.startTurn({ handle: owner.handle, mode: "prompt", text: `Controlled pending permission ${boundary}.`, requestId: `peer-${boundary}` });
|
||||
const drained = (async () => { for await (const _event of turn.events) {} })();
|
||||
try {
|
||||
await bounded(seen, `${boundary} native permission observation`);
|
||||
await assert.rejects(access(path), { code: "ENOENT" });
|
||||
const worker = await nativeWorker(owner);
|
||||
if (boundary === "stop") await bounded(turn.cancel({ reason: "controlled pending permission Stop" }), "native Stop");
|
||||
else if (selectorPeer) {
|
||||
const { terminateKimiLocalProvider } = await tsImport("../../../tests/runner-e2e/kimi-local-provider-fault.ts", import.meta.url);
|
||||
const receipt = terminateKimiLocalProvider(owner.selectorInput);
|
||||
assert.equal(receipt.target.pid, worker.pid); assert.equal(receipt.workerSignalled, false);
|
||||
} else process.kill(worker.pid, "SIGKILL");
|
||||
const result = await bounded(turn.result, `${boundary} terminal result`);
|
||||
await bounded(drained, `${boundary} event drain`);
|
||||
if (peerFailure) throw peerFailure;
|
||||
assert.equal(result.status, boundary === "stop" ? "cancelled" : "failed");
|
||||
if (boundary === "loss") assert.equal(typeof result.error?.code, "string");
|
||||
assert.equal(permissionGate.context.signal.aborted, true, "pending host callback was revoked at settlement");
|
||||
const countAtSettlement = modelRequests.length;
|
||||
permissionGate.decide({ outcome: "allow_once" });
|
||||
assert.equal(await bounded(permissionGate.delivery, `${boundary} stale permission suppression`), "suppressed");
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
assert.equal(modelRequests.length, countAtSettlement, "a stale callback cannot restart provider inference");
|
||||
assert.ok(!nativeMessages.some(entry => entry.owner === owner && entry.direction === "outbound" && entry.message.id === permissionGate.requestId && entry.message.result?.outcome?.outcome === "selected"), "no stale allow response crossed the provider connection");
|
||||
await assert.rejects(access(path), { code: "ENOENT" });
|
||||
await closeOwner(owner);
|
||||
await assertRetired(worker);
|
||||
await assert.rejects(access(path), { code: "ENOENT" });
|
||||
} finally { permissionGate.decide({ outcome: "reject_once" }); permissionGate = undefined; }
|
||||
}
|
||||
assert.equal(modelRequests.length, 9);
|
||||
assert.equal(permissions.length, 6);
|
||||
if (selectorPeer) t.diagnostic("Compiled production factories and real lifetime fences selected the exact native Kimi child despite ps rendering; actual title mutation and environment exclusion passed, missing/foreign/ambiguous targets were refused, and sealed-byte/inode/run-bound fault injection retired only the owned native tree");
|
||||
t.diagnostic("SIMULATED provider service; pending native Write permission Stop and exact observed native-worker SIGKILL both revoked host callbacks, suppressed stale allow responses, produced no file/model continuation, settled typed results and retired owned native processes; zero external model calls");
|
||||
}
|
||||
} finally {
|
||||
try { await Promise.all(owners.map(closeOwner)); }
|
||||
finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
keys.forEach((key, index) => { if (previous[index] === undefined) delete process.env[key]; else process.env[key] = previous[index]; });
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile, spawn, spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { once } from "node:events";
|
||||
import { mkdtemp, mkdir, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import test from "node:test";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { promisify } from "node:util";
|
||||
import { bundleKimiProvisioner } from "./build-verified-provider-entrypoints.mjs";
|
||||
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../src/drivers/acpx/kimi-closure-pins.ts";
|
||||
|
||||
const run = promisify(execFile);
|
||||
|
||||
test("installed Kimi setup is self-contained, rejects corrupt assets and cancels pending downloads", { timeout: 10_000 }, async t => {
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-public-layout-")));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const pkg = join(root, "package"), cli = join(pkg, "dist/vendor/paperclip-runner/cli");
|
||||
await mkdir(cli, { recursive: true });
|
||||
await writeFile(join(pkg, "package.json"), '{"name":"@paperclipai/server","type":"module"}');
|
||||
await writeFile(join(pkg, "dist/index.js"), 'throw Error("server must not start");');
|
||||
await writeFile(join(cli, "acpx-runtime-sidecar.cjs"), "// installed layout fixture");
|
||||
const entry = join(cli, "provision-kimi.cjs");
|
||||
const packageRoot = resolve(dirname(new URL(import.meta.url).pathname), "..");
|
||||
const result = await bundleKimiProvisioner({ outputRoot: cli });
|
||||
for (const output of Object.values(result.metafile.outputs)) for (const dependency of output.imports) assert.match(dependency.path, /^node:/);
|
||||
assert.deepEqual((await readdir(join(cli, "kimi-provision-inputs"))).sort(), ["package-lock.json", "package.json"]);
|
||||
for (const name of ["package-lock.json", "package.json"]) assert.deepEqual(await readFile(join(cli, "kimi-provision-inputs", name)), await readFile(join(packageRoot, "scripts/kimi-distribution", name)));
|
||||
const setup = createRequire(import.meta.url)(entry);
|
||||
const layout = await setup.provisionKimiPackageRoot(entry);
|
||||
assert.equal(layout.root, pkg);
|
||||
assert.equal(layout.assetRoot, join(pkg, "dist/vendor/paperclip-runner"));
|
||||
const linked = join(cli, "linked.cjs"); await symlink(entry, linked);
|
||||
await assert.rejects(setup.provisionKimiPackageRoot(linked), /installed entrypoint/);
|
||||
assert.deepEqual(setup.provisionEnvironment({ PATH: "/usr/bin:/bin", HTTPS_PROXY: "http://127.0.0.1:9", KIMI_MODEL_API_KEY: "sensitive-canary", HOME: "/private/home", NODE_OPTIONS: "--require /foreign", NODE_PATH: "/foreign", NPM_TOKEN: "sensitive-canary" }), { PATH: "/usr/bin:/bin", LANG: "C.UTF-8", HTTPS_PROXY: "http://127.0.0.1:9" });
|
||||
const target = `${process.platform}-${process.arch}`;
|
||||
const parent = join(layout.assetRoot, "provider-assets/kimi");
|
||||
await mkdir(join(parent, target, "runtime"), { recursive: true });
|
||||
const deny = join(root, "deny.cjs");
|
||||
await writeFile(deny, `process.nextTick(()=>{const assert=require('node:assert/strict');for(const key of ['KIMI_MODEL_API_KEY','NPM_TOKEN','HOME','NODE_OPTIONS','NODE_PATH'])assert.equal(process.env[key],undefined,key)});const fail=()=>{throw Error('UNEXPECTED_NETWORK_OR_CHILD')};globalThis.fetch=fail;for(const m of ['node:net','node:tls','node:http','node:https']){const x=require(m);for(const key of ['connect','createConnection','request','get'])if(key in x)x[key]=fail}const c=require('node:child_process');for(const key of ['spawn','execFile','exec'])c[key]=fail;`);
|
||||
const child = spawnSync(process.execPath, ["--require", deny, entry], { env: { PATH: "/usr/bin:/bin", KIMI_MODEL_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", HOME: "/private/home", NODE_OPTIONS: "", NODE_PATH: "/foreign" }, encoding: "utf8", timeout: 10_000 });
|
||||
assert.ifError(child.error); assert.equal(child.status, 1); assert.match(child.stderr, /Kimi setup failed/);
|
||||
assert.doesNotMatch(child.stderr + child.stdout, /UNEXPECTED_NETWORK_OR_CHILD|sensitive-canary/);
|
||||
assert.deepEqual(await readdir(parent), [target]);
|
||||
assert.equal(JSON.parse(await readFile(join(pkg, "package.json"), "utf8")).name, "@paperclipai/server");
|
||||
await rm(join(parent, target), { recursive: true });
|
||||
const ready = join(root, "download-ready"), aborted = join(root, "download-aborted");
|
||||
const preload = join(root, "pending-download.cjs");
|
||||
await writeFile(preload, `globalThis.fetch=(_url,{signal})=>new Promise((_resolve,reject)=>{const pending=setInterval(()=>{},1000);require('node:fs').writeFileSync(${JSON.stringify(ready)},'ready');signal.addEventListener('abort',()=>{clearInterval(pending);require('node:fs').writeFileSync(${JSON.stringify(aborted)},'aborted');reject(signal.reason)},{once:true})});`);
|
||||
const cancelled = spawn(process.execPath, ["--require", preload, entry], { env: { PATH: "/usr/bin:/bin" }, stdio: ["ignore", "ignore", "pipe"] });
|
||||
const closed = once(cancelled, "close");
|
||||
let stderr = ""; cancelled.stderr.on("data", chunk => { stderr = (stderr + chunk).slice(-4096); });
|
||||
const kill = setTimeout(() => cancelled.kill("SIGKILL"), 5_000);
|
||||
try {
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
try { await readFile(ready); break; }
|
||||
catch (error) { if (error.code !== "ENOENT") throw error; await delay(10); }
|
||||
}
|
||||
assert.equal(await readFile(ready, "utf8"), "ready");
|
||||
cancelled.kill("SIGTERM");
|
||||
const [code, signal] = await closed;
|
||||
assert.equal(code, 1); assert.equal(signal, null);
|
||||
assert.match(stderr, /Kimi setup failed/);
|
||||
assert.equal(await readFile(aborted, "utf8"), "aborted");
|
||||
assert.deepEqual(await readdir(parent), []);
|
||||
} finally { clearTimeout(kill); if (cancelled.exitCode === null && cancelled.signalCode === null) { cancelled.kill("SIGKILL"); await closed; } }
|
||||
});
|
||||
|
||||
test("fresh archived installed provisioner downloads the exact closure, reuses it offline and rejects tampering", {
|
||||
skip: process.env.PAPERCLIP_TEST_KIMI_PUBLIC_SETUP !== "1" && "Set PAPERCLIP_TEST_KIMI_PUBLIC_SETUP=1 for public registry downloads; no model calls or provider credentials",
|
||||
timeout: 180_000,
|
||||
}, async t => {
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-public-setup-")));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const packed = join(root, "packed"), pkg = join(packed, "package"), cli = join(pkg, "dist/vendor/paperclip-runner/cli");
|
||||
await mkdir(cli, { recursive: true });
|
||||
await writeFile(join(pkg, "package.json"), '{"name":"@paperclipai/server","type":"module"}');
|
||||
await writeFile(join(pkg, "dist/index.js"), 'throw Error("server must not start");');
|
||||
await writeFile(join(cli, "acpx-runtime-sidecar.cjs"), "// installed layout fixture");
|
||||
await bundleKimiProvisioner({ outputRoot: cli });
|
||||
const archive = join(root, "setup.tgz"), installed = join(root, "installed");
|
||||
await run("tar", ["-czf", archive, "-C", packed, "package"], { timeout: 10_000 });
|
||||
await mkdir(installed);
|
||||
await run("tar", ["-xzf", archive, "-C", installed], { timeout: 10_000 });
|
||||
const entry = join(installed, "package/dist/vendor/paperclip-runner/cli/provision-kimi.cjs");
|
||||
const api = createRequire(import.meta.url)(entry);
|
||||
const environment = api.provisionEnvironment(process.env);
|
||||
const fresh = await run(process.execPath, ["--use-env-proxy", entry], { env: { ...environment, KIMI_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", HOME: "/private/foreign-home" }, timeout: 120_000, maxBuffer: 1024 * 1024 });
|
||||
assert.doesNotMatch(fresh.stdout + fresh.stderr, /sensitive-canary/);
|
||||
const receipt = JSON.parse(fresh.stdout.trim());
|
||||
assert.equal(receipt.status, "installed_verified");
|
||||
const target = `${process.platform}-${process.arch}`;
|
||||
const assets = join(installed, "package/dist/vendor/paperclip-runner/provider-assets/kimi", target);
|
||||
const metadata = JSON.parse(await readFile(join(assets, "kimi-distribution.json"), "utf8"));
|
||||
assert.equal(metadata.nativeClosureSha256, KIMI_DISTRIBUTION_CLOSURE_SHA256[target]);
|
||||
const offline = { ...environment, HTTPS_PROXY: "http://127.0.0.1:9", HTTP_PROXY: "http://127.0.0.1:9", https_proxy: "http://127.0.0.1:9", http_proxy: "http://127.0.0.1:9", NO_PROXY: "", no_proxy: "" };
|
||||
const reused = await run(process.execPath, ["--use-env-proxy", entry], { env: offline, timeout: 20_000, maxBuffer: 1024 * 1024 });
|
||||
assert.equal(JSON.parse(reused.stdout.trim()).status, "verified_existing");
|
||||
const worker = join(assets, "runtime/node_modules/@moonshot-ai/kimi-code/dist/search-worker.mjs");
|
||||
const changed = Buffer.concat([await readFile(worker), Buffer.from("\n// integrity test\n")]);
|
||||
await writeFile(worker, changed);
|
||||
await assert.rejects(run(process.execPath, ["--use-env-proxy", entry], { env: offline, timeout: 20_000, maxBuffer: 1024 * 1024 }), error => {
|
||||
assert.match(error.stderr, /Kimi setup failed/);
|
||||
assert.doesNotMatch(error.stderr, /download|ECONNREFUSED/i);
|
||||
return true;
|
||||
});
|
||||
assert.deepEqual(await readFile(worker), changed, "Invalid existing assets must not be silently replaced");
|
||||
t.diagnostic(JSON.stringify({ target, closure: metadata.nativeClosureSha256, patch: metadata.patch.version, provisionerSha256: createHash("sha256").update(await readFile(entry)).digest("hex"), freshInstalled: true, offlineReuse: true, tamperRejected: true, modelPrompts: 0, scope: "self-contained public-layout setup component; full Paperclip tarball is a separate proof" }));
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env node
|
||||
import { constants } from "node:fs";
|
||||
import { lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { materializeKimiDistribution, kimiDistributionEnvironment } from "./materialize-kimi-distribution.mjs";
|
||||
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../src/drivers/acpx/kimi-closure-pins.ts";
|
||||
import { verifyKimiInstallation } from "../src/drivers/acpx/kimi-installation.ts";
|
||||
import { QUALIFIED_ACPX_PROFILES } from "../src/drivers/acpx/qualified-profiles.ts";
|
||||
|
||||
export function provisionEnvironment(source = process.env) {
|
||||
return { ...kimiDistributionEnvironment(source), LANG: "C.UTF-8", PATH: source.PATH ?? "/usr/bin:/bin" };
|
||||
}
|
||||
|
||||
export async function provisionKimiPackageRoot(entrypoint) {
|
||||
const canonical = await realpath(entrypoint);
|
||||
if (canonical !== resolve(entrypoint) || basename(canonical) !== "provision-kimi.cjs" || !(await lstat(canonical)).isFile()) throw new Error("Kimi setup requires its installed entrypoint");
|
||||
const cli = dirname(canonical);
|
||||
const vendored = cli.endsWith("/dist/vendor/paperclip-runner/cli");
|
||||
if (!vendored && !cli.endsWith("/dist/cli")) throw new Error("Kimi setup requires the installed runner or server layout");
|
||||
const root = resolve(cli, vendored ? "../../../.." : "../..");
|
||||
const manifest = join(root, "package.json");
|
||||
const file = await open(manifest, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
||||
try {
|
||||
const before = await file.stat({ bigint: true });
|
||||
if (!before.isFile() || before.nlink !== 1n || before.size < 1n || before.size > 64n * 1024n) throw new Error("Kimi setup package manifest is invalid");
|
||||
const bytes = await file.readFile(), after = await file.stat({ bigint: true }), named = await lstat(manifest, { bigint: true });
|
||||
if (bytes.length !== Number(before.size) || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || named.dev !== before.dev || named.ino !== before.ino || named.isSymbolicLink()) throw new Error("Kimi setup package manifest changed");
|
||||
if (JSON.parse(bytes.toString("utf8")).name !== (vendored ? "@paperclipai/server" : "@paperclipai/paperclip-runner")) throw new Error("Kimi setup package identity does not match its layout");
|
||||
} finally { await file.close(); }
|
||||
return { root, manifest, cli, assetRoot: vendored ? resolve(cli, "..") : root };
|
||||
}
|
||||
|
||||
async function verifyInstallation(root, manifest) {
|
||||
const keys = ["PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"], previous = keys.map(key => process.env[key]);
|
||||
process.env[keys[0]] = root; process.env[keys[1]] = manifest;
|
||||
try {
|
||||
const installation = await verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi);
|
||||
const lease = await installation.openCommand();
|
||||
await lease.close();
|
||||
} finally { keys.forEach((key, index) => { if (previous[index] === undefined) delete process.env[key]; else process.env[key] = previous[index]; }); }
|
||||
}
|
||||
|
||||
async function absent(path) { try { await lstat(path); return false; } catch (error) { if (error.code === "ENOENT") return true; throw error; } }
|
||||
async function containedDirectory(root, path) {
|
||||
await mkdir(path, { recursive: true, mode: 0o700 });
|
||||
if (await realpath(path) !== path || !(await lstat(path)).isDirectory() || !path.startsWith(root + "/")) throw new Error("Kimi setup asset directory escapes its package");
|
||||
}
|
||||
|
||||
export async function provisionKimi(entrypoint, checkCancelled = () => {}, signal) {
|
||||
checkCancelled(); signal?.throwIfAborted();
|
||||
const target = `${process.platform}-${process.arch}`;
|
||||
if (!Object.hasOwn(KIMI_DISTRIBUTION_CLOSURE_SHA256, target)) throw new Error(`Kimi is not supported on ${target}`);
|
||||
const { root, manifest, cli, assetRoot } = await provisionKimiPackageRoot(entrypoint);
|
||||
const assets = join(assetRoot, "provider-assets"), parent = join(assets, "kimi");
|
||||
await containedDirectory(root, assets); await containedDirectory(root, parent);
|
||||
const lockPath = join(parent, `.setup-${target}.lock`);
|
||||
const lock = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
||||
let lockIdentity, temporary, temporaryIdentity, published, committed = false;
|
||||
const output = join(parent, target);
|
||||
try {
|
||||
lockIdentity = await lock.stat({ bigint: true });
|
||||
checkCancelled();
|
||||
if (!(await absent(output))) {
|
||||
await verifyInstallation(root, manifest);
|
||||
return { status: "verified_existing", target, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest };
|
||||
}
|
||||
temporary = await mkdtemp(join(parent, ".setup-private-")); temporaryIdentity = await lstat(temporary, { bigint: true });
|
||||
const stagingRoot = join(temporary, "package"); await mkdir(stagingRoot, { mode: 0o700 });
|
||||
await writeFile(join(stagingRoot, "package.json"), JSON.stringify({ name: "@paperclipai/paperclip-runner" }), { flag: "wx", mode: 0o600 });
|
||||
const stagedOutput = join(stagingRoot, "provider-assets/kimi", target);
|
||||
await materializeKimiDistribution({ outputRoot: stagedOutput, lockDirectory: join(cli, "kimi-provision-inputs"), checkCancelled, signal });
|
||||
await verifyInstallation(stagingRoot, join(stagingRoot, "package.json"));
|
||||
if (!(await absent(output))) throw new Error("Kimi setup destination appeared during installation; refusing replacement");
|
||||
checkCancelled();
|
||||
await mkdir(output, { mode: 0o700 }); published = await lstat(output, { bigint: true });
|
||||
for (const name of await readdir(stagedOutput)) {
|
||||
const named = await lstat(output, { bigint: true });
|
||||
if (named.dev !== published.dev || named.ino !== published.ino || named.isSymbolicLink()) throw new Error("Kimi setup output ownership changed during publication");
|
||||
await rename(join(stagedOutput, name), join(output, name));
|
||||
}
|
||||
await verifyInstallation(root, manifest);
|
||||
checkCancelled(); committed = true;
|
||||
return { status: "installed_verified", target, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest };
|
||||
} finally {
|
||||
const cleanup = [];
|
||||
if (published && !committed) cleanup.push((async () => {
|
||||
const named = await lstat(output, { bigint: true });
|
||||
if (named.dev !== published.dev || named.ino !== published.ino || named.isSymbolicLink()) throw new Error("Kimi setup output ownership changed; refusing cleanup");
|
||||
await rm(output, { recursive: true });
|
||||
})());
|
||||
if (temporary) cleanup.push((async () => {
|
||||
const named = await lstat(temporary, { bigint: true });
|
||||
if (!temporaryIdentity || named.dev !== temporaryIdentity.dev || named.ino !== temporaryIdentity.ino || named.isSymbolicLink()) throw new Error("Kimi setup staging ownership changed; refusing cleanup");
|
||||
await rm(temporary, { recursive: true });
|
||||
})());
|
||||
cleanup.push((async () => {
|
||||
try {
|
||||
lockIdentity ??= await lock.stat({ bigint: true }); const named = await lstat(lockPath, { bigint: true });
|
||||
if (named.dev !== lockIdentity.dev || named.ino !== lockIdentity.ino) throw new Error("Kimi setup lock ownership changed; refusing cleanup");
|
||||
await unlink(lockPath);
|
||||
} finally { await lock.close(); }
|
||||
})());
|
||||
const results = await Promise.allSettled(cleanup), failed = results.filter(result => result.status === "rejected");
|
||||
if (failed.length) throw new AggregateError(failed.map(result => result.reason), "Kimi setup cleanup failed; inspect the package before retrying");
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
||||
if (process.argv.length !== 2) throw new Error("Usage: paperclipai runtime setup kimi (explicit host installation; no model calls)");
|
||||
const environment = provisionEnvironment();
|
||||
for (const key of Object.keys(process.env)) delete process.env[key]; Object.assign(process.env, environment);
|
||||
const controller = new AbortController();
|
||||
const cancel = () => controller.abort();
|
||||
process.on("SIGINT", cancel); process.on("SIGTERM", cancel);
|
||||
const deadline = setTimeout(cancel, 900_000); deadline.unref();
|
||||
provisionKimi(fileURLToPath(import.meta.url), () => { if (controller.signal.aborted) throw new Error("Kimi setup cancelled; no installation was admitted"); }, controller.signal).finally(() => {
|
||||
clearTimeout(deadline); process.off("SIGINT", cancel); process.off("SIGTERM", cancel);
|
||||
}).then(result => console.log(JSON.stringify(result))).catch(error => { console.error(`Kimi setup failed: ${error.message}`); process.exitCode = 1; });
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import profiles from "../acpx-profiles.json" with { type: "json" };
|
||||
import distributions from "../cursor-distributions.json" with { type: "json" };
|
||||
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 as kimiClosures } from "../src/drivers/acpx/kimi-closure-pins.ts";
|
||||
|
||||
const canonical = value => Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
|
||||
: value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`
|
||||
: JSON.stringify(value);
|
||||
|
||||
/** Release assembly must reject a self-consistent pack built for another release. */
|
||||
export function verifyReleaseProviderPack(identity, sourceRevision, target = "linux-x64", expected = { profiles, distributions }) {
|
||||
export function verifyReleaseProviderPack(identity, sourceRevision, target = "linux-x64", expected = { profiles, distributions, kimiClosures }) {
|
||||
const { profiles: releaseProfiles, distributions: releaseDistributions } = expected;
|
||||
const payload = identity?.payload;
|
||||
if (identity?.schema !== "paperclip-runner/remote-provider-pack/v1" || !payload
|
||||
@@ -46,5 +47,20 @@ export function verifyReleaseProviderPack(identity, sourceRevision, target = "li
|
||||
|| !/^sha256:[a-f0-9]{64}$/.test(copilot.sha256 ?? "")) {
|
||||
throw new Error("Remote provider-pack Copilot identity does not match this release");
|
||||
}
|
||||
const kimiProfile = releaseProfiles.profiles.kimi;
|
||||
if (kimiProfile && kimiProfile.qualificationStatus !== "pending") {
|
||||
const releaseKimiClosures = expected.kimiClosures
|
||||
?? (kimiProfile.commandDigest === profiles.profiles.kimi.commandDigest ? kimiClosures : undefined);
|
||||
const kimi = payload.providers?.kimi;
|
||||
if (Object.hasOwn(payload.candidateProviders ?? {}, "kimi") || !kimi
|
||||
|| kimi.version !== kimiProfile.agentServerVersion || kimi.profileDigest !== kimiProfile.commandDigest
|
||||
|| !/^[a-f0-9]{64}$/.test(releaseKimiClosures?.["linux-x64"] ?? "")
|
||||
|| kimi.closureDigest !== `sha256:${releaseKimiClosures["linux-x64"]}`
|
||||
|| kimi.qualification !== "qualified" || kimi.path !== "provider-assets/kimi/linux-x64"
|
||||
|| Object.keys(kimi).sort().join(",") !== "closureDigest,path,profileDigest,qualification,sha256,version"
|
||||
|| !/^sha256:[a-f0-9]{64}$/.test(kimi.sha256 ?? "")) {
|
||||
throw new Error("Remote provider-pack Kimi identity does not match this release");
|
||||
}
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import profiles from "../acpx-profiles.json" with { type: "json" };
|
||||
import historicalKimi from "../test/fixtures/kimi-profile-v14-identity.json" with { type: "json" };
|
||||
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 as kimiClosures } from "../src/drivers/acpx/kimi-closure-pins.ts";
|
||||
import distributions from "../cursor-distributions.json" with { type: "json" };
|
||||
import { verifyReleaseProviderPack } from "./release-provider-pack.mjs";
|
||||
import { existingReleaseProviderPack, releaseProviderPackManifest } from "./stage-release-runner-binaries.mjs";
|
||||
@@ -25,6 +27,9 @@ function fixture() {
|
||||
payload.providers.copilot = { version: profiles.profiles.copilot.agentServerVersion,
|
||||
profileDigest: profiles.profiles.copilot.commandDigest, closureDigest: "sha256:a3d8f4367cfa1694d79e3f8b7930b6fbfe42a229c272b375b11951d631db8d07",
|
||||
qualification: "qualified", path: "provider-assets/copilot/linux-x64", sha256: digest };
|
||||
payload.providers.kimi = { version: profiles.profiles.kimi.agentServerVersion,
|
||||
profileDigest: profiles.profiles.kimi.commandDigest, closureDigest: `sha256:${kimiClosures["linux-x64"]}`,
|
||||
qualification: "qualified", path: "provider-assets/kimi/linux-x64", sha256: digest };
|
||||
return payload;
|
||||
}
|
||||
function seal(payload) { return { schema: "paperclip-runner/remote-provider-pack/v1", payload,
|
||||
@@ -55,7 +60,7 @@ test("release assembly rejects rehashed stale or missing packs before staging an
|
||||
const source = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
try {
|
||||
// Run the actual assembler in an isolated layout so it cannot overwrite built artifacts.
|
||||
for (const file of ["scripts/stage-release-runner-binaries.mjs", "scripts/release-provider-pack.mjs", "src/live/runner-binary.ts", "acpx-profiles.json", "cursor-distributions.json", "release-runner-targets.json"]) {
|
||||
for (const file of ["scripts/stage-release-runner-binaries.mjs", "scripts/release-provider-pack.mjs", "src/live/runner-binary.ts", "src/drivers/acpx/kimi-closure-pins.ts", "acpx-profiles.json", "cursor-distributions.json", "release-runner-targets.json"]) {
|
||||
mkdirSync(dirname(join(root, file)), { recursive: true }); cpSync(join(source, file), join(root, file));
|
||||
}
|
||||
const platforms = {};
|
||||
@@ -172,3 +177,29 @@ test("uses the frozen release's Copilot profile when the workflow checkout has a
|
||||
assert.equal(verifyReleaseProviderPack(manifest, revision, "linux-x64", { profiles: releaseProfiles, distributions }), manifest);
|
||||
assert.throws(() => verifyReleaseProviderPack(manifest, revision), /Copilot identity/);
|
||||
});
|
||||
|
||||
for (const field of ["version", "profileDigest", "closureDigest", "qualification", "path", "sha256"]) {
|
||||
test(`rejects an independently rehashed stale Kimi ${field}`, () => {
|
||||
const payload = fixture(); payload.providers.kimi[field] = "stale";
|
||||
assert.throws(() => verifyReleaseProviderPack(seal(payload), revision), /Kimi identity/);
|
||||
});
|
||||
}
|
||||
test("released Kimi requires one ordinary identity and exact frozen closure", () => {
|
||||
const missing = fixture(); delete missing.providers.kimi;
|
||||
assert.throws(() => verifyReleaseProviderPack(seal(missing), revision), /Kimi identity/);
|
||||
const duplicate = fixture(); duplicate.candidateProviders = { kimi: duplicate.providers.kimi };
|
||||
assert.throws(() => verifyReleaseProviderPack(seal(duplicate), revision), /Kimi identity/);
|
||||
const frozen = structuredClone(profiles); frozen.profiles.kimi.commandDigest = "sha256:" + "e".repeat(64);
|
||||
const payload = fixture(); payload.providers.kimi.profileDigest = frozen.profiles.kimi.commandDigest;
|
||||
assert.throws(() => verifyReleaseProviderPack(seal(payload), revision, "linux-x64", { profiles: frozen, distributions }), /Kimi identity/);
|
||||
assert.equal(verifyReleaseProviderPack(seal(payload), revision, "linux-x64", { profiles: frozen, distributions, kimiClosures }).payload, payload);
|
||||
});
|
||||
test("historical pending releases keep their separate candidate inventory", () => {
|
||||
const historical = structuredClone(profiles);
|
||||
historical.profiles.kimi = { ...historical.profiles.kimi, agentProfileVersion: 14,
|
||||
commandDigest: historicalKimi.commandDigest, qualificationStatus: historicalKimi.declaration.qualificationStatus };
|
||||
const payload = fixture(); payload.candidateProviders = { kimi: { ...payload.providers.kimi,
|
||||
profileDigest: historicalKimi.commandDigest, qualification: "pending" } }; delete payload.providers.kimi;
|
||||
const pack = seal(payload);
|
||||
assert.equal(verifyReleaseProviderPack(pack, revision, "linux-x64", { profiles: historical, distributions }), pack);
|
||||
});
|
||||
@@ -7,7 +7,7 @@
|
||||
"src/scenarios/scenario-plan.ts"
|
||||
],
|
||||
"counts": {
|
||||
"actions": 69,
|
||||
"actions": 70,
|
||||
"legacyRequirements": 106
|
||||
},
|
||||
"actions": [
|
||||
@@ -106,6 +106,25 @@
|
||||
"src/scenarios/scenario-explorer.test.ts::renders every scenario with exposure, control plane, authorization, diff, and parity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "send_chat_message",
|
||||
"ownership": "always_agent_tool",
|
||||
"surfaces": [
|
||||
"live"
|
||||
],
|
||||
"legacyAliases": [],
|
||||
"contractCase": "protocol-action:send_chat_message",
|
||||
"contractOwner": "src/catalog/protocol-action-contracts.test.ts::send_chat_message has a schema-valid canonical example and every declared projection",
|
||||
"legacyBehavioralCases": [],
|
||||
"deterministicCases": [
|
||||
"protocol-action:send_chat_message"
|
||||
],
|
||||
"legacyRequirementCases": [],
|
||||
"deterministicOwners": [
|
||||
"src/catalog/protocol-action-contracts.test.ts::send_chat_message has a schema-valid canonical example and every declared projection",
|
||||
"src/scenarios/scenario-explorer.test.ts::renders every scenario with exposure, control plane, authorization, diff, and parity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "set_task_monitor",
|
||||
"ownership": "optional_agent_tool",
|
||||
|
||||
@@ -262,12 +262,13 @@
|
||||
"owner": "always tools",
|
||||
"operationIds": [
|
||||
"get_task_history",
|
||||
"report_progress"
|
||||
"report_progress",
|
||||
"send_chat_message"
|
||||
],
|
||||
"controlPlaneOperationIds": [
|
||||
"append_audit_record"
|
||||
],
|
||||
"realSurface": "issue comment list/get/create routes",
|
||||
"realSurface": "issue comment list/get/create routes and own-agent chat messaging service",
|
||||
"mockStateDomains": [
|
||||
"task",
|
||||
"comments",
|
||||
@@ -275,8 +276,8 @@
|
||||
"idempotency",
|
||||
"audit"
|
||||
],
|
||||
"prpEvidence": "bounded read result or idempotent comment-write receipt plus audit reference",
|
||||
"gap": "Active-task binding is unbound; cross-task comment mutation is deliberately outside V1."
|
||||
"prpEvidence": "bounded read result, idempotent progress receipt, or persisted proactive chat comment with source task/run provenance",
|
||||
"gap": "Proactive chat delivery is bound to the running agent and responsible human; arbitrary chat destinations are unsupported. Other comment/history binding gaps remain recorded per operation."
|
||||
},
|
||||
{
|
||||
"id": "se",
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: canonical explanatory contract for the Paperclip runner V1 surface.
|
||||
|
||||
This document keeps three independent meanings of **group** separate. PRP families describe wire evidence and controller commands; capability placement decides who owns an operation; behavioral eval groups organize the 106 scenario corpus. None of the three axes can be used as a substitute for another.
|
||||
|
||||
The generated totals are **107 PRP events in 32 event families**, **18 controller commands in 7 command families**, **10 control-plane operations**, **69 reconciled semantic operations** (18 always, 51 optional), and **106 scenarios in 16 behavior groups**.
|
||||
The generated totals are **107 PRP events in 32 event families**, **18 controller commands in 7 command families**, **10 control-plane operations**, **70 reconciled semantic operations** (19 always, 51 optional), and **106 scenarios in 16 behavior groups**.
|
||||
|
||||
## Axis 1: PRP v1 event and command families
|
||||
|
||||
@@ -84,9 +84,9 @@ Placement has exactly three outcomes:
|
||||
| `schedule_blocker_wake` | Dependency-resolution wake scheduling. |
|
||||
| `reconcile_run` | Work assessment, terminal status arbitration, and recovery reconciliation. |
|
||||
|
||||
### Always-agent operations (18)
|
||||
### Always-agent operations (19)
|
||||
|
||||
`answer_status_question`, `block_task`, `finish_task`, `get_agent_instruction_history`, `get_task_context`, `get_task_history`, `inspect_operation_result`, `list_document_revisions`, `list_documents`, `read_agent_instructions`, `read_document`, `register_deliverable`, `report_progress`, `request_human_input`, `request_review`, `restore_agent_instructions`, `update_agent_instructions`, `write_document`.
|
||||
`answer_status_question`, `block_task`, `finish_task`, `get_agent_instruction_history`, `get_task_context`, `get_task_history`, `inspect_operation_result`, `list_document_revisions`, `list_documents`, `read_agent_instructions`, `read_document`, `register_deliverable`, `report_progress`, `request_human_input`, `request_review`, `restore_agent_instructions`, `send_chat_message`, `update_agent_instructions`, `write_document`.
|
||||
|
||||
### Optional operations (51) and grant groups (14)
|
||||
|
||||
@@ -172,6 +172,7 @@ Grant groups are documentation/exposure bundles, not additional authority. The o
|
||||
| `search_api` | `optional_agent_tool` | `api:discover` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | inline/no mapping | `live`<br>`live_codex` | `PaperclipRunnerToolAuthority`<br>Authenticated PRP tool input/result and existing HTTP route authorization/activity records.<br>catalog PRP status: `bound` |
|
||||
| `search_tasks` | `optional_agent_tool` | `discovery:tasks:read` | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `read` | `none` | no | `snapshot_read:company_tasks` | `scenario` + `live`<br>`live_codex` | `unbound`<br>read projection surfaced via a tool-result item event; no control-plane state diff<br>catalog PRP status: `audit_pending` |
|
||||
| `select_workspace` | `optional_agent_tool` | none | `standard`<br>`skill_test` | `write` | `required` | no | inline/no mapping | `live`<br>`live_codex` | `PaperclipRunnerToolAuthority`<br>Authenticated task workspace routes and durable selection/repository receipts.<br>catalog PRP status: `bound` |
|
||||
| `send_chat_message` | `always_agent_tool` | none | `standard` | `task_write` | `required` | no | inline/no mapping | `live`<br>`live_codex` | `sendAgentChatMessage`<br>Attributed chat comment and transactional delivery receipt without execution or task disposition changes.<br>catalog PRP status: `bound` |
|
||||
| `set_dependencies` | `optional_agent_tool` | `dependencies:write` | `standard`<br>`skill_test` | `company_write` | `required` | no | `semantic_command:set_dependencies` | `scenario` + `live`<br>`live_codex` | `issues.update.blockedByIssueIds`<br>semantic-operation item event plus company-entity state diff and audit record<br>catalog PRP status: `bound` |
|
||||
| `set_task_monitor` | `optional_agent_tool` | none | `standard` | `task_write` | `required` | no | inline/no mapping | `live`<br>`live_codex` | `prepareIssueMonitorUpdate`<br>Transactional persisted issue monitor, audit activity and idempotent run receipt.<br>catalog PRP status: `bound` |
|
||||
| `set_task_title` | `optional_agent_tool` | none | `standard`<br>`ask`<br>`planning`<br>`skill_test` | `task_write` | `required` | no | inline/no mapping | `live`<br>`live_codex` | `setIssueTitle`<br>Run-bound title update and transactional activity record.<br>catalog PRP status: `bound` |
|
||||
@@ -196,7 +197,7 @@ Behavior groups describe expected outcomes and trajectories. They do not grant t
|
||||
| [`hb` — Heartbeat](#behavior-group-hb-heartbeat) | control plane + always tools | `get_task_context`<br>`set_task_title` | `select_work`<br>`enforce_budget` | agent identity, inbox-lite, heartbeat context, budget and active-issue services | `company`<br>`actor`<br>`wake`<br>`task`<br>`budget`<br>`run` | 5 | runner/session/run context plus bounded task-context tool results | Production semantic binding is unbound; identity and work selection remain injected/control-plane-owned. |
|
||||
| [`co` — Checkout](#behavior-group-co-checkout) | control plane | none | `checkout_task` | POST /api/issues/:id/checkout and execution-lock services | `task`<br>`actor`<br>`run`<br>`idempotency`<br>`fault` | 6 | run preparation and issue-status decision evidence with checkout receipt | Intentionally no model tool; the production checkout receipt still needs the additive semantic-receipt envelope. |
|
||||
| [`st` — Status](#behavior-group-st-status) | always tools + control-plane arbitration | `answer_status_question`<br>`finish_task`<br>`block_task`<br>`request_review` | `reconcile_run`<br>`append_audit_record` | issue PATCH, review/liveness policy, and native finalization arbitration | `task`<br>`comments`<br>`interactions`<br>`blockers`<br>`audit`<br>`run` | 8 | semantic operation receipt, work assessment, issue-status decision, and terminal causality | Production semantic binding and additive typed operation/conflict receipts remain unimplemented. |
|
||||
| [`cm` — Comments](#behavior-group-cm-comments) | always tools | `get_task_history`<br>`report_progress` | `append_audit_record` | issue comment list/get/create routes | `task`<br>`comments`<br>`actor`<br>`idempotency`<br>`audit` | 6 | bounded read result or idempotent comment-write receipt plus audit reference | Active-task binding is unbound; cross-task comment mutation is deliberately outside V1. |
|
||||
| [`cm` — Comments](#behavior-group-cm-comments) | always tools | `get_task_history`<br>`report_progress`<br>`send_chat_message` | `append_audit_record` | issue comment list/get/create routes and own-agent chat messaging service | `task`<br>`comments`<br>`actor`<br>`idempotency`<br>`audit` | 6 | bounded read result, idempotent progress receipt, or persisted proactive chat comment with source task/run provenance | Proactive chat delivery is bound to the running agent and responsible human; arbitrary chat destinations are unsupported. Other comment/history binding gaps remain recorded per operation. |
|
||||
| [`se` — Search](#behavior-group-se-search) | optional discovery tools | `search_tasks`<br>`list_agents`<br>`get_agent`<br>`list_projects`<br>`list_goals`<br>`list_project_repositories` | none | company issue search and agent/project/goal list/get routes | `company`<br>`task`<br>`actor`<br>`project`<br>`goal` | 4 | bounded redacted read projections through tool-result item events | Goal operations remain scenario-only. Project and repository discovery contracts are delivered by the live server authority; they are not implemented by the mock command dispatcher. |
|
||||
| [`su` — Subtasks](#behavior-group-su-subtasks) | optional delegation tools | `create_task`<br>`reassign_task`<br>`hire_agent`<br>`handoff_case` | `route_wake` | company issue create, child issue, guarded reassignment, and wake services | `company`<br>`task`<br>`actor`<br>`blockers`<br>`wake`<br>`audit` | 4 | company/task state diff, audit reference, and continuation wake evidence | create_task is production-bound to ordinary active-issue child creation with assignment, dependency-ready wake, company checks, child limits, and durable source-scoped idempotency. handoff_case is live-only: the dedicated case service moves the same primary task after source execution settles, or records completion intent subject to ordinary completion gates; legacy scenarios do not claim that coverage. |
|
||||
| [`bl` — Blockers](#behavior-group-bl-blockers) | always/optional tools + control plane | `block_task`<br>`set_dependencies` | `schedule_blocker_wake`<br>`route_wake` | issue relations, blocker projection, liveness validation, and blocker wake services | `task`<br>`blockers`<br>`wake`<br>`actor`<br>`audit`<br>`fault` | 5 | dependency diff, block receipt, attention routing, and issue-status decision | set_dependencies is production-bound for the active issue; block_task remains unbound, and cancelled-blocker receipts still need typed additive evidence. |
|
||||
@@ -482,10 +483,10 @@ Current responsibility-based paths are normative. Numbered `phase-*` or mileston
|
||||
### Catalog split and deliberate replacement
|
||||
|
||||
- Scenario/eval catalog: **40** operations.
|
||||
- Live dispatcher catalog: **57** operations.
|
||||
- Shared: **28**; union/canonical authority: **69**.
|
||||
- Live dispatcher catalog: **58** operations.
|
||||
- Shared: **28**; union/canonical authority: **70**.
|
||||
- Scenario-only: `administer_company`, `export_company`, `inspect_operation_result`, `list_cases`, `list_company_skills`, `list_goals`, `list_routines`, `list_secret_metadata`, `manage_routine`, `read_secret_value`, `sync_company_skills`, `upsert_case`.
|
||||
- Live-only: `call_api`, `create_case`, `create_project`, `create_workflow`, `get_agent`, `get_agent_instruction_history`, `get_approval`, `get_approval_context`, `get_workspace`, `handoff_case`, `hire_agent`, `list_project_repositories`, `list_workflows`, `list_workspaces`, `prepare_repository`, `read_agent_instructions`, `read_case`, `read_workflow`, `restore_agent_instructions`, `schedule_wake`, `search_api`, `select_workspace`, `set_task_monitor`, `set_task_title`, `submit_complaint`, `submit_suggestion`, `update_agent_instructions`, `update_case`, `update_workflow`.
|
||||
- Live-only: `call_api`, `create_case`, `create_project`, `create_workflow`, `get_agent`, `get_agent_instruction_history`, `get_approval`, `get_approval_context`, `get_workspace`, `handoff_case`, `hire_agent`, `list_project_repositories`, `list_workflows`, `list_workspaces`, `prepare_repository`, `read_agent_instructions`, `read_case`, `read_workflow`, `restore_agent_instructions`, `schedule_wake`, `search_api`, `select_workspace`, `send_chat_message`, `set_task_monitor`, `set_task_title`, `submit_complaint`, `submit_suggestion`, `update_agent_instructions`, `update_case`, `update_workflow`.
|
||||
- The generated provider contract contains exactly the live catalog; the canonical union remains the migration authority until all scenario-only operations are either implemented, deferred, or removed by an explicit reconciliation decision.
|
||||
- `generic_api_request` stays exported only for controlled tests and cannot be cited as real-surface, mock-parity, or PRP product coverage.
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
it("pins the reconciled op-set relationship between the two catalogs", () => {
|
||||
const summary = capabilityCatalogReconciliation();
|
||||
expect(summary.scenarioCount).toBe(40);
|
||||
expect(summary.liveCount).toBe(57);
|
||||
expect(summary.liveCount).toBe(58);
|
||||
expect(summary.sharedCount).toBe(28);
|
||||
expect(summary.unionCount).toBe(69);
|
||||
expect(summary.unionCount).toBe(70);
|
||||
// Any operation added to or removed from either catalog without a
|
||||
// reconciliation decision changes these exact sets and fails the gate.
|
||||
expect(summary.liveOnly).toEqual([
|
||||
@@ -48,6 +48,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
"schedule_wake",
|
||||
"search_api",
|
||||
"select_workspace",
|
||||
"send_chat_message",
|
||||
"set_task_monitor",
|
||||
"set_task_title",
|
||||
"submit_complaint",
|
||||
@@ -73,7 +74,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
});
|
||||
|
||||
it("is the single source both catalogs derive their operation set from", () => {
|
||||
expect(CAPABILITY_CANONICAL_OPERATIONS).toHaveLength(69);
|
||||
expect(CAPABILITY_CANONICAL_OPERATIONS).toHaveLength(70);
|
||||
const canonicalIds = new Set(CAPABILITY_CANONICAL_OPERATIONS.map((operation) => operation.operationId));
|
||||
// Neither catalog may contain an operation absent from the canonical source.
|
||||
for (const tool of SCENARIO_CATALOG) expect(canonicalIds.has(tool.operationId)).toBe(true);
|
||||
@@ -107,7 +108,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
});
|
||||
|
||||
it("names placement, claims, task modes, side-effect class, idempotency, redaction, mock mapping, real binding status, and PRP evidence for every operation", () => {
|
||||
expect(CAPABILITY_CANONICAL_CATALOG).toHaveLength(69);
|
||||
expect(CAPABILITY_CANONICAL_CATALOG).toHaveLength(70);
|
||||
for (const operation of CAPABILITY_CANONICAL_CATALOG) {
|
||||
expect(operation.placement).toMatch(/^(always|optional)_agent_tool$/);
|
||||
expect(Array.isArray(operation.requiredClaims)).toBe(true);
|
||||
@@ -133,7 +134,7 @@ describe("canonical semantic-catalog reconciliation authority", () => {
|
||||
it("classifies real binding status so generic_api_request is never product coverage", () => {
|
||||
const summary = capabilityCatalogReconciliation();
|
||||
expect(summary.byRealBindingStatus).toEqual({
|
||||
live_codex: 56,
|
||||
live_codex: 57,
|
||||
scenario_mock: 12,
|
||||
test_only: 1,
|
||||
});
|
||||
|
||||
@@ -127,8 +127,9 @@ describe("semantic action catalog", () => {
|
||||
(action) => action.operationId,
|
||||
);
|
||||
|
||||
expect(operationIds).toHaveLength(49);
|
||||
expect(operationIds).toHaveLength(50);
|
||||
expect(operationIds).toContain("set_task_monitor");
|
||||
expect(operationIds).toContain("send_chat_message");
|
||||
expect(new Set(operationIds).size).toBe(operationIds.length);
|
||||
expect(operationIds).not.toContain("generic_api_request");
|
||||
expect(Object.isFrozen(PAPERCLIP_SEMANTIC_ACTION_CATALOG)).toBe(true);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { prepareRepositoryAction } from "../protocol-actions/prepare-repository.
|
||||
import { setTaskMonitorInputSchema } from "../protocol-actions/set-task-monitor.js";
|
||||
import { listProjectsDescription, listProjectsInputSchema } from "../protocol-actions/list-projects.js";
|
||||
import { setTaskTitleAction } from "../protocol-actions/set-task-title.js";
|
||||
import { sendChatMessageAction } from "../protocol-actions/send-chat-message.js";
|
||||
import { reassignTaskAction } from "../protocol-actions/reassign-task.js";
|
||||
import type {
|
||||
PaperclipJsonSchema,
|
||||
@@ -191,6 +192,12 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [
|
||||
),
|
||||
outputSchema: operationReceipt,
|
||||
}),
|
||||
descriptor({
|
||||
operationId: "send_chat_message", title: "Send message to your chat",
|
||||
description: sendChatMessageAction.documentation.description, effect: "write",
|
||||
allowedModes: ["standard"], inputSchema: sendChatMessageAction.live.descriptor.inputSchema,
|
||||
outputSchema: openObject,
|
||||
}),
|
||||
descriptor({
|
||||
operationId: "answer_status_question",
|
||||
title: "Answer status question",
|
||||
|
||||
@@ -2,6 +2,7 @@ export type PaperclipSemanticActionId =
|
||||
| "search_api"
|
||||
| "call_api"
|
||||
| "set_task_title"
|
||||
| "send_chat_message"
|
||||
| "set_task_monitor"
|
||||
| "get_task_context"
|
||||
| "get_task_history"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { bindAcpxCandidateAdmission } from "../drivers/acpx/candidate-admission.js";
|
||||
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../drivers/acpx/kimi-closure-pins.js";
|
||||
import { ANTIGRAVITY_DISTRIBUTION_PINS } from "../drivers/acpx/generated-profiles.js";
|
||||
import { canonicalNativeRuntimeContextDigest } from "../contracts/runtime-context.js";
|
||||
import { acpxProfileActivity } from "../drivers/acpx/profile-activity.js";
|
||||
@@ -15,6 +16,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createAcpxSidecarHostEnvironment, createSanitizedAcpxSpawnInput } from "../drivers/acpx/environment.js";
|
||||
import { createKimiMessageProjection } from "../drivers/acpx/kimi-message-boundary.js";
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -563,6 +565,31 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
expect(wire.writes).toEqual([]); expect(wire.sequence()).toBe(0);
|
||||
});
|
||||
|
||||
it("preserves validated native assistant boundaries without exposing Kimi provenance metadata", () => {
|
||||
const source = readFileSync(fileURLToPath(new URL("./acpx-runtime-sidecar.ts", import.meta.url)), "utf8");
|
||||
const start = source.indexOf(' if (event.type === "text_delta") {', source.indexOf("function sanitizeRuntimeEvent"));
|
||||
const end = source.indexOf(' if (event.type === "status") {', start);
|
||||
const sanitize = new Function("boundedOptionalText", "stableProviderIdentity", `return event => {${source.slice(start, end)} return null;}`)(
|
||||
(value: string) => value, (value: string) => `stable-${value}`,
|
||||
);
|
||||
const projection = createKimiMessageProjection<{ type: string; text: string; stream: string; messageId: string; meta: Record<string, string> }>();
|
||||
const event = (kind: string, text = "", stream = "output") => ({ type: "text_delta", text, stream,
|
||||
messageId: `kimi-message-${"a".repeat(64)}`,
|
||||
meta: { origin: "kimi-native-assistant", source: "kimi-turn-step-v1", kind, secret: "PRIVATE_PROVENANCE" } });
|
||||
const frames = [sanitize(projection.normalize(event("start"))),
|
||||
sanitize(projection.normalize(event("delta", "Working."))),
|
||||
sanitize(projection.normalize(event("delta", "Private reasoning", "thought"))),
|
||||
sanitize(projection.normalize(event("end:tool_use")))];
|
||||
expect(frames[0]).toMatchObject({ text: "", assistantMessageBoundary: { phase: "start" } });
|
||||
expect(frames[1]).toMatchObject({ type: "text_delta", text: "Working." });
|
||||
expect(frames[2]).toMatchObject({ type: "thinking" });
|
||||
expect(frames[3]).toMatchObject({ text: "", assistantMessageBoundary: { phase: "end", stopReason: "toolUse" } });
|
||||
expect(new Set(frames.map(frame => frame.messageId)).size).toBe(1);
|
||||
expect(JSON.stringify(frames)).not.toContain("PRIVATE_PROVENANCE");
|
||||
expect(JSON.stringify(frames)).not.toContain("kimi-native-assistant");
|
||||
projection.settle();
|
||||
});
|
||||
|
||||
it("passes only validated Pi native boundaries and history through the real text sanitizer", () => {
|
||||
const source = readFileSync(fileURLToPath(new URL("./acpx-runtime-sidecar.ts", import.meta.url)), "utf8");
|
||||
const start = source.indexOf(' if (event.type === "text_delta") {', source.indexOf("function sanitizeRuntimeEvent"));
|
||||
@@ -1258,17 +1285,56 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["cursor", "explicit-cursor-model"],
|
||||
["copilot", "explicit-copilot-model"],
|
||||
] as const)("initializes the declared %s profile with its current admission status", async (agent, model) => {
|
||||
["cursor", "explicit-cursor-model", "qualified"],
|
||||
["copilot", "explicit-copilot-model", "pending"],
|
||||
["kimi", "explicit-kimi-model", resolveQualifiedAcpxProfile("kimi", "explicit-kimi-model").qualificationStatus === "pending" ? "pending" : "qualified"],
|
||||
] as const)("initializes the declared %s profile with its current admission status", async (agent, model, qualification) => {
|
||||
const sidecar = startSidecar();
|
||||
const profile = resolveQualifiedAcpxProfile(agent, model);
|
||||
sidecar.write(initializeRequest(1, agent, model));
|
||||
const frame = await sidecar.next((value) => value.id === 1);
|
||||
expect(ACPX_CAPABILITY_PROFILES[agent].qualification).toBe(qualification);
|
||||
if (agent === "kimi" && profile.qualificationStatus === "pending") {
|
||||
expect(frame).toMatchObject({ id: 1, ok: false,
|
||||
error: { message: expect.stringContaining("qualification is pending"), retryable: false } });
|
||||
return;
|
||||
}
|
||||
expect(frame).toMatchObject({ id: 1, ok: true });
|
||||
const result = frame.result as Record<string, unknown>;
|
||||
expect(result.profile).toEqual(resolveQualifiedAcpxProfile(agent, model));
|
||||
expect(result.profile).toEqual(profile);
|
||||
expect(result.profile).toMatchObject({ reportedModelId: model });
|
||||
expect(ACPX_CAPABILITY_PROFILES[agent].qualification).toBe(agent === "cursor" ? "qualified" : "pending");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["darwin", "arm64"], ["linux", "x64"],
|
||||
] as const)("binds Kimi startup to its declared %s/%s host and session before provider launch", async (platform, architecture) => {
|
||||
const sidecar = startSidecar(), model = "explicit-kimi-model";
|
||||
const profile = resolveQualifiedAcpxProfile("kimi", model);
|
||||
const request = initializeRequest(1, "kimi", model);
|
||||
if (profile.qualificationStatus !== "pending") {
|
||||
sidecar.write(request);
|
||||
expect(await sidecar.next(frame => frame.id === 1)).toMatchObject({ ok: true, result: { profile } });
|
||||
return;
|
||||
}
|
||||
const target = `${platform}-${architecture}` as "darwin-arm64" | "linux-x64";
|
||||
const candidateAdmission = bindAcpxCandidateAdmission({ schema: "paperclip.acpx-candidate-admission.v1",
|
||||
companyId: "company-kimi-candidate", agent: "kimi", model, profileDigest: profile.commandDigest,
|
||||
nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[target]}`,
|
||||
platform, architecture, expiresAt: Date.now() + 60_000,
|
||||
}, { runId: "run-kimi-candidate", normalizedSessionId: "session-kimi-candidate" });
|
||||
sidecar.write({ ...request, params: { ...request.params, candidateAdmission } });
|
||||
const frame = await sidecar.next(value => value.id === 1);
|
||||
if (process.platform !== platform || process.arch !== architecture) {
|
||||
expect(frame).toMatchObject({ ok: false, error: { message: expect.stringContaining("candidate admission"), retryable: false } });
|
||||
return;
|
||||
}
|
||||
expect(frame).toMatchObject({ ok: true, result: { profile } });
|
||||
expect((frame.result as Record<string, unknown>).profile).toMatchObject({ reportedModelId: model });
|
||||
sidecar.write({ protocolVersion: ACPX_SIDECAR_PROTOCOL_VERSION, id: 2, command: "session.open", params: {
|
||||
runtimeDirectory: "/unused", workingDirectory: "/unused", normalizedSessionId: "other-kimi-session", agent: "kimi", model,
|
||||
candidateAdmission, permissionMode: "deny-all", permissionModePinned: true, providerPolicy: { readOnly: true }, systemInstructions: "", tools: [],
|
||||
} });
|
||||
expect(await sidecar.next(value => value.id === 2)).toMatchObject({ ok: false, error: { message: expect.stringContaining("candidate admission") } });
|
||||
});
|
||||
|
||||
it("refuses pending Antigravity bootstrap before any native process opens", async () => {
|
||||
@@ -1312,7 +1378,7 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
ok: false,
|
||||
error: {
|
||||
code: "acpx_sidecar_command_failed",
|
||||
message: "ACPX agent must be claude, codex, grok, cursor, copilot, pi, or antigravity",
|
||||
message: "ACPX agent must be claude, codex, grok, cursor, copilot, kimi, pi, or antigravity",
|
||||
retryable: false,
|
||||
},
|
||||
});
|
||||
@@ -1338,7 +1404,7 @@ function initializeRequest(
|
||||
id: number,
|
||||
agent: string,
|
||||
model = "gpt-5.6-sol",
|
||||
): Record<string, unknown> {
|
||||
) {
|
||||
return {
|
||||
protocolVersion: ACPX_SIDECAR_PROTOCOL_VERSION,
|
||||
id,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, validateAcpxRichE
|
||||
import type { PaperclipQuestionSet } from "../contracts/question-set.js";
|
||||
import { createAcpxToolEventNormalizer, createGrokMessageNormalizer } from "../provider-events.js";
|
||||
import { createPiMessageProjection, type PiProjectedMessageEvent } from "../drivers/acpx/pi-message-projection.js";
|
||||
import { createKimiMessageProjection, type KimiProjectedMessageEvent } from "../drivers/acpx/kimi-message-boundary.js";
|
||||
import { parseNativeRuntimeContext } from "../contracts/runtime-context.js";
|
||||
import {
|
||||
PRP_BLOCK_TOOL_NAME,
|
||||
@@ -667,7 +668,8 @@ async function pumpTurn(
|
||||
// the sidecar boundary, matching the in-process ACPX driver path.
|
||||
const normalizeToolEvent = createAcpxToolEventNormalizer<AcpRuntimeEvent>();
|
||||
const piMessages = initializedAgent === "pi" ? createPiMessageProjection<AcpRuntimeEvent>() : null;
|
||||
const normalizeMessage = piMessages ? piMessages.normalize : initializedAgent === "grok"
|
||||
const kimiMessages = initializedAgent === "kimi" ? createKimiMessageProjection<AcpRuntimeEvent>() : null;
|
||||
const normalizeMessage = kimiMessages ? kimiMessages.normalize : piMessages ? piMessages.normalize : initializedAgent === "grok"
|
||||
? createGrokMessageNormalizer<AcpRuntimeEvent>() : (event: AcpRuntimeEvent) => event;
|
||||
for await (const event of runtimeTurn.events) {
|
||||
toolEvidence?.tool(event);
|
||||
@@ -680,7 +682,7 @@ async function pumpTurn(
|
||||
);
|
||||
}
|
||||
const result = await runtimeTurn.result;
|
||||
if (result.status === "completed") piMessages?.settle();
|
||||
if (result.status === "completed") { piMessages?.settle(); kimiMessages?.settle(); }
|
||||
await drainExtensions();
|
||||
try {
|
||||
const usageAfter = await readSidecarHostStatusWithin(activeHost);
|
||||
@@ -1037,7 +1039,7 @@ function boundRuntimeEventForNormalization(
|
||||
} as BoundedRuntimeToolEvent;
|
||||
}
|
||||
|
||||
function sanitizeRuntimeEvent(event: PiProjectedMessageEvent<AcpRuntimeEvent>): Record<string, unknown> {
|
||||
function sanitizeRuntimeEvent(event: PiProjectedMessageEvent<AcpRuntimeEvent> & KimiProjectedMessageEvent<AcpRuntimeEvent>): Record<string, unknown> {
|
||||
const runtimeType = text(record(event).type);
|
||||
if (runtimeType === "plan") {
|
||||
return {
|
||||
@@ -1053,6 +1055,8 @@ function sanitizeRuntimeEvent(event: PiProjectedMessageEvent<AcpRuntimeEvent>):
|
||||
tag: event.tag ?? null,
|
||||
...(event.piMessageBoundary ? { piMessageBoundary: event.piMessageBoundary } : {}),
|
||||
...(event.piMessageHistory ? { piMessageHistory: true } : {}),
|
||||
...(event.assistantMessageBoundary ? { assistantMessageBoundary: event.assistantMessageBoundary } : {}),
|
||||
...(event.assistantMessageHistory ? { assistantMessageHistory: true } : {}),
|
||||
messageId:
|
||||
typeof event.messageId === "string" && event.messageId.length > 0
|
||||
? stableProviderIdentity(event.messageId, "message")
|
||||
@@ -1478,8 +1482,8 @@ function requireHost(
|
||||
}
|
||||
|
||||
function requireQualifiedAgent(value: unknown, model?: string, candidateAdmission?: AcpxCandidateStartupAdmission): QualifiedAcpxAgent {
|
||||
if (value !== "grok" && value !== "codex" && value !== "claude" && value !== "pi" && value !== "cursor" && value !== "copilot" && value !== "antigravity") {
|
||||
throw new Error("ACPX agent must be claude, codex, grok, cursor, copilot, pi, or antigravity");
|
||||
if (value !== "grok" && value !== "codex" && value !== "claude" && value !== "pi" && value !== "cursor" && value !== "copilot" && value !== "kimi" && value !== "antigravity") {
|
||||
throw new Error("ACPX agent must be claude, codex, grok, cursor, copilot, kimi, pi, or antigravity");
|
||||
}
|
||||
if (candidateAdmission) assertAcpxExecutionAdmission(resolveQualifiedAcpxProfile(value, model ?? ""), candidateAdmission);
|
||||
else assertAcpxProfileReleased(value);
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("eval-session request contract", () => {
|
||||
expect(parseEvalSessionCliArgs([...args, "--candidate-profile", "pi", "--expected-acpx-profile", JSON.stringify(expected)]))
|
||||
.toMatchObject({ candidateProfile: "pi", expectedAcpxProfile: expected });
|
||||
expect(() => parseEvalSessionCliArgs([...args, "--candidate-profile", "pi"])).toThrow("require --expected-acpx-profile");
|
||||
expect(() => parseEvalSessionCliArgs([...args, "--candidate-profile", "codex"])).toThrow("must be pi, cursor, or copilot");
|
||||
expect(() => parseEvalSessionCliArgs([...args, "--candidate-profile", "codex"])).toThrow("must be pi, cursor, copilot, or kimi");
|
||||
expect(() => parseEvalSessionCliArgs([...args, "--candidate-profile"])).toThrow("missing");
|
||||
expect(() => parseEvalSessionCliArgs([...args, "--request", "/tmp/other.json"])).toThrow("duplicate");
|
||||
expect(() => parseEvalSessionRequest(request(), { candidateProfile: "pi" })).toThrow("must match");
|
||||
|
||||
@@ -60,7 +60,7 @@ export interface EvalSessionAgentCoreProfile {
|
||||
timeoutSeconds: number;
|
||||
}
|
||||
|
||||
export type EvalCandidateProfile = "pi" | "cursor" | "copilot";
|
||||
export type EvalCandidateProfile = "pi" | "cursor" | "copilot" | "kimi";
|
||||
|
||||
export interface EvalSessionRequest {
|
||||
schema: typeof EVAL_SESSION_REQUEST_SCHEMA;
|
||||
@@ -255,7 +255,7 @@ export function parseEvalSessionRequest(
|
||||
acpxAgent !== undefined &&
|
||||
acpxAgent !== "codex" &&
|
||||
acpxAgent !== "claude" && acpxAgent !== "grok" &&
|
||||
acpxAgent !== "pi" && acpxAgent !== "cursor" && acpxAgent !== "copilot"
|
||||
acpxAgent !== "pi" && acpxAgent !== "cursor" && acpxAgent !== "copilot" && acpxAgent !== "kimi"
|
||||
) {
|
||||
throw new Error("eval-session acpxAgent must be a registered ACPX profile");
|
||||
}
|
||||
@@ -263,7 +263,7 @@ export function parseEvalSessionRequest(
|
||||
throw new Error("eval-session acpxAgent requires provider acpx");
|
||||
}
|
||||
const piThinkingLevel = resolvePiThinkingLevel(provider === "acpx" ? String(acpxAgent) : "", input.piThinkingLevel);
|
||||
const candidate = acpxAgent === "pi" || acpxAgent === "cursor" || acpxAgent === "copilot";
|
||||
const candidate = acpxAgent === "pi" || acpxAgent === "cursor" || acpxAgent === "copilot" || acpxAgent === "kimi";
|
||||
if (options.candidateProfile !== undefined && (provider !== "acpx" || acpxAgent !== options.candidateProfile || !candidate)) {
|
||||
throw new Error("--candidate-profile must match the request's registered candidate ACPX agent");
|
||||
}
|
||||
@@ -401,7 +401,7 @@ export function evalSessionUsage(
|
||||
throw new Error("completed turn omitted usage accounting");
|
||||
}
|
||||
const candidate = snapshot.config?.provider === "acpx"
|
||||
&& ["pi", "cursor", "copilot"].includes(snapshot.config.acpxAgent ?? "");
|
||||
&& ["pi", "cursor", "copilot", "kimi"].includes(snapshot.config.acpxAgent ?? "");
|
||||
const codex = snapshot.config?.provider === "codex";
|
||||
const providerCostUnavailable = candidate || codex;
|
||||
let estimate: EstimatedModelCost | null;
|
||||
|
||||
@@ -71,8 +71,8 @@ export function parseEvalSessionCliArgs(args: string[]): EvalSessionCliOptions {
|
||||
if (seen.has("--observability-config") && !seen.has("--gateway-config")) throw new Error("Eval observability requires an explicit controller gateway");
|
||||
const candidateIndex = args.indexOf("--candidate-profile");
|
||||
const candidateProfile = candidateIndex < 0 ? undefined : args[candidateIndex + 1];
|
||||
if (candidateProfile !== undefined && candidateProfile !== "pi" && candidateProfile !== "cursor" && candidateProfile !== "copilot") {
|
||||
throw new Error("--candidate-profile must be pi, cursor, or copilot");
|
||||
if (candidateProfile !== undefined && candidateProfile !== "pi" && candidateProfile !== "cursor" && candidateProfile !== "copilot" && candidateProfile !== "kimi") {
|
||||
throw new Error("--candidate-profile must be pi, cursor, copilot, or kimi");
|
||||
}
|
||||
const expectedIndex = args.indexOf("--expected-acpx-profile");
|
||||
const expectedText = expectedIndex < 0 ? undefined : args[expectedIndex + 1];
|
||||
@@ -317,7 +317,7 @@ export function boundedEvalSessionUsage(
|
||||
}
|
||||
const candidate = request.provider === "acpx"
|
||||
&& request.acpxAgent !== undefined
|
||||
&& ["pi", "cursor", "copilot"].includes(request.acpxAgent)
|
||||
&& ["pi", "cursor", "copilot", "kimi"].includes(request.acpxAgent)
|
||||
&& turn.snapshot.config.provider === "acpx"
|
||||
&& turn.snapshot.config.acpxAgent === request.acpxAgent;
|
||||
const measuredTurns = new Set(turn.snapshot.usageLedger?.map((entry) => entry.turnId));
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env node
|
||||
import { parseAcpxCandidateStartupAdmission, type AcpxCandidateStartupAdmission } from "../drivers/acpx/candidate-admission.js";
|
||||
import { probeKimiMetadata } from "../drivers/acpx/kimi-metadata-probe.js";
|
||||
import { KIMI_NODE_VERSION } from "../drivers/acpx/kimi-closure-pins.js";
|
||||
|
||||
async function main() {
|
||||
const model = process.env.PAPERCLIP_KIMI_PROBE_MODEL ?? "", apiKey = process.env.KIMI_API_KEY ?? "";
|
||||
const encodedAdmission = process.env.PAPERCLIP_ACPX_CANDIDATE_ADMISSION;
|
||||
const bindings = { PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT,
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST };
|
||||
for (const name of Object.keys(process.env)) delete process.env[name];
|
||||
for (const [name, value] of Object.entries(bindings)) if (value !== undefined) process.env[name] = value;
|
||||
let candidateAdmission: AcpxCandidateStartupAdmission | undefined;
|
||||
if (encodedAdmission !== undefined) {
|
||||
if (Buffer.byteLength(encodedAdmission) > 4096) throw new Error("Invalid metadata admission");
|
||||
candidateAdmission = parseAcpxCandidateStartupAdmission(JSON.parse(encodedAdmission), { agent: "kimi", model });
|
||||
}
|
||||
const result = process.versions.node !== KIMI_NODE_VERSION
|
||||
? { schema: "paperclip.kimi.metadata.v1", status: "failed", code: "KIMI_INSTALLATION_INVALID",
|
||||
message: "Kimi metadata verification requires the pinned Node runtime.", promptSent: false }
|
||||
: await probeKimiMetadata(model, apiKey, candidateAdmission);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
if (result.status !== "verified") process.exitCode = 1;
|
||||
}
|
||||
|
||||
void main().catch(() => {
|
||||
process.stdout.write(`${JSON.stringify({ schema: "paperclip.kimi.metadata.v1", status: "failed", code: "KIMI_REQUEST_FAILED",
|
||||
message: "Kimi Code metadata verification failed.", promptSent: false })}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -72,6 +72,23 @@ describe("configured environment authority", () => {
|
||||
expect(projected.PAPERCLIP_API_KEY).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reserves Kimi credentials for the selected provider instead of the generic task projection", async () => {
|
||||
const { createSanitizedAcpxSpawnInput } = await import("./drivers/acpx/environment.js");
|
||||
const projected = configuredEnvironmentProjection({ KIMI_API_KEY: "selected-kimi-secret", CUSTOM_FLAG: "allowed" });
|
||||
expect(projected.KIMI_API_KEY).toBeUndefined();
|
||||
expect(JSON.parse(projected[CONFIGURED_ENVIRONMENT_KEYS]!)).toEqual(["CUSTOM_FLAG"]);
|
||||
expect(() => configuredEnvironment({ [CONFIGURED_ENVIRONMENT_KEYS]: '["KIMI_API_KEY"]', KIMI_API_KEY: "selected-kimi-secret" }))
|
||||
.toThrow("Invalid configured environment projection");
|
||||
const selected = { ...projected, KIMI_API_KEY: "selected-kimi-secret" };
|
||||
expect(createSanitizedCodexEnvironment(selected).KIMI_API_KEY).toBeUndefined();
|
||||
for (const agent of ["claude", "codex", "grok", "cursor", "copilot"] as const) {
|
||||
expect(createSanitizedAcpxSpawnInput(selected, agent).env.KIMI_API_KEY).toBeUndefined();
|
||||
}
|
||||
expect(createSanitizedAcpxSpawnInput(selected, "kimi").env).toMatchObject({ KIMI_API_KEY: "selected-kimi-secret", CUSTOM_FLAG: "allowed" });
|
||||
// Pi already supports this credential through its explicit SDK allowlist.
|
||||
expect(createSanitizedAcpxSpawnInput(selected, "pi").env.KIMI_API_KEY).toBe("selected-kimi-secret");
|
||||
});
|
||||
|
||||
it("does not accept an ambient projection in the provider or shell policy", () => {
|
||||
vi.stubEnv(CONFIGURED_ENVIRONMENT_KEYS, JSON.stringify(["PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY"]));
|
||||
vi.stubEnv("PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY", "ambient-secret");
|
||||
@@ -81,7 +98,7 @@ describe("configured environment authority", () => {
|
||||
} finally { vi.unstubAllEnvs(); }
|
||||
});
|
||||
|
||||
it.each(["not-json", "[1]", '["NODE_OPTIONS"]', '["PAPERCLIP_API_KEY"]', '["PAPERCLIP_RUNNER_BOOTSTRAP_TICKET"]', '["OPENAI_API_KEY"]', '["LD_PRELOAD"]', '["bad=name"]', '["CUSTOM","CUSTOM"]', '["MISSING"]'])
|
||||
it.each(["not-json", "[1]", '["NODE_OPTIONS"]', '["PAPERCLIP_API_KEY"]', '["PAPERCLIP_RUNNER_BOOTSTRAP_TICKET"]', '["OPENAI_API_KEY"]', '["KIMI_API_KEY"]', '["LD_PRELOAD"]', '["bad=name"]', '["CUSTOM","CUSTOM"]', '["MISSING"]'])
|
||||
("rejects malformed or reserved projection %s", raw => {
|
||||
expect(() => configuredEnvironment({ [CONFIGURED_ENVIRONMENT_KEYS]: raw })).toThrow("Invalid configured environment");
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ const RESERVED_KEYS = new Set([
|
||||
"PYTHONPATH", "PYTHONHOME", "RUBYOPT", "RUBYLIB", "PERL5OPT", "PERL5LIB", "BASH_ENV", "ENV", "ZDOTDIR", "GH_CONFIG_DIR",
|
||||
"OPENAI_API_KEY", "CODEX_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN",
|
||||
"AWS_BEARER_TOKEN_BEDROCK", "OPENROUTER_API_KEY", "XAI_API_KEY", "CURSOR_API_KEY", "CURSOR_AUTH_TOKEN",
|
||||
"COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN", "GH_ENTERPRISE_TOKEN", "GITHUB_ENTERPRISE_TOKEN", "PAPERCLIP_GIT_TOKEN",
|
||||
"COPILOT_GITHUB_TOKEN", "KIMI_API_KEY", "GH_TOKEN", "GITHUB_TOKEN", "GH_ENTERPRISE_TOKEN", "GITHUB_ENTERPRISE_TOKEN", "PAPERCLIP_GIT_TOKEN",
|
||||
]);
|
||||
|
||||
function eligibleKey(key: string): boolean {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { resolveGrokDisableWebSearch } from "../drivers/acpx/grok-web-search.js"
|
||||
import { resolveGrokEffort } from "../drivers/acpx/grok-effort.js";
|
||||
import { resolveClaudeEffort } from "../drivers/acpx/claude-effort.js";
|
||||
import type { DotBindingSnapshot, MuseBindingSnapshot, ExternalBotProviderKind } from "./external-provider.js";
|
||||
import { QUALIFIED_ACPX_VERSION } from "../drivers/acpx/generated-profiles.js";
|
||||
import { QUALIFIED_ACPX_VERSION, QUALIFIED_ACPX_PROFILE_DATA } from "../drivers/acpx/generated-profiles.js";
|
||||
import { isSupportedAcpxProfileVersion, type AcpxProfileVersion } from "../drivers/acpx/profile-compatibility.js";
|
||||
import { isProviderMode } from "./provider-mode.js";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -93,7 +93,11 @@ export interface NativeAwsAgentCoreProfileSnapshot {
|
||||
eventExpiryDays: 90;
|
||||
}
|
||||
|
||||
export type NativeAcpxAgent = "pi" | "claude" | "codex" | "grok" | "cursor" | "copilot" | "antigravity";
|
||||
export type NativeAcpxAgent = keyof typeof QUALIFIED_ACPX_PROFILE_DATA;
|
||||
|
||||
function isNativeAcpxAgent(value: unknown): value is NativeAcpxAgent {
|
||||
return typeof value === "string" && Object.hasOwn(QUALIFIED_ACPX_PROFILE_DATA, value);
|
||||
}
|
||||
export type NativeCodexApprovalPolicy = "never" | "on-request" | "untrusted";
|
||||
export type NativeOpenCodePermissionMode = "allow" | "ask" | "deny";
|
||||
export type NativeAcpxPermissionMode = "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all";
|
||||
@@ -709,8 +713,8 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|
||||
if (providerModel === null) {
|
||||
throw new NativeExecutionInputError("input.provider.model is required for acpx");
|
||||
}
|
||||
if (provider.agent !== "pi" && provider.agent !== "claude" && provider.agent !== "codex" && provider.agent !== "grok" && provider.agent !== "cursor" && provider.agent !== "copilot" && provider.agent !== "antigravity") {
|
||||
throw new NativeExecutionInputError("input.provider.agent must be pi, claude, codex, grok, cursor, copilot, or antigravity");
|
||||
if (!isNativeAcpxAgent(provider.agent)) {
|
||||
throw new NativeExecutionInputError("input.provider.agent must name a registered ACPX profile");
|
||||
}
|
||||
if (isV4) {
|
||||
if (provider.permissionMode !== "approve-all" && provider.permissionMode !== "approve-paperclip" && provider.permissionMode !== "approve-reads" && provider.permissionMode !== "deny-all") {
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
DurablePrpControlPlane,
|
||||
decodeDurablePrpCheckpoint,
|
||||
inspectDurablePrpCheckpointHeader,
|
||||
inspectStagedRunAttachment,
|
||||
inspectStagedRunAttachmentHeader,
|
||||
type DurablePrpCheckpoint,
|
||||
durableRecoveryInternals,
|
||||
SemanticToolNotDispatchedError,
|
||||
@@ -53,6 +55,66 @@ const identity: DurableRecoveryIdentity = {
|
||||
const expectedRunnerVersion = "0.3.0";
|
||||
const expectedRunnerDigest = `sha256:${"a".repeat(64)}`;
|
||||
|
||||
it("persists an undispatched source-to-target attachment before bootstrap and preserves it after cache loss", async () => {
|
||||
const root = mkdtempSync(resolve(tmpdir(), "runner-staged-attachment-"));
|
||||
const target = { ...identity, runId: "00000000-0000-4000-8000-000000000002", turnId: "next-turn", itemId: "next-item" };
|
||||
const template = { provider: { provider: "codex", command: "fixture", instructions: "assigned instructions ".repeat(1_500) }, authorizedTools: {} };
|
||||
const core = new DurablePrpControlPlane({ stateDirectory: resolve(root, "first"), identity, expectedRunnerVersion, expectedRunnerDigest });
|
||||
let checkpoint!: DurablePrpCheckpoint, templateWrites = 0;
|
||||
core.setCheckpointPersistence(async value => { checkpoint = value; if (value.changedRecords.has("staged-attachment-template")) templateWrites++; });
|
||||
let restored: DurablePrpControlPlane | undefined;
|
||||
let client: AuthenticatedClient | null = null;
|
||||
try {
|
||||
core.stageRunAttachment(target, template);
|
||||
await core.flushCheckpoint();
|
||||
expect(checkpoint.runId).toBe(target.runId);
|
||||
expect(checkpoint.predecessor).toEqual({ runId: identity.runId, normalizedSessionId: identity.normalizedSessionId });
|
||||
expect(checkpoint.workCommandIds).toEqual([]);
|
||||
const proof = inspectStagedRunAttachmentHeader(checkpoint.records.get("header")!);
|
||||
expect(proof).toMatchObject({ attachment: { sourceIdentity: identity, targetIdentity: target, commandId: null } });
|
||||
expect(proof?.attachment).not.toHaveProperty("runAttachTemplate");
|
||||
expect(checkpoint.records.get("header")!.length).toBeLessThan(JSON.stringify(template).length / 2);
|
||||
expect(JSON.parse(checkpoint.records.get("staged-attachment-template")!)).toEqual(template);
|
||||
expect(proof?.runnerVersion).toBe(expectedRunnerVersion);
|
||||
expect(proof?.runnerDigest).toBe(expectedRunnerDigest);
|
||||
checkpoint.restoreSessionDirectory(resolve(root, "replacement"));
|
||||
restored = new DurablePrpControlPlane({ stateDirectory: resolve(root, "replacement", "control-plane"), identity, expectedRunnerVersion, expectedRunnerDigest });
|
||||
expect(inspectStagedRunAttachment(restored.store.state, target)).toMatchObject({ ...proof?.attachment, runAttachTemplate: template });
|
||||
expect(restored.store.state.commands).toEqual([]);
|
||||
expect(() => restored!.stageRunAttachment({ ...target, itemId: "foreign" }, template)).toThrow("conflicts");
|
||||
expect(() => restored!.queueCommand("turn.start", {})).toThrow("maintenance only");
|
||||
expect(() => restored!.queueCommand("run.attach", { ...template, paperclipNextAuthority: { identity: { ...target, itemId: "foreign" } } })).toThrow("conflicts");
|
||||
const corruptedRecords = new Map(checkpoint.records);
|
||||
corruptedRecords.set("staged-attachment-template", JSON.stringify({ provider: { command: "changed" } }));
|
||||
expect(() => decodeDurablePrpCheckpoint(corruptedRecords)).toThrow("native_runner_checkpoint_invalid");
|
||||
expect(inspectStagedRunAttachment(restored.store.state, { ...target, turnId: "foreign" })).toBeNull();
|
||||
for (const field of ["runnerInstanceId", "environmentLeaseId", "normalizedSessionId"] as const) {
|
||||
const changed = JSON.parse(checkpoint.records.get("header")!);
|
||||
changed.state.stagedRunAttachment.targetIdentity[field] = "foreign";
|
||||
expect(inspectStagedRunAttachmentHeader(JSON.stringify(changed))).toBeNull();
|
||||
}
|
||||
const downgraded = JSON.parse(checkpoint.records.get("header")!);
|
||||
downgraded.state.schema = "paperclip.runner.durable.control-plane-state.v1";
|
||||
expect(inspectStagedRunAttachmentHeader(JSON.stringify(downgraded))).toBeNull();
|
||||
const beforeRetry = checkpoint;
|
||||
core.stageRunAttachment(target, template); await core.flushCheckpoint(); expect(checkpoint).toBe(beforeRetry);
|
||||
core.queueCommand("session.snapshot", {}); await core.flushCheckpoint();
|
||||
expect(checkpoint.changedRecords.has("staged-attachment-template")).toBe(false);
|
||||
await core.start(); client = await authenticate(core, core.issueBootstrapTicket());
|
||||
expect(client).not.toBeNull();
|
||||
core.queueCommand("run.attach", { ...template, paperclipNextAuthority: { identity: target,
|
||||
connection: { mode: "connect", connectUrl: core.connectUrl } } }, "staged-next-attach");
|
||||
await core.flushCheckpoint();
|
||||
expect(templateWrites).toBe(1);
|
||||
expect(checkpoint.records.has("staged-attachment-template")).toBe(false);
|
||||
expect(checkpoint.removedRecords).toContain("staged-attachment-template");
|
||||
expect(() => decodeDurablePrpCheckpoint(checkpoint.records)).not.toThrow();
|
||||
} finally {
|
||||
client?.socket.destroy();
|
||||
await restored?.stop(); await core.stop(); rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("continues externally persisted commands after losing the local controller directory", async () => {
|
||||
const root = mkdtempSync(resolve(tmpdir(), "runner-external-store-"));
|
||||
const options = { stateDirectory: resolve(root, "control-plane"), identity, expectedRunnerVersion, expectedRunnerDigest };
|
||||
@@ -106,6 +168,7 @@ it("restores authentication and commands from opaque checkpoint records", async
|
||||
try {
|
||||
const ticket = first.issueBootstrapTicket();
|
||||
first.queueCommand("session.snapshot", {}, "maintenance");
|
||||
first.queueCommand("turn.stop", {}, "retirement-maintenance");
|
||||
first.queueCommand("turn.start", {}, "execution");
|
||||
first.persistPreparedLaunchConfiguration(identity, "retained-configuration");
|
||||
await first.flushCheckpoint();
|
||||
|
||||
@@ -54,6 +54,7 @@ const protocolVersion = RUNNER_PRP_PROTOCOL_MAX;
|
||||
const secureFrameSchema = "paperclip.runner.secure-frame.v1";
|
||||
const websocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
const coreStateSchema = "paperclip.runner.durable.control-plane-state.v1";
|
||||
const stagedCoreStateSchema = "paperclip.runner.durable.control-plane-state.staged-attachment.v1";
|
||||
const externalCoreStateSchema = "paperclip.runner.external-control-plane-state.v1";
|
||||
const transitionCoreStateSchema =
|
||||
"paperclip.runner.durable.control-plane-state.warm-transition.v1";
|
||||
@@ -140,8 +141,17 @@ interface ConnectionLeaseRecord {
|
||||
revokedAt: string | null;
|
||||
}
|
||||
|
||||
export interface StagedRunAttachment {
|
||||
readonly sourceIdentity: DurableRecoveryIdentity;
|
||||
readonly targetIdentity: DurableRecoveryIdentity;
|
||||
readonly runAttachTemplate: Record<string, unknown>;
|
||||
readonly templateDigest: string;
|
||||
readonly commandId: string | null;
|
||||
}
|
||||
|
||||
interface StoredCoreState {
|
||||
schema: typeof coreStateSchema | typeof transitionCoreStateSchema;
|
||||
schema: typeof coreStateSchema | typeof transitionCoreStateSchema | typeof stagedCoreStateSchema;
|
||||
stagedRunAttachment?: Omit<StagedRunAttachment, "runAttachTemplate"> & { runAttachTemplate?: Record<string, unknown> };
|
||||
identity: DurableRecoveryIdentity;
|
||||
/** External checkpoints contain control state, not a duplicate output log. */
|
||||
checkpointEventPolicy?: "control-v1";
|
||||
@@ -311,10 +321,12 @@ type CheckpointHeader = {
|
||||
schema: "paperclip.runner.controller-checkpoint.v2";
|
||||
runnerVersion: string;
|
||||
runnerDigest: string;
|
||||
state: Omit<StoredCoreState, "commands" | "committedEvents" | "ackedSourceSeq" | "replayDeliveries">;
|
||||
state: Omit<StoredCoreState, "commands" | "committedEvents" | "ackedSourceSeq" | "replayDeliveries" | "stagedRunAttachment">
|
||||
& { stagedRunAttachment?: Omit<StagedRunAttachment, "runAttachTemplate"> };
|
||||
};
|
||||
|
||||
function checkpointRunId(state: StoredCoreState): string {
|
||||
if (state.stagedRunAttachment) return state.stagedRunAttachment.targetIdentity.runId;
|
||||
if (state.warmTransition) return state.warmTransition.receipt.newIdentity.runId;
|
||||
const attachment = state.commands.findLast(command => command.type === "run.attach");
|
||||
const next = attachment?.payload.paperclipNextAuthority as { identity?: { runId?: string } } | undefined;
|
||||
@@ -347,17 +359,19 @@ function checkpointChanges(records: ReadonlyMap<string, string>, runId: string,
|
||||
function captureCheckpoint(state: StoredCoreState, runnerVersion: string, runnerDigest: string): DurablePrpCheckpoint {
|
||||
const runId = checkpointRunId(state);
|
||||
const records = new Map<string, string>();
|
||||
const { commands, committedEvents, ackedSourceSeq: _cursor, replayDeliveries: _replays, ...metadata } = state;
|
||||
const { commands, committedEvents, ackedSourceSeq: _cursor, replayDeliveries: _replays, stagedRunAttachment, ...metadata } = state;
|
||||
const stagedDescriptor = stagedRunAttachment ? (({ runAttachTemplate: _template, ...descriptor }) => descriptor)(stagedRunAttachment) : undefined;
|
||||
if (stagedRunAttachment?.commandId === null) records.set("staged-attachment-template", JSON.stringify(stagedRunAttachment.runAttachTemplate));
|
||||
const latestUsage = committedEvents.findLast(event => event.eventType === "usage.reported");
|
||||
const recoveryEvents = committedEvents.filter(event => !isStreamingEvent(event) || event === latestUsage);
|
||||
for (const command of commands) records.set(`command:${command.controllerSeq}`, JSON.stringify(command));
|
||||
for (const event of recoveryEvents) records.set(`event:${event.sourceSeq}`, JSON.stringify(event));
|
||||
const header: CheckpointHeader = { schema: "paperclip.runner.controller-checkpoint.v2",
|
||||
runnerVersion, runnerDigest, state: { ...metadata, checkpointEventPolicy: "control-v1" } };
|
||||
runnerVersion, runnerDigest, state: { ...metadata, ...(stagedDescriptor ? { stagedRunAttachment: stagedDescriptor } : {}), checkpointEventPolicy: "control-v1" } };
|
||||
records.set("header", JSON.stringify(header));
|
||||
records.set("cursor", JSON.stringify({ ackedSourceSeq: state.ackedSourceSeq, replayDeliveries: state.replayDeliveries,
|
||||
commandCount: commands.length, eventCount: recoveryEvents.length }));
|
||||
const maintenance = new Set(["runner.drain", "runner.suspend", "session.snapshot", "session.close"]);
|
||||
const maintenance = new Set(["turn.stop", "runner.drain", "runner.suspend", "session.snapshot", "session.close"]);
|
||||
return {
|
||||
runnerVersion, runnerDigest, runId, records, ...checkpointChanges(records, runId),
|
||||
recoveryEvidence: { runnerInstanceId: state.identity.runnerInstanceId, normalizedSessionId: state.identity.normalizedSessionId,
|
||||
@@ -397,11 +411,43 @@ export function inspectDurablePrpCheckpointHeader(contents: string): DurableReco
|
||||
const header = parseCheckpointHeader(contents);
|
||||
if (header.schema !== "paperclip.runner.controller-checkpoint.v2"
|
||||
|| header.state.checkpointEventPolicy !== "control-v1"
|
||||
|| ![coreStateSchema, transitionCoreStateSchema].includes(String(header.state.schema))) return null;
|
||||
|| ![coreStateSchema, transitionCoreStateSchema, stagedCoreStateSchema].includes(String(header.state.schema))
|
||||
|| (header.state.schema === stagedCoreStateSchema && !validStagedRunAttachment(header.state as unknown as StoredCoreState, true))) return null;
|
||||
return { ...header.state.identity };
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/** Bounded checkpoint metadata inspection; never itself grants recovery authority. */
|
||||
export function inspectStagedRunAttachmentHeader(contents: string): {
|
||||
attachment: Omit<StagedRunAttachment, "runAttachTemplate">; runnerVersion: string; runnerDigest: string;
|
||||
protocolIdentity: DurableRecoveryIdentity;
|
||||
phase: "source_bootstrap" | "attachment_queued" | "awaiting_result" | "prepared" | "activated" | "completed";
|
||||
receipt: DurableWarmRunTransition | null;
|
||||
} | null {
|
||||
try {
|
||||
if (Buffer.byteLength(contents) > maxStateBytes) return null;
|
||||
const header = parseCheckpointHeader(contents), state = header.state as unknown as StoredCoreState;
|
||||
if (header.schema !== "paperclip.runner.controller-checkpoint.v2"
|
||||
|| state.checkpointEventPolicy !== "control-v1" || !validStagedRunAttachment(state, true)
|
||||
|| !state.stagedRunAttachment) return null;
|
||||
const transition = state.warmTransition;
|
||||
const completed = stagedTargetActivated(state) ? state.completedWarmTransition : undefined;
|
||||
const bound = transition ?? completed;
|
||||
if (bound) {
|
||||
const receipt = bound.receipt;
|
||||
const result = transition?.phase === "awaiting_result" ? transition.expectedResult : bound.command.result;
|
||||
const expected = warmTransitionReceipt(receipt.oldIdentity, bound.command, result!, receipt.oldAckedSourceSeq,
|
||||
{ leaseId: receipt.leaseId, expiresAtUnixMs: receipt.leaseExpiresAtUnixMs, revocationEpoch: receipt.leaseRevocationEpoch },
|
||||
header.runnerVersion, header.runnerDigest);
|
||||
if (canonicalJson(expected) !== canonicalJson(receipt)) return null;
|
||||
}
|
||||
return { attachment: structuredClone(state.stagedRunAttachment), protocolIdentity: structuredClone(state.identity),
|
||||
phase: transition?.phase ?? (completed ? "completed" : state.stagedRunAttachment.commandId ? "attachment_queued" : "source_bootstrap"),
|
||||
receipt: bound ? structuredClone(bound.receipt) : null,
|
||||
runnerVersion: header.runnerVersion, runnerDigest: header.runnerDigest };
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function checkpointState(records: ReadonlyMap<string, string>) {
|
||||
const header = parseCheckpointHeader(records.get("header") ?? "null");
|
||||
const read = (key: string): unknown => {
|
||||
@@ -423,9 +469,14 @@ function checkpointState(records: ReadonlyMap<string, string>) {
|
||||
}
|
||||
const cursor = v2 ? read("cursor") : header.state;
|
||||
if (!isRecord(cursor) || (v2 && (cursor.commandCount !== commandKeys.length || cursor.eventCount !== eventKeys.length
|
||||
|| records.size !== commandKeys.length + eventKeys.length + 2))) throw new Error("native_runner_checkpoint_incomplete");
|
||||
|| records.size !== commandKeys.length + eventKeys.length + 2 + (isRecord(header.state.stagedRunAttachment) && header.state.stagedRunAttachment.commandId === null ? 1 : 0)))) throw new Error("native_runner_checkpoint_incomplete");
|
||||
const state = { ...header.state, ackedSourceSeq: cursor.ackedSourceSeq, replayDeliveries: cursor.replayDeliveries,
|
||||
commands: commandKeys.map(read), committedEvents: eventKeys.map(read) };
|
||||
commands: commandKeys.map(read), committedEvents: eventKeys.map(read) } as unknown as StoredCoreState;
|
||||
if (state.stagedRunAttachment?.commandId === null) {
|
||||
const template = read("staged-attachment-template");
|
||||
if (!isRecord(template)) throw new Error("native_runner_checkpoint_invalid");
|
||||
state.stagedRunAttachment = { ...state.stagedRunAttachment, runAttachTemplate: template };
|
||||
}
|
||||
if (!isStoredCoreState(state, header.state.identity)) throw new Error("native_runner_checkpoint_invalid");
|
||||
if (state.commands.some((command, index) => commandKeys[index] !== `command:${command.controllerSeq}`)
|
||||
|| state.committedEvents.some((event, index) => eventKeys[index] !== `event:${event.sourceSeq}`)) {
|
||||
@@ -683,9 +734,9 @@ function warmTransitionReceipt(
|
||||
|
||||
function validStoredWarmTransition(state: StoredCoreState): boolean {
|
||||
const transition = state.warmTransition;
|
||||
if (!transition) return state.schema === coreStateSchema;
|
||||
if (!transition) return state.schema === coreStateSchema || (state.schema === stagedCoreStateSchema && validStagedRunAttachment(state));
|
||||
if (
|
||||
state.schema !== transitionCoreStateSchema ||
|
||||
(state.schema !== transitionCoreStateSchema && state.schema !== stagedCoreStateSchema) ||
|
||||
!["awaiting_result", "prepared", "activated"].includes(transition.phase) ||
|
||||
!isRecord(transition.receipt) ||
|
||||
!isRecord(transition.command) ||
|
||||
@@ -802,6 +853,76 @@ function unsettledSemanticInput(
|
||||
}
|
||||
}
|
||||
|
||||
function stagedCommandMatches(staged: Pick<StagedRunAttachment, "targetIdentity" | "templateDigest" | "commandId">, command: DurableRecoveryCoreCommand): boolean {
|
||||
if (command.type !== "run.attach" || command.commandId !== staged.commandId) return false;
|
||||
const { paperclipNextAuthority, ...template } = command.payload;
|
||||
return isRecord(paperclipNextAuthority)
|
||||
&& canonicalJson(paperclipNextAuthority.identity) === canonicalJson(staged.targetIdentity)
|
||||
&& `sha256:${canonicalDigest(template)}` === staged.templateDigest;
|
||||
}
|
||||
|
||||
function stagedAttachmentTemplate(state: StoredCoreState): Record<string, unknown> | null {
|
||||
const staged = state.stagedRunAttachment;
|
||||
if (!staged) return null;
|
||||
if (staged.commandId === null) return staged.runAttachTemplate ?? null;
|
||||
const command = state.commands?.find(command => command.commandId === staged.commandId)
|
||||
?? state.warmTransition?.command ?? state.completedWarmTransition?.command;
|
||||
if (!command || !stagedCommandMatches(staged, command)) return null;
|
||||
const { paperclipNextAuthority: _authority, ...template } = command.payload;
|
||||
return template;
|
||||
}
|
||||
|
||||
function stagedTargetActivated(state: StoredCoreState): boolean {
|
||||
const staged = state.stagedRunAttachment, completed = state.completedWarmTransition;
|
||||
return !!staged && !state.warmTransition && !!completed
|
||||
&& canonicalJson(state.identity) === canonicalJson(staged.targetIdentity)
|
||||
&& completed.receipt.commandId === staged.commandId
|
||||
&& canonicalJson(completed.receipt.oldIdentity) === canonicalJson(staged.sourceIdentity)
|
||||
&& canonicalJson(completed.receipt.newIdentity) === canonicalJson(staged.targetIdentity)
|
||||
&& stagedCommandMatches(staged, completed.command);
|
||||
}
|
||||
|
||||
function validStagedRunAttachment(state: StoredCoreState, headerOnly = false): boolean {
|
||||
const staged = state.stagedRunAttachment;
|
||||
if (staged === undefined) return state.schema !== stagedCoreStateSchema;
|
||||
if (!(state.schema === stagedCoreStateSchema && isRecord(staged)
|
||||
&& Object.keys(staged).sort().join(",") === (headerOnly || staged.commandId !== null ? "commandId,sourceIdentity,targetIdentity,templateDigest" : "commandId,runAttachTemplate,sourceIdentity,targetIdentity,templateDigest")
|
||||
&& exactIdentity(staged.sourceIdentity) && exactIdentity(staged.targetIdentity)
|
||||
&& staged.sourceIdentity.runId !== staged.targetIdentity.runId
|
||||
&& ["runnerInstanceId", "environmentLeaseId", "normalizedSessionId"].every(key =>
|
||||
staged.sourceIdentity[key as keyof DurableRecoveryIdentity] === staged.targetIdentity[key as keyof DurableRecoveryIdentity])
|
||||
&& /^sha256:[0-9a-f]{64}$/.test(staged.templateDigest)
|
||||
&& (headerOnly || staged.commandId !== null || (isRecord(staged.runAttachTemplate) && isRecord(staged.runAttachTemplate.provider)
|
||||
&& staged.runAttachTemplate.paperclipNextAuthority === undefined
|
||||
&& `sha256:${canonicalDigest(staged.runAttachTemplate)}` === staged.templateDigest
|
||||
&& Buffer.byteLength(JSON.stringify(staged.runAttachTemplate)) <= maxCommandBytes))
|
||||
&& (staged.commandId === null || (typeof staged.commandId === "string" && stableIdPattern.test(staged.commandId))))) return false;
|
||||
const sourceCurrent = canonicalJson(state.identity) === canonicalJson(staged.sourceIdentity);
|
||||
if (!sourceCurrent && canonicalJson(state.identity) !== canonicalJson(staged.targetIdentity)) return false;
|
||||
if (state.warmTransition) {
|
||||
const transition = state.warmTransition;
|
||||
return staged.commandId !== null && stagedCommandMatches(staged, transition.command)
|
||||
&& canonicalJson(transition.receipt.oldIdentity) === canonicalJson(staged.sourceIdentity)
|
||||
&& canonicalJson(transition.receipt.newIdentity) === canonicalJson(staged.targetIdentity)
|
||||
&& (sourceCurrent ? transition.phase !== "activated" : transition.phase === "activated");
|
||||
}
|
||||
if (!sourceCurrent) return stagedTargetActivated(state);
|
||||
if (staged.commandId === null) return true;
|
||||
// Header-only inspection cannot certify the separate command record. Full
|
||||
// restoration below requires its exact tuple, payload and command ID.
|
||||
return !Array.isArray(state.commands) || state.commands.some(command => stagedCommandMatches(staged, command));
|
||||
}
|
||||
|
||||
/** Inspection is evidence only; the caller still owns recovery authorization. */
|
||||
export function inspectStagedRunAttachment(state: unknown, expectedTarget?: DurableRecoveryIdentity): StagedRunAttachment | null {
|
||||
try {
|
||||
if (!isRecord(state) || !exactIdentity(state.identity) || !isStoredCoreState(state, state.identity)
|
||||
|| !state.stagedRunAttachment || (expectedTarget && canonicalJson(state.stagedRunAttachment.targetIdentity) !== canonicalJson(expectedTarget))) return null;
|
||||
const template = stagedAttachmentTemplate(state);
|
||||
return template ? { ...structuredClone(state.stagedRunAttachment), runAttachTemplate: structuredClone(template) } : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function isStoredCoreState(
|
||||
value: unknown,
|
||||
identity: DurableRecoveryIdentity,
|
||||
@@ -811,7 +932,7 @@ function isStoredCoreState(
|
||||
const events = value.committedEvents;
|
||||
if (
|
||||
(value.schema !== coreStateSchema &&
|
||||
value.schema !== transitionCoreStateSchema) ||
|
||||
value.schema !== transitionCoreStateSchema && value.schema !== stagedCoreStateSchema) ||
|
||||
canonicalJson(value.identity) !== canonicalJson(identity) ||
|
||||
(value.checkpointEventPolicy !== undefined && value.checkpointEventPolicy !== "control-v1") ||
|
||||
!isRecord(value.tickets) ||
|
||||
@@ -828,6 +949,7 @@ function isStoredCoreState(
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!validStagedRunAttachment(value as unknown as StoredCoreState)) return false;
|
||||
if (!validStoredWarmTransition(value as unknown as StoredCoreState))
|
||||
return false;
|
||||
if (value.completedWarmTransition !== undefined) {
|
||||
@@ -1758,6 +1880,7 @@ class AuthorityConnection {
|
||||
/** Authenticated, replay-safe PRP transport authority. Business operations are caller supplied. */
|
||||
export class DurablePrpControlPlane {
|
||||
#identity: DurableRecoveryIdentity;
|
||||
#stagedTargetCheckpointPublished: string | null = null;
|
||||
readonly #store: DurableCoreStore;
|
||||
#expectedRunnerVersion: string;
|
||||
#expectedRunnerDigest: string;
|
||||
@@ -1854,7 +1977,12 @@ export class DurablePrpControlPlane {
|
||||
cached.set(part, prior?.record === record ? prior : { record, command: JSON.parse(record) });
|
||||
}
|
||||
this.#publicationCommands = cached;
|
||||
return { ...header.state, ...cursor, commands: [...cached.values()].map(value => value.command),
|
||||
const descriptor = header.state.stagedRunAttachment;
|
||||
const workingStage = this.#store.state.stagedRunAttachment;
|
||||
const staged = descriptor?.commandId === null ? { ...descriptor,
|
||||
runAttachTemplate: workingStage?.templateDigest === descriptor.templateDigest && workingStage.runAttachTemplate
|
||||
? workingStage.runAttachTemplate : JSON.parse(snapshot.records.get("staged-attachment-template")!) as Record<string, unknown> } : descriptor;
|
||||
return { ...header.state, ...(staged ? { stagedRunAttachment: staged } : {}), ...cursor, commands: [...cached.values()].map(value => value.command),
|
||||
// Event records are immutable after insertion. Retain their array, not
|
||||
// another deep copy of the streaming payload window.
|
||||
committedEvents: this.#store.state.committedEvents };
|
||||
@@ -2323,6 +2451,23 @@ export class DurablePrpControlPlane {
|
||||
return ticket;
|
||||
}
|
||||
|
||||
/** Import exact durable handoff evidence for a caller-verified surviving participant; issues no credential. */
|
||||
prepareWarmTransitionAdoption(input: { transitionId: string; runnerState: Record<string, unknown> }): void {
|
||||
this.#store.assertWritable();
|
||||
const pending = input.runnerState.warmTransition;
|
||||
const proof = warmTransitionRecoveryProof({ controlPlaneState: this.#store.state, runnerState: input.runnerState,
|
||||
expectedNewIdentity: (isRecord(pending) && isRecord(pending.receipt) ? pending.receipt.newIdentity : null) as DurableRecoveryIdentity,
|
||||
expectedRunnerVersion: this.#expectedRunnerVersion, expectedRunnerDigest: this.#expectedRunnerDigest });
|
||||
if (!proof || proof.transition.receipt.transitionId !== input.transitionId || !this.#store.state.stagedRunAttachment) {
|
||||
throw new Error("Warm transition adoption snapshot proof is not authorized.");
|
||||
}
|
||||
if (this.#store.state.warmTransition && canonicalJson(this.#store.state.warmTransition) === canonicalJson(proof.transition)) return;
|
||||
const candidate = structuredClone(this.#store.state);
|
||||
candidate.schema = stagedCoreStateSchema;
|
||||
candidate.warmTransition = structuredClone(proof.transition);
|
||||
this.#store.commit(candidate);
|
||||
}
|
||||
|
||||
/** Caller-owned recovery admission is required; a receipt is not a credential. */
|
||||
issueWarmTransitionBootstrapTicket(
|
||||
input: {
|
||||
@@ -2361,7 +2506,7 @@ export class DurablePrpControlPlane {
|
||||
original.expiresAtUnixMs,
|
||||
);
|
||||
const candidate = structuredClone(this.#store.state);
|
||||
candidate.schema = transitionCoreStateSchema;
|
||||
candidate.schema = candidate.stagedRunAttachment ? stagedCoreStateSchema : transitionCoreStateSchema;
|
||||
candidate.warmTransition = structuredClone(transition);
|
||||
candidate.tickets[material.credentialId] = {
|
||||
recordId: `bootstrap_ticket_${randomUUID()}`,
|
||||
@@ -2380,6 +2525,36 @@ export class DurablePrpControlPlane {
|
||||
return ticket;
|
||||
}
|
||||
|
||||
/** Caller confirms its existing session-checkpoint transaction committed. No journal write. */
|
||||
markStagedTargetCheckpointPublished(identity: DurableRecoveryIdentity): boolean {
|
||||
if (!stagedTargetActivated(this.#store.state) || canonicalJson(identity) !== canonicalJson(this.#store.state.identity)) return false;
|
||||
this.#stagedTargetCheckpointPublished = canonicalJson(identity);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Durable intent precedes source restoration; it is never a dispatched command. */
|
||||
stageRunAttachment(targetIdentity: DurableRecoveryIdentity, runAttachTemplate: Record<string, unknown>): void {
|
||||
this.#store.assertWritable();
|
||||
const existing = this.#store.state.stagedRunAttachment;
|
||||
if (existing && !(stagedTargetActivated(this.#store.state)
|
||||
&& this.#stagedTargetCheckpointPublished === canonicalJson(this.#store.state.identity)
|
||||
&& targetIdentity.runId !== existing.targetIdentity.runId)) {
|
||||
if (!validStagedRunAttachment(this.#store.state)
|
||||
|| canonicalJson(existing.targetIdentity) !== canonicalJson(targetIdentity)
|
||||
|| canonicalJson(stagedAttachmentTemplate(this.#store.state)) !== canonicalJson(runAttachTemplate)) {
|
||||
throw new Error("Staged run attachment conflicts with its durable intent.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const candidate = structuredClone(this.#store.state);
|
||||
candidate.schema = stagedCoreStateSchema;
|
||||
candidate.stagedRunAttachment = { sourceIdentity: structuredClone(candidate.identity),
|
||||
targetIdentity: structuredClone(targetIdentity), runAttachTemplate: structuredClone(runAttachTemplate),
|
||||
templateDigest: `sha256:${canonicalDigest(runAttachTemplate)}`, commandId: null };
|
||||
if (!validStagedRunAttachment(candidate)) throw new Error("Staged run attachment binding is invalid.");
|
||||
this.#store.commit(candidate);
|
||||
}
|
||||
|
||||
queueCommand(
|
||||
type: string,
|
||||
payload: Record<string, unknown> = {},
|
||||
@@ -2387,6 +2562,21 @@ export class DurablePrpControlPlane {
|
||||
deliverImmediately = false,
|
||||
): DurableRecoveryCoreCommand {
|
||||
this.#store.assertWritable();
|
||||
const staged = this.#store.state.stagedRunAttachment;
|
||||
const retireStaged = stagedTargetActivated(this.#store.state) && (type === "turn.start"
|
||||
|| (this.#stagedTargetCheckpointPublished === canonicalJson(this.#store.state.identity)
|
||||
&& !["runner.drain", "session.snapshot", "turn.stop", "runner.suspend", "session.close"].includes(type)));
|
||||
if (staged && !retireStaged) {
|
||||
if (type === "run.attach") {
|
||||
const { paperclipNextAuthority, ...template } = payload;
|
||||
if (!isRecord(paperclipNextAuthority)
|
||||
|| canonicalJson(paperclipNextAuthority.identity) !== canonicalJson(staged.targetIdentity)
|
||||
|| `sha256:${canonicalDigest(template)}` !== staged.templateDigest
|
||||
|| (staged.commandId !== null && staged.commandId !== commandId)) throw new Error("Staged run attachment command conflicts.");
|
||||
} else if (!stagedTargetActivated(this.#store.state) && !["runner.drain", "session.open", "session.snapshot", "turn.stop", "runner.suspend", "session.close", "semantic_tool.result"].includes(type)) {
|
||||
throw new Error("Staged run attachment permits recovery maintenance only.");
|
||||
}
|
||||
}
|
||||
const transition = this.#store.state.warmTransition;
|
||||
if (transition && transition.phase !== "activated") {
|
||||
if (
|
||||
@@ -2461,6 +2651,16 @@ export class DurablePrpControlPlane {
|
||||
if (Buffer.byteLength(JSON.stringify(command)) > maxCommandBytes)
|
||||
throw new CommandJournalLimitError("command_payload_too_large");
|
||||
this.#store.state.commands.push(command);
|
||||
if (staged && !retireStaged && type === "run.attach") {
|
||||
const { runAttachTemplate: _template, ...descriptor } = staged;
|
||||
this.#store.state.stagedRunAttachment = { ...descriptor, commandId: command.commandId };
|
||||
} else if (retireStaged) {
|
||||
// Native runtime persists the recovered session before its first turn.
|
||||
// Read/goal controls during opening must not retire startup evidence.
|
||||
delete this.#store.state.stagedRunAttachment;
|
||||
this.#store.state.schema = coreStateSchema;
|
||||
this.#stagedTargetCheckpointPublished = null;
|
||||
}
|
||||
this.#store.save();
|
||||
void this.flushCheckpoint().catch(() => undefined);
|
||||
if (deliverImmediately) {
|
||||
@@ -2677,7 +2877,7 @@ export class DurablePrpControlPlane {
|
||||
}
|
||||
if (transition) {
|
||||
const candidate = structuredClone(this.#store.state);
|
||||
candidate.schema = coreStateSchema;
|
||||
candidate.schema = candidate.stagedRunAttachment ? stagedCoreStateSchema : coreStateSchema;
|
||||
candidate.leases[transition.credentialId]!.identity = structuredClone(
|
||||
receipt.newIdentity,
|
||||
);
|
||||
@@ -3205,8 +3405,9 @@ export class DurablePrpControlPlane {
|
||||
}
|
||||
if (transition.phase === "prepared") {
|
||||
const candidate = initialCoreState(transition.receipt.newIdentity);
|
||||
candidate.stagedRunAttachment = structuredClone(this.#store.state.stagedRunAttachment);
|
||||
candidate.preparedLaunchConfigurations = structuredClone(this.#store.state.preparedLaunchConfigurations);
|
||||
candidate.schema = transitionCoreStateSchema;
|
||||
candidate.schema = candidate.stagedRunAttachment ? stagedCoreStateSchema : transitionCoreStateSchema;
|
||||
candidate.warmTransition = {
|
||||
...structuredClone(transition),
|
||||
phase: "activated",
|
||||
@@ -3507,7 +3708,7 @@ export class DurablePrpControlPlane {
|
||||
)!;
|
||||
completed.status = "completed";
|
||||
completed.result = structuredClone(result);
|
||||
candidate.schema = transitionCoreStateSchema;
|
||||
candidate.schema = candidate.stagedRunAttachment ? stagedCoreStateSchema : transitionCoreStateSchema;
|
||||
candidate.warmTransition = {
|
||||
receipt,
|
||||
phase: "prepared",
|
||||
|
||||
@@ -36,6 +36,10 @@ export interface RunnerExecutionDriver {
|
||||
checkpoint?: (settlement: "settled" | "unsettled") => Promise<void> | void;
|
||||
release: (reason?: "detach") => Promise<void> | void;
|
||||
}>;
|
||||
/** Register source and target checkpoint ownership before staging changes the stored run. */
|
||||
onStagedRunAttachment?: (authority: DurablePrpControlPlane, proof: {
|
||||
sourceIdentity: DurableRecoveryIdentity; targetIdentity: DurableRecoveryIdentity; templateDigest: string;
|
||||
}) => Promise<void>;
|
||||
/** Override process ownership; omission uses the SDK's local process owner. */
|
||||
runnerProcessLauncher?: (
|
||||
spec: RunnerProcessLaunchSpec,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { redactPaperclipSemanticValue } from "../../semantic-tools/redaction.js";
|
||||
import { safeAcpxLocations } from "./safe-locations.js";
|
||||
import { lstatSync, realpathSync } from "node:fs";
|
||||
import { basename, dirname, isAbsolute, join } from "node:path";
|
||||
|
||||
const record = (value: unknown): Record<string, unknown> =>
|
||||
value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
|
||||
function aliasedLocation(path: string, cwd: string): string | undefined {
|
||||
if (!isAbsolute(path)) return;
|
||||
try {
|
||||
const canonicalCwd = realpathSync(cwd);
|
||||
let canonicalPath: string;
|
||||
try {
|
||||
lstatSync(path);
|
||||
canonicalPath = realpathSync(path);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return;
|
||||
try { lstatSync(path); return; } catch (missing) {
|
||||
if ((missing as NodeJS.ErrnoException).code !== "ENOENT") return;
|
||||
}
|
||||
canonicalPath = join(realpathSync(dirname(path)), basename(path));
|
||||
}
|
||||
return safeAcpxLocations([{ path: canonicalPath }], canonicalCwd, "edit")[0]?.path as string | undefined;
|
||||
} catch { return; }
|
||||
}
|
||||
|
||||
export function safeAcpxEditTarget(value: unknown, workingDirectory: string | undefined): string | undefined {
|
||||
const call = record(value);
|
||||
if (call.kind !== "edit" || !workingDirectory) return;
|
||||
const input = record(call.rawInput);
|
||||
if (call.locations !== undefined && !Array.isArray(call.locations)) return;
|
||||
if (Array.isArray(call.locations) && (call.locations.length > 16 || call.locations.some(x => typeof record(x).path !== "string"))) return;
|
||||
const paths = [input.path, input.fileName, ...(Array.isArray(call.locations) ? call.locations.map(x => record(x).path) : [])].filter(x => x !== undefined);
|
||||
if (!paths.length || paths.some(x => typeof x !== "string" || !x || x.trim() !== x || x.length > 2048 || /[\\\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(x))) return;
|
||||
const safe = paths.flatMap(path => {
|
||||
const locations = safeAcpxLocations([{ path }], workingDirectory, call.kind, call.title);
|
||||
if (locations.length) return locations;
|
||||
const aliased = aliasedLocation(path as string, workingDirectory);
|
||||
return aliased ? [{ path: aliased }] : [];
|
||||
});
|
||||
const names = safe.map(x => x.path).filter((x): x is string => typeof x === "string" && x.length <= 1024 && x.trim() === x && !x.includes(":"));
|
||||
if (names.length !== paths.length || new Set(names).size !== 1) return;
|
||||
const target = names[0];
|
||||
return target && redactPaperclipSemanticValue(target) === target ? target : undefined;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ function request(kinds = ["allow_once", "allow_always", "reject_once"]): AcpPerm
|
||||
} } as AcpPermissionRequest;
|
||||
}
|
||||
describe("ACP permission normalization", () => {
|
||||
it.each([undefined, "codex", "claude", "pi", "copilot"])("preserves existing %s native identity behavior", provider => {
|
||||
it.each([undefined, "codex", "claude", "pi", "copilot", "kimi"])("preserves existing %s native identity behavior", provider => {
|
||||
const native = request(); native.raw.toolCall.toolCallId = "native\u0000tool";
|
||||
expect(normalizeAcpxPermission(native, { provider }).toolCallId).toBe("native\u0000tool");
|
||||
});
|
||||
@@ -93,6 +93,42 @@ describe("ACP permission normalization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kimi canonical file permission context", () => {
|
||||
const context = { provider: "kimi", workingDirectory: "/fixture/workspace", allowAlwaysScope: "session" as const };
|
||||
function write(call: Record<string, unknown> = {}) {
|
||||
const value = request();
|
||||
Object.assign(value.raw.toolCall, { toolCallId: "kimi-write-native-id", kind: "edit", title: "Write", rawInput: { path: "/fixture/workspace/nested/result.txt", content: "PRIVATE_KIMI_CONTENT" }, locations: [{ path: "nested/result.txt" }], ...call });
|
||||
return value;
|
||||
}
|
||||
it("shows the native Write target and preserves its exact operation and decision identity", () => {
|
||||
const normalized = normalizeAcpxPermission(write(), context);
|
||||
expect(normalized.title).toBe("Change file: nested/result.txt");
|
||||
expect(normalized.toolCallId).toBe("kimi-write-native-id");
|
||||
expect(JSON.stringify(normalized)).not.toContain("PRIVATE_KIMI_CONTENT");
|
||||
expect(normalized.resolve({ action: "accept" })).toEqual({ outcome: "allow_once" });
|
||||
expect(normalized.resolve({ action: "accept_for_session" })).toEqual({ outcome: "allow_always" });
|
||||
expect(normalized.resolve({ action: "decline" })).toEqual({ outcome: "reject_once" });
|
||||
});
|
||||
it.each([
|
||||
{ name: "unknown", rawInput: {}, locations: [] },
|
||||
{ name: "conflicting", rawInput: { path: "one.txt" }, locations: [{ path: "two.txt" }] },
|
||||
{ name: "outside", rawInput: { path: "/outside/result.txt" }, locations: [] },
|
||||
])("offers only denial or cancellation for a $name Write target", ({ rawInput, locations }) => {
|
||||
const normalized = normalizeAcpxPermission(write({ rawInput, locations }), context);
|
||||
expect(normalized.title).toContain("target unavailable");
|
||||
expect(normalized.toolCallId).toBe("kimi-write-native-id");
|
||||
expect(normalized.choices.map(choice => choice.key)).toEqual(["decline", "cancel"]);
|
||||
expect(normalized.resolve({ action: "decline" })).toEqual({ outcome: "reject_once" });
|
||||
expect(normalized.resolve({ action: "cancel" })).toEqual({ outcome: "cancel" });
|
||||
for (const action of ["accept", "accept_for_session"] as const) expect(() => normalized.resolve({ action })).toThrow("not an offered choice");
|
||||
});
|
||||
it("requires a bound workspace before allowing a native Write", () => {
|
||||
const normalized = normalizeAcpxPermission(write(), { provider: "kimi" });
|
||||
expect(normalized.choices.map(choice => choice.key)).toEqual(["decline", "cancel"]);
|
||||
expect(() => normalized.resolve({ action: "accept" })).toThrow("not an offered choice");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Copilot canonical file permission context", () => {
|
||||
const context = { provider: "copilot", workingDirectory: "/fixture/workspace", allowAlwaysScope: "session" as const };
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cursorToolIdentity } from "./cursor-plan-tool-identity.js";
|
||||
|
||||
|
||||
|
||||
import { safeCopilotEditTarget } from "./copilot-permission-context.js";
|
||||
import { safeAcpxEditTarget } from "./acp-edit-context.js";
|
||||
|
||||
export type AcpxPermissionAction = "accept" | "accept_for_session" | "decline" | "cancel";
|
||||
export interface NormalizedAcpxPermission {
|
||||
@@ -39,8 +39,8 @@ export function normalizeAcpxPermission(request: AcpPermissionRequest, options:
|
||||
|| (options.provider === "cursor" && !call.toolCallId.trim())) {
|
||||
throw new Error("ACP permission request omitted its tool identity");
|
||||
}
|
||||
const needsEditContext = options.provider === "copilot" && call.kind === "edit";
|
||||
const target = needsEditContext ? safeCopilotEditTarget(call, options.workingDirectory) : undefined;
|
||||
const needsEditContext = (options.provider === "copilot" || options.provider === "kimi") && call.kind === "edit";
|
||||
const target = needsEditContext ? safeAcpxEditTarget(call, options.workingDirectory) : undefined;
|
||||
const bindings = new Map<AcpxPermissionAction, AcpPermissionDecision["outcome"]>();
|
||||
const choices: NormalizedAcpxPermission["choices"] = [];
|
||||
for (const [key, kind, label] of [
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "./kimi-closure-pins.js";
|
||||
import { ANTIGRAVITY_DISTRIBUTION_PINS } from "./generated-profiles.js";
|
||||
import { assertAcpxProfileReleased, resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
|
||||
import { assertAcpxExecutionAdmission, bindAcpxCandidateAdmission, parseAcpxCandidateAdmission, parseAcpxCandidateStartupAdmission, type AcpxCandidateAdmission } from "./candidate-admission.js";
|
||||
import historicalKimi from "../../../test/fixtures/kimi-profile-v16-identity.json" with { type: "json" };
|
||||
import { verifyAcpxProfileInstallation } from "./profile-installation.js";
|
||||
import { AcpxRuntimeHost } from "./runtime-host.js";
|
||||
import * as integrity from "./installation-integrity.js";
|
||||
@@ -95,3 +97,73 @@ describe("candidate installation remains cryptographically verified", () => {
|
||||
expect(openRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("trusted Kimi candidate admission", () => {
|
||||
const kimiExpected = { companyId: "company-kimi", agent: "kimi", model: "kimi-for-coding" };
|
||||
const kimiCandidate = (target: "darwin-arm64" | "linux-x64"): AcpxCandidateAdmission => ({
|
||||
schema: "paperclip.acpx-candidate-admission.v1", ...kimiExpected, agent: "kimi",
|
||||
profileDigest: resolveQualifiedAcpxProfile("kimi", kimiExpected.model).commandDigest,
|
||||
nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[target]}`,
|
||||
platform: target === "darwin-arm64" ? "darwin" : "linux",
|
||||
architecture: target === "darwin-arm64" ? "arm64" : "x64", expiresAt: now + 60_000,
|
||||
});
|
||||
|
||||
it.each(["darwin-arm64", "linux-x64"] as const)("binds only the current pending Kimi profile and exact %s closure", target => {
|
||||
const profile = resolveQualifiedAcpxProfile("kimi", kimiExpected.model);
|
||||
if (profile.qualificationStatus === "pending") {
|
||||
expect(parseAcpxCandidateAdmission(kimiCandidate(target), kimiExpected, now)).toEqual(kimiCandidate(target));
|
||||
expect(() => assertAcpxProfileReleased("kimi")).toThrow("qualification is pending");
|
||||
expect(() => assertAcpxExecutionAdmission(profile)).toThrow("qualification is pending");
|
||||
} else {
|
||||
expect(profile.agentProfileVersion).toBe(17);
|
||||
expect(() => assertAcpxProfileReleased("kimi")).not.toThrow();
|
||||
expect(() => assertAcpxExecutionAdmission(profile)).not.toThrow();
|
||||
expect(() => parseAcpxCandidateAdmission(kimiCandidate(target), kimiExpected, now)).toThrow();
|
||||
expect(() => parseAcpxCandidateAdmission({ ...kimiCandidate(target), profileDigest: historicalKimi.commandDigest }, kimiExpected, now)).toThrow();
|
||||
}
|
||||
const other = target === "darwin-arm64" ? "linux-x64" : "darwin-arm64";
|
||||
for (const change of [
|
||||
{ companyId: "foreign-company" }, { agent: "antigravity" }, { model: "another-model" },
|
||||
{ model: "auto" }, { model: "default" }, { model: " kimi-for-coding" },
|
||||
{ profileDigest: `sha256:${"0".repeat(64)}` },
|
||||
{ nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[other]}` },
|
||||
{ platform: "linux", architecture: "arm64" }, { platform: "darwin", architecture: "x64" },
|
||||
{ expiresAt: now }, { expiresAt: now + 86_400_001 }, { expiresAt: "1800000060000" },
|
||||
{ unknown: true }, { runId: "injected-run" },
|
||||
]) expect(() => parseAcpxCandidateAdmission({ ...kimiCandidate(target), ...change }, kimiExpected, now)).toThrow(expect.objectContaining({ code: "ACPX_CANDIDATE_ADMISSION_INVALID" }));
|
||||
});
|
||||
|
||||
it.each(["darwin-arm64", "linux-x64"] as const)("requires the exact actual host and startup identity for %s", target => {
|
||||
const profile = resolveQualifiedAcpxProfile("kimi", kimiExpected.model);
|
||||
const source = { ...kimiCandidate(target), expiresAt: Date.now() + 60_000 };
|
||||
const identity = { runId: "kimi-run", normalizedSessionId: "kimi-session" };
|
||||
const startup = profile.qualificationStatus === "pending" ? bindAcpxCandidateAdmission(source, identity) : { ...source, ...identity };
|
||||
if (profile.qualificationStatus !== "pending") expect(() => bindAcpxCandidateAdmission(source, identity)).toThrow();
|
||||
const platform = Object.getOwnPropertyDescriptor(process, "platform")!;
|
||||
const architecture = Object.getOwnPropertyDescriptor(process, "arch")!;
|
||||
try {
|
||||
Object.defineProperty(process, "platform", { ...platform, value: startup.platform });
|
||||
Object.defineProperty(process, "arch", { ...architecture, value: startup.architecture });
|
||||
if (profile.qualificationStatus === "pending") expect(() => assertAcpxExecutionAdmission(profile, startup)).not.toThrow();
|
||||
else {
|
||||
expect(() => assertAcpxExecutionAdmission(profile)).not.toThrow();
|
||||
expect(() => assertAcpxExecutionAdmission(profile, startup)).toThrow();
|
||||
}
|
||||
for (const identity of [{ runId: "foreign-run" }, { normalizedSessionId: "foreign-session" }, { model: "foreign-model" }]) {
|
||||
expect(() => parseAcpxCandidateStartupAdmission(startup, { ...kimiExpected, runId: "kimi-run", normalizedSessionId: "kimi-session", ...identity })).toThrow();
|
||||
}
|
||||
expect(() => assertAcpxExecutionAdmission({ ...profile, commandDigest: `sha256:${"f".repeat(64)}` }, startup)).toThrow();
|
||||
expect(() => parseAcpxCandidateStartupAdmission(startup, kimiExpected, startup.expiresAt)).toThrow();
|
||||
for (const [os, arch] of [["win32", "x64"], ["linux", "arm64"], ["darwin", "x64"],
|
||||
target === "darwin-arm64" ? ["linux", "x64"] : ["darwin", "arm64"]]) {
|
||||
Object.defineProperty(process, "platform", { ...platform, value: os });
|
||||
Object.defineProperty(process, "arch", { ...architecture, value: arch });
|
||||
expect(() => assertAcpxExecutionAdmission(profile, startup)).toThrow();
|
||||
}
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", platform);
|
||||
Object.defineProperty(process, "arch", architecture);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "./kimi-closure-pins.js";
|
||||
import { ANTIGRAVITY_DISTRIBUTION_PINS } from "./generated-profiles.js";
|
||||
import { QUALIFIED_ACPX_PROFILES, assertAcpxProfileReleased, type QualifiedAcpxProfile } from "./qualified-profiles.js";
|
||||
|
||||
@@ -5,12 +6,12 @@ import { QUALIFIED_ACPX_PROFILES, assertAcpxProfileReleased, type QualifiedAcpxP
|
||||
export interface AcpxCandidateAdmission {
|
||||
schema: "paperclip.acpx-candidate-admission.v1";
|
||||
companyId: string;
|
||||
agent: "antigravity";
|
||||
agent: "antigravity" | "kimi";
|
||||
model: string;
|
||||
profileDigest: string;
|
||||
nativeClosureDigest: string;
|
||||
platform: "darwin";
|
||||
architecture: "arm64";
|
||||
platform: "darwin" | "linux";
|
||||
architecture: "arm64" | "x64";
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
@@ -31,16 +32,23 @@ export function parseAcpxCandidateAdmission(
|
||||
): AcpxCandidateAdmission {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) return invalid();
|
||||
const entry = value as Record<string, unknown>;
|
||||
const profile = entry.agent === "antigravity" ? QUALIFIED_ACPX_PROFILES.antigravity
|
||||
: entry.agent === "kimi" ? QUALIFIED_ACPX_PROFILES.kimi : undefined;
|
||||
const target = `${entry.platform}-${entry.architecture}`;
|
||||
const closure = entry.agent === "antigravity" && target === "darwin-arm64"
|
||||
? ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256
|
||||
: entry.agent === "kimi" && (target === "darwin-arm64" || target === "linux-x64")
|
||||
? KIMI_DISTRIBUTION_CLOSURE_SHA256[target] : undefined;
|
||||
if (!Number.isSafeInteger(now) || now < 0 || Object.keys(entry).length !== BASE_FIELDS.length || Object.keys(entry).some(key => !BASE_FIELDS.includes(key))
|
||||
|| entry.schema !== "paperclip.acpx-candidate-admission.v1"
|
||||
|| !stableId(entry.companyId) || entry.companyId !== expected.companyId
|
||||
|| entry.agent !== "antigravity" || entry.agent !== expected.agent
|
||||
|| !profile || entry.agent !== expected.agent
|
||||
|| entry.agent === "kimi" && profile.qualificationStatus !== "pending"
|
||||
|| typeof entry.model !== "string" || entry.model !== expected.model || !entry.model.trim()
|
||||
|| entry.model !== entry.model.trim() || Buffer.byteLength(entry.model) > 512 || /[\u0000-\u001f\u007f]/.test(entry.model)
|
||||
|| ["auto", "default"].includes(entry.model.toLowerCase())
|
||||
|| entry.profileDigest !== QUALIFIED_ACPX_PROFILES.antigravity.commandDigest
|
||||
|| entry.nativeClosureDigest !== `sha256:${ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256}`
|
||||
|| entry.platform !== "darwin" || entry.architecture !== "arm64"
|
||||
|| entry.profileDigest !== profile.commandDigest
|
||||
|| !closure || entry.nativeClosureDigest !== `sha256:${closure}`
|
||||
|| !Number.isSafeInteger(entry.expiresAt) || Number(entry.expiresAt) <= now
|
||||
|| Number(entry.expiresAt) > now + 24 * 60 * 60 * 1000) return invalid();
|
||||
return { ...entry } as unknown as AcpxCandidateAdmission;
|
||||
@@ -74,7 +82,7 @@ export function parseAcpxCandidateStartupAdmission(
|
||||
/** This exception is deliberately absent from direct driver/config admission. */
|
||||
export function assertAcpxExecutionAdmission(profile: QualifiedAcpxProfile, candidate?: AcpxCandidateStartupAdmission): void {
|
||||
if (candidate === undefined) return assertAcpxProfileReleased(profile.agent);
|
||||
if (process.platform !== "darwin" || process.arch !== "arm64") return invalid();
|
||||
parseAcpxCandidateStartupAdmission(candidate, { agent: profile.agent, model: profile.qualificationModel });
|
||||
if (candidate.platform !== process.platform || candidate.architecture !== process.arch) return invalid();
|
||||
if (profile.commandDigest !== candidate.profileDigest) return invalid();
|
||||
}
|
||||
@@ -62,6 +62,12 @@ export const ACPX_CAPABILITY_PROFILES: Readonly<Record<QualifiedAcpxAgent, AcpxC
|
||||
artifacts: "references-pending", extensionRequests: [],
|
||||
extensionNotifications: ["github.com/copilot/sessionEvent"],
|
||||
},
|
||||
kimi: {
|
||||
displayName: "Kimi Code", qualification: "qualified", models: "explicit-provider-verified",
|
||||
permissions: "interactive", questions: "semantic-only", plans: "semantic-only", tools: "authenticated-mcp",
|
||||
recovery: "unverified", usage: "unverified", steering: "unsupported", followUp: "controller-queue",
|
||||
artifacts: "policy_disabled", extensionRequests: [], extensionNotifications: [],
|
||||
},
|
||||
pi: {
|
||||
displayName: "Pi", qualification: "qualified", models: "explicit-provider-verified",
|
||||
permissions: "interactive", questions: "form", plans: "semantic-only", tools: "owned-extension",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
|
||||
import { cursorToolIdentity } from "./cursor-plan-tool-identity.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { AcpxProviderProcessLostError } from "./codex-runtime-adapter.js";
|
||||
import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js";
|
||||
|
||||
import type { AcpRuntimeEvent } from "acpx/runtime";
|
||||
|
||||
@@ -209,6 +210,46 @@ describe("Codex ACPX harness driver", () => {
|
||||
await session.close({ reason: "native attribution verified" });
|
||||
});
|
||||
|
||||
it("enforces the current Kimi release gate before opening a direct runtime host", () => {
|
||||
const openHost = vi.fn();
|
||||
const construct = () => driverFixture({ agent: "kimi", model: "kimi-for-coding", providerPolicy: { readOnly: true } }, { openHost });
|
||||
if (QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending") {
|
||||
expect(construct).toThrow(expect.objectContaining({ code: "ACPX_PROFILE_QUALIFICATION_PENDING", retryable: false }));
|
||||
} else expect(construct).not.toThrow();
|
||||
expect(openHost).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.skipIf(QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending").each(["exact-final", "tool-only", "empty-final", "missing-end", "cancelled", "filtered", "paused", "max-tokens"])("qualified Kimi direct-driver step boundaries separate commentary from the actual final response: %s", async scenario => {
|
||||
const id = (n: number) => `kimi-message-${String(n).padStart(64, "0")}`;
|
||||
const chunk = (n: number, kind: string, text = "", stream: "output" | "thought" = "output"): AcpRuntimeEvent => ({
|
||||
type: "text_delta", text, stream, messageId: id(n), meta: { origin: "kimi-native-assistant", source: "kimi-turn-step-v1", kind },
|
||||
});
|
||||
const runtimeEvents = [chunk(1, "start"), chunk(1, "delta", "Calling finish."), chunk(1, "end:tool_use")];
|
||||
if (scenario !== "tool-only") {
|
||||
runtimeEvents.push(chunk(2, "start"), chunk(2, "delta", "private thought", "thought"));
|
||||
if (scenario !== "empty-final") runtimeEvents.push(chunk(2, "delta", "EXACT_"), chunk(2, "delta", "MARKER"));
|
||||
if (scenario !== "missing-end" && scenario !== "cancelled") runtimeEvents.push(chunk(2,
|
||||
`end:${scenario === "filtered" || scenario === "paused" ? scenario : scenario === "max-tokens" ? "max_tokens" : "end_turn"}`));
|
||||
}
|
||||
const fixture = driverFixture({ agent: "kimi", model: "kimi-for-coding", providerPolicy: { readOnly: true } }, { runtimeEvents });
|
||||
const session = await fixture.driver.openSession({ runId: "run-kimi-boundaries", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
const { turnId } = await session.startTurn({ message: { text: "Answer exactly" } });
|
||||
fixture.finishTurn({ status: scenario === "cancelled" ? "cancelled" : "completed" });
|
||||
const events = await collectUntil(session.events(), scenario === "missing-end" ? "turn.failed" : scenario === "cancelled" ? "turn.interrupted" : "turn.completed");
|
||||
const progress = events.find(event => event.eventType === "item.delta" && event.payload.text === "Calling finish.")!;
|
||||
expect(progress).toBeDefined();
|
||||
expect(progress.itemId).toBe(`${turnId}:assistant-message:${id(1)}`);
|
||||
const commentary = events.filter(event => event.eventType === "item.completed" && event.payload.kind === "agentMessage" && event.payload.channel === "commentary");
|
||||
expect(commentary.filter(event => event.payload.text === "Calling finish.")).toHaveLength(1);
|
||||
expect(commentary[0]!.itemId).toBe(progress.itemId);
|
||||
const final = events.filter(event => event.eventType === "item.completed" && event.payload.kind === "agentMessage" && event.payload.channel === "final");
|
||||
expect(final.map(event => event.payload.text)).toEqual(["exact-final", "max-tokens"].includes(scenario) ? ["EXACT_MARKER"] : []);
|
||||
if (final.length) expect(final[0]!.itemId).toBe(`${turnId}:assistant-message:${id(2)}`);
|
||||
expect(events.filter(event => event.payload.kind === "agentMessage").some(event => event.payload.text === "private thought")).toBe(false);
|
||||
expect(events.filter(event => event.eventType === "item.delta" && event.payload.text === "").length).toBeGreaterThanOrEqual(2);
|
||||
await session.close({ reason: "Kimi native final attribution verified" });
|
||||
});
|
||||
|
||||
it("delivers negotiated steering and follow-up distinctly, once, for the active turn", async () => {
|
||||
const fixture = driverFixture({ agent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", providerPolicy: { readOnly: true } }, { runtimeEvents: [] });
|
||||
fixture.host.steeringCapability.mockReturnValue({ steering: true, queuedFollowUp: true });
|
||||
|
||||
@@ -5,6 +5,7 @@ import { acpxProfileActivity, type AcpxActivityAdapter, type AcpxToolEvidence }
|
||||
|
||||
import { requireAcpxResponseDelivery } from "./response-delivery.js";
|
||||
import { createPiMessageProjection, piBoundaryClearsFinal, type PiProjectedMessageEvent } from "./pi-message-projection.js";
|
||||
import { createKimiMessageProjection, kimiMessageBoundaryClears, type KimiProjectedMessageEvent } from "./kimi-message-boundary.js";
|
||||
import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, validateAcpxRichEvent, createAcpxProfileExtensionAdapter, type AcpxExtensionInput } from "./profile-extensions.js";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
|
||||
@@ -290,7 +291,7 @@ export class CodexAcpxDriver implements HarnessDriver {
|
||||
dependencies: CodexAcpxDriverDependencies = {},
|
||||
) {
|
||||
assertAcpxProfileReleased(options.agent ?? "codex");
|
||||
if (["pi", "cursor", "copilot"].includes(options.agent ?? "codex") && typeof options.providerPolicy?.readOnly !== "boolean") {
|
||||
if (["pi", "cursor", "copilot", "kimi"].includes(options.agent ?? "codex") && typeof options.providerPolicy?.readOnly !== "boolean") {
|
||||
throw new Error("ACP candidate requires an explicit provider read-only policy");
|
||||
}
|
||||
this.#options = {
|
||||
@@ -330,7 +331,7 @@ export class CodexAcpxDriver implements HarnessDriver {
|
||||
},
|
||||
capabilities: {
|
||||
...descriptor.capabilities,
|
||||
resume: true,
|
||||
resume: descriptor.capabilities.resume,
|
||||
runtimeRequestResolution: true,
|
||||
runtimeRequestHandoff: true,
|
||||
unsupported: descriptor.capabilities.unsupported,
|
||||
@@ -1481,7 +1482,8 @@ class CodexAcpxSession implements HarnessSession {
|
||||
const normalizeToolEvent =
|
||||
createAcpxToolEventNormalizer<AcpRuntimeEvent>();
|
||||
const piMessages = this.#agent === "pi" ? createPiMessageProjection<AcpRuntimeEvent>() : null;
|
||||
const normalizeMessage = piMessages ? piMessages.normalize : this.#agent === "grok"
|
||||
const kimiMessages = this.#agent === "kimi" ? createKimiMessageProjection<AcpRuntimeEvent>() : null;
|
||||
const normalizeMessage = piMessages ? piMessages.normalize : kimiMessages ? kimiMessages.normalize : this.#agent === "grok"
|
||||
? createGrokMessageNormalizer<AcpRuntimeEvent>() : (event: AcpRuntimeEvent) => event;
|
||||
for await (const event of turn.events) {
|
||||
toolEvidence?.tool(event);
|
||||
@@ -1490,7 +1492,7 @@ class CodexAcpxSession implements HarnessSession {
|
||||
this.#mapRuntimeEvent(normalizeMessage(normalizeToolEvent(projected)), turnId, ++index);
|
||||
}
|
||||
const result = await turn.result;
|
||||
if (result.status === "completed") piMessages?.settle();
|
||||
if (result.status === "completed") { piMessages?.settle(); kimiMessages?.settle(); }
|
||||
await drainExtensions();
|
||||
const usageAfter = await readUsageStatus(this.#host);
|
||||
// Diagnostic projection failures cannot replace the provider's terminal result.
|
||||
@@ -1676,12 +1678,12 @@ class CodexAcpxSession implements HarnessSession {
|
||||
}
|
||||
|
||||
#assistantItemId(turnId: string, messageId: string | null): string {
|
||||
if (this.#agent !== "copilot") return `${turnId}:assistant-message`;
|
||||
if (this.#agent !== "copilot" && this.#agent !== "kimi") return `${turnId}:assistant-message`;
|
||||
return messageId ? `${turnId}:assistant-message:${messageId}` : `${turnId}:assistant-activity`;
|
||||
}
|
||||
|
||||
#mapRuntimeEvent(
|
||||
event: PiProjectedMessageEvent<AcpRuntimeEvent>,
|
||||
event: PiProjectedMessageEvent<KimiProjectedMessageEvent<AcpRuntimeEvent>>,
|
||||
turnId: string,
|
||||
index: number,
|
||||
): void {
|
||||
@@ -1691,10 +1693,15 @@ class CodexAcpxSession implements HarnessSession {
|
||||
const isReasoning =
|
||||
event.stream === "thought" || event.tag === "agent_thought_chunk";
|
||||
const messageId = typeof event.messageId === "string" && event.messageId ? event.messageId : null;
|
||||
let kimiCommentary: { text: string; itemId: string } | undefined;
|
||||
// The pinned Copilot mapper supplies actual native identity, including
|
||||
// empty starts. Unidentified session info/warnings remain activity only.
|
||||
if (!isReasoning && !event.piMessageHistory && (this.#agent !== "copilot" || messageId)) {
|
||||
if (piBoundaryClearsFinal(event)) this.#assistantText = "";
|
||||
if (!isReasoning && !event.piMessageHistory && !event.assistantMessageHistory && (this.#agent !== "copilot" || messageId)) {
|
||||
const kimiClears = this.#agent === "kimi" && kimiMessageBoundaryClears(event);
|
||||
if (kimiClears && this.#assistantText) {
|
||||
kimiCommentary = { text: this.#assistantText, itemId: this.#assistantItemId(turnId, this.#assistantMessageId) };
|
||||
}
|
||||
if (piBoundaryClearsFinal(event) || kimiClears) this.#assistantText = "";
|
||||
if (messageId && this.#assistantMessageId && messageId !== this.#assistantMessageId) {
|
||||
if (this.#agent === "copilot" && this.#assistantText) {
|
||||
this.#emit("item.completed", { kind: "agentMessage", channel: "commentary", text: this.#assistantText },
|
||||
@@ -1722,6 +1729,8 @@ class CodexAcpxSession implements HarnessSession {
|
||||
: this.#assistantItemId(turnId, messageId),
|
||||
},
|
||||
);
|
||||
if (kimiCommentary) this.#emit("item.completed", { kind: "agentMessage", channel: "commentary", text: kimiCommentary.text },
|
||||
{ turnId, itemId: kimiCommentary.itemId });
|
||||
}
|
||||
if (event.type === "status" && event.tag === "usage_update") {
|
||||
this.#usage = boundedRecord({
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { admitAcpxSessionContext } from "./session-context-admission.js";
|
||||
import { createKimiInstructionAdmission } from "./kimi-instructions.js";
|
||||
import { kimiReadOnlyPermissionAllowed } from "./kimi-config.js";
|
||||
import { resolveGrokDisableWebSearch } from "./grok-web-search.js";
|
||||
import { resolveGrokEffort, createGrokEffortAdmission } from "./grok-effort.js";
|
||||
import { resolveClaudeEffort, createClaudeEffortAdmission } from "./claude-effort.js";
|
||||
@@ -37,7 +40,6 @@ import { AcpxApprovalRequiredError, decideAcpxPermission } from "./permission-po
|
||||
import { ACPX_CAPABILITY_PROFILES } from "./capability-profiles.js";
|
||||
import { createCursorInstructionAdmission } from "./cursor-instructions.js";
|
||||
import { createAntigravityAdmission } from "./antigravity-admission.js";
|
||||
import { admitAcpxSessionContext } from "./session-context-admission.js";
|
||||
import { createAcpxModeBinding } from "./provider-mode.js";
|
||||
import { assertCopilotPromptPolicy, createCopilotProtocolGuard } from "./copilot-policy.js";
|
||||
|
||||
@@ -326,17 +328,19 @@ export async function openQualifiedAcpxRuntime(
|
||||
const grokDisableWebSearch = resolveGrokDisableWebSearch(options.profile.agent, options.grokDisableWebSearch);
|
||||
const effortAdmission = selectedClaudeEffort ? createClaudeEffortAdmission(selectedClaudeEffort)
|
||||
: selectedGrokEffort ? createGrokEffortAdmission(selectedGrokEffort, options.profile.reportedModelId) : undefined;
|
||||
const cursorInstructions = options.profile.agent === "cursor"
|
||||
const instructionAdmission = options.profile.agent === "kimi"
|
||||
? createKimiInstructionAdmission(options.systemInstructions)
|
||||
: options.profile.agent === "cursor"
|
||||
? createCursorInstructionAdmission(options.systemInstructions)
|
||||
: null;
|
||||
const antigravityAdmission = options.profile.agent === "antigravity"
|
||||
? createAntigravityAdmission(options.systemInstructions, options.profile.reportedModelId)
|
||||
: null;
|
||||
const contextAdmission = cursorInstructions ?? antigravityAdmission;
|
||||
const contextAdmission = instructionAdmission ?? antigravityAdmission;
|
||||
const runtimeOptions: GoalAwareAcpRuntimeOptions = {
|
||||
cwd: options.cwd,
|
||||
...(cursorInstructions || modeBinding ? { protocolGuardFactory: () => {
|
||||
const instructions = cursorInstructions?.createGuard();
|
||||
...(instructionAdmission || modeBinding ? { protocolGuardFactory: () => {
|
||||
const instructions = instructionAdmission?.createGuard();
|
||||
const mode = modeBinding?.createGuard();
|
||||
return (direction: "inbound" | "outbound", message: unknown) => { instructions?.(direction, message); mode?.(direction, message); };
|
||||
} } : {}),
|
||||
@@ -412,6 +416,8 @@ export async function openQualifiedAcpxRuntime(
|
||||
}
|
||||
if ((permissionBoundary.hasAdmittedTurn && !extensionBoundary.active)
|
||||
|| extensionBoundary.active?.signal.aborted || context.signal.aborted) return { outcome: "cancel" };
|
||||
if (options.profile.agent === "kimi" && options.readOnly === true
|
||||
&& !kimiReadOnlyPermissionAllowed(request, runnerOwnedMcpServerNames)) return { outcome: "reject_once" };
|
||||
const disposition = decideAcpxPermission(
|
||||
options.profile.agent,
|
||||
options.permissionMode,
|
||||
@@ -532,7 +538,7 @@ export async function openQualifiedAcpxRuntime(
|
||||
// Its supported exact-model control forces session/load and the
|
||||
// current instruction acknowledgement before host admission.
|
||||
if (!runtime.setConfigOption) {
|
||||
throw new Error("ACP context admission requires exact model configuration");
|
||||
throw new Error("Cold instruction admission requires exact model configuration");
|
||||
}
|
||||
await runtime.setConfigOption({
|
||||
handle: ensuredHandle, key: "model", value: options.profile.reportedModelId,
|
||||
|
||||
@@ -1,52 +1,2 @@
|
||||
import { redactPaperclipSemanticValue } from "../../semantic-tools/redaction.js";
|
||||
import { safeAcpxLocations } from "./safe-locations.js";
|
||||
import { lstatSync, realpathSync } from "node:fs";
|
||||
import { basename, dirname, isAbsolute, join } from "node:path";
|
||||
|
||||
export const COPILOT_PERMISSION_CONTEXT_CONTRACT = "copilot-edit-permission-context-v1";
|
||||
const record = (value: unknown): Record<string, unknown> =>
|
||||
value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
|
||||
/** Native providers can spell an absolute workspace path through a host alias
|
||||
* (macOS /tmp and /private/tmp). Attest the real entry, or its existing parent
|
||||
* for a new file, before converting it to a display target. */
|
||||
function aliasedLocation(path: string, cwd: string): string | undefined {
|
||||
if (!isAbsolute(path)) return;
|
||||
try {
|
||||
const canonicalCwd = realpathSync(cwd);
|
||||
let canonicalPath: string;
|
||||
try {
|
||||
lstatSync(path);
|
||||
canonicalPath = realpathSync(path);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return;
|
||||
// A dangling symlink must not become an apparently safe create target.
|
||||
try { lstatSync(path); return; } catch (missing) {
|
||||
if ((missing as NodeJS.ErrnoException).code !== "ENOENT") return;
|
||||
}
|
||||
canonicalPath = join(realpathSync(dirname(path)), basename(path));
|
||||
}
|
||||
return safeAcpxLocations([{ path: canonicalPath }], canonicalCwd, "edit")[0]?.path as string | undefined;
|
||||
} catch { return; }
|
||||
}
|
||||
|
||||
/** Display context only; this does not authorize filesystem access. */
|
||||
export function safeCopilotEditTarget(value: unknown, workingDirectory: string | undefined): string | undefined {
|
||||
const call = record(value);
|
||||
if (call.kind !== "edit" || !workingDirectory) return;
|
||||
const input = record(call.rawInput);
|
||||
if (call.locations !== undefined && !Array.isArray(call.locations)) return;
|
||||
if (Array.isArray(call.locations) && (call.locations.length > 16 || call.locations.some(x => typeof record(x).path !== "string"))) return;
|
||||
const paths = [input.path, input.fileName, ...(Array.isArray(call.locations) ? call.locations.map(x => record(x).path) : [])].filter(x => x !== undefined);
|
||||
if (!paths.length || paths.some(x => typeof x !== "string" || !x || x.trim() !== x || x.length > 2048 || /[\\\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(x))) return;
|
||||
const safe = paths.flatMap(path => {
|
||||
const locations = safeAcpxLocations([{ path }], workingDirectory, call.kind, call.title);
|
||||
if (locations.length) return locations;
|
||||
const aliased = aliasedLocation(path as string, workingDirectory);
|
||||
return aliased ? [{ path: aliased }] : [];
|
||||
});
|
||||
const names = safe.map(x => x.path).filter((x): x is string => typeof x === "string" && x.length <= 1024 && x.trim() === x && !x.includes(":"));
|
||||
if (names.length !== paths.length || new Set(names).size !== 1) return;
|
||||
const target = names[0];
|
||||
return target && redactPaperclipSemanticValue(target) === target ? target : undefined;
|
||||
}
|
||||
export { safeAcpxEditTarget as safeCopilotEditTarget } from "./acp-edit-context.js";
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
|
||||
import { ACPX_CAPABILITY_PROFILES } from "./capability-profiles.js";
|
||||
|
||||
const ACPX_AGENTS = ["claude", "codex", "grok", "pi", "cursor", "copilot", "antigravity"] as const;
|
||||
const ACPX_AGENTS = ["claude", "codex", "grok", "pi", "cursor", "copilot", "kimi", "antigravity"] as const;
|
||||
const ACPX_PERMISSION_MODES = [
|
||||
"approve-all",
|
||||
"approve-paperclip",
|
||||
@@ -113,7 +113,7 @@ export function validateAcpxDriverConfig(
|
||||
return invalid(
|
||||
"agent",
|
||||
"invalid_agent",
|
||||
"ACPX agent must be claude, codex, grok, cursor, copilot, pi, or antigravity.",
|
||||
"ACPX agent must be claude, codex, grok, cursor, copilot, kimi, pi, or antigravity.",
|
||||
);
|
||||
}
|
||||
if (ACPX_CAPABILITY_PROFILES[agent].qualification !== "qualified") {
|
||||
|
||||
@@ -44,9 +44,9 @@ describe("ACPX launch environment", () => {
|
||||
expect(launch.ACPX_CLAUDE_INCLUDE_USER_SETTINGS).toBeUndefined();
|
||||
expect(environment.CLAUDE_CONFIG_DIR).toBe(configured);
|
||||
});
|
||||
it("retains Runner notification discovery while denying task-supplied overrides", () => {
|
||||
it.each(["claude", "kimi"] as const)("retains %s Runner notification discovery while denying task-supplied overrides", agent => {
|
||||
for (const key of ["PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT", "PAPERCLIP_RUNTIME_NOTIFICATION_HELPER"]) {
|
||||
expect(createSanitizedAcpxSpawnInput({ [key]: "/private/runner/context" }, "claude").env[key]).toBe("/private/runner/context");
|
||||
expect(createSanitizedAcpxSpawnInput({ [key]: "/private/runner/context" }, agent).env[key]).toBe("/private/runner/context");
|
||||
expect(configuredEnvironmentProjection({ [key]: "/caller/override" })).not.toHaveProperty(key);
|
||||
}
|
||||
});
|
||||
@@ -137,7 +137,9 @@ describe("ACPX launch environment", () => {
|
||||
|
||||
it.each([
|
||||
["pi", "OPENROUTER_API_KEY"], ["cursor", "CURSOR_API_KEY"],
|
||||
["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"], ["antigravity", "GEMINI_API_KEY"],
|
||||
["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"],
|
||||
["kimi", "KIMI_API_KEY"],
|
||||
["antigravity", "GEMINI_API_KEY"],
|
||||
] as const)("requires an explicit %s credential binding for %s", (agent, name) => {
|
||||
vi.stubEnv(name, "ambient-secret");
|
||||
expect(createSanitizedAcpxSpawnInput(undefined, agent).env[name]).toBeUndefined();
|
||||
@@ -157,9 +159,33 @@ describe("ACPX launch environment", () => {
|
||||
expect(createSanitizedAcpxSpawnInput(source, "pi").env).toEqual({ COPILOT_GITHUB_TOKEN: "copilot-key" });
|
||||
});
|
||||
|
||||
it("strips configured Kimi model redirects and home overrides while retaining selected task and MCP values", () => {
|
||||
const overrides = {
|
||||
KIMI_MODEL_NAME: "__kimi_env_model__", KIMI_MODEL_API_KEY: "unselected-model-key",
|
||||
KIMI_MODEL_BASE_URL: "https://unselected.invalid/v1", KIMI_MODEL_PROVIDER_TYPE: "openai_legacy",
|
||||
KIMI_MODEL_FUTURE_OVERRIDE: "cannot-bypass-the-prefix-check", KIMI_SECONDARY_MODEL: "other-model",
|
||||
KIMI_CODE_HOME: "/unselected/home",
|
||||
};
|
||||
const source = { ...configuredEnvironmentProjection({ ...overrides, CUSTOM_SETTING: "allowed" }),
|
||||
KIMI_API_KEY: "selected-code-key", PAPERCLIP_NATIVE_MCP_NAME: "assigned",
|
||||
PAPERCLIP_NATIVE_MCP_URL: "https://assigned.example/mcp", PAPERCLIP_NATIVE_MCP_TOKEN: "controller-only" };
|
||||
const binding = createAcpxCredentialBinding(source, "kimi", "session-1")!;
|
||||
const sidecar = createAcpxSidecarHostEnvironment({ ...source, [ACPX_CREDENTIAL_BINDING_ENV]: binding }, "kimi", "session-1");
|
||||
for (const environment of [sidecar, createSanitizedAcpxSpawnInput(source, "kimi").env,
|
||||
createSanitizedAcpxSpawnInput(sidecar, "kimi").env]) {
|
||||
for (const name of Object.keys(overrides)) expect(environment).not.toHaveProperty(name);
|
||||
expect(environment).toMatchObject({ KIMI_API_KEY: "selected-code-key", CUSTOM_SETTING: "allowed",
|
||||
PAPERCLIP_NATIVE_MCP_NAME: "assigned", PAPERCLIP_NATIVE_MCP_URL: "https://assigned.example/mcp" });
|
||||
expect(JSON.parse(environment[CONFIGURED_ENVIRONMENT_KEYS]!)).toEqual(["CUSTOM_SETTING"]);
|
||||
}
|
||||
expect(createSanitizedAcpxSpawnInput(source, "kimi").env).not.toHaveProperty("PAPERCLIP_NATIVE_MCP_TOKEN");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["pi", "OPENROUTER_API_KEY"], ["cursor", "CURSOR_API_KEY"],
|
||||
["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"], ["antigravity", "GEMINI_API_KEY"],
|
||||
["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"],
|
||||
["kimi", "KIMI_API_KEY"],
|
||||
["antigravity", "GEMINI_API_KEY"],
|
||||
] as const)("admits only session-bound %s sidecar credentials for %s", (agent, name) => {
|
||||
const source = { [name]: "task-secret" };
|
||||
const binding = createAcpxCredentialBinding(source, agent, "session-1");
|
||||
|
||||
@@ -10,6 +10,7 @@ export const ACPX_CREDENTIAL_BINDING_ENV = "PAPERCLIP_ACPX_CREDENTIAL_BINDING";
|
||||
export const ACPX_CREDENTIAL_NAMES: Readonly<Record<QualifiedAcpxAgent, readonly string[]>> = {
|
||||
antigravity: ["GEMINI_API_KEY", "PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET"],
|
||||
grok: ["XAI_API_KEY"],
|
||||
kimi: ["KIMI_API_KEY"],
|
||||
pi: PI_CREDENTIAL_NAMES,
|
||||
cursor: ["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"],
|
||||
copilot: ["COPILOT_GITHUB_TOKEN"],
|
||||
@@ -27,7 +28,17 @@ function removePiIamCredentials(environment: NodeJS.ProcessEnv): void {
|
||||
}
|
||||
}
|
||||
|
||||
const isCandidate = (agent: QualifiedAcpxAgent) => agent === "pi" || agent === "cursor" || agent === "copilot" || agent === "antigravity";
|
||||
function removeKimiRuntimeOverrides(environment: NodeJS.ProcessEnv): void {
|
||||
const isOverride = (name: string) => /^KIMI_MODEL_/.test(name.toUpperCase())
|
||||
|| ["KIMI_SECONDARY_MODEL", "KIMI_CODE_HOME"].includes(name.toUpperCase());
|
||||
const names = configuredEnvironmentKeys(environment);
|
||||
for (const name of Object.keys(environment)) if (isOverride(name)) delete environment[name];
|
||||
if (environment[CONFIGURED_ENVIRONMENT_KEYS] !== undefined) {
|
||||
environment[CONFIGURED_ENVIRONMENT_KEYS] = JSON.stringify(names.filter(name => !isOverride(name)));
|
||||
}
|
||||
}
|
||||
|
||||
const isCandidate = (agent: QualifiedAcpxAgent) => agent === "pi" || agent === "cursor" || agent === "copilot" || agent === "kimi" || agent === "antigravity";
|
||||
|
||||
/** Mint only at the controller's explicit task-environment boundary, never by copying a marker. */
|
||||
export function createAcpxCredentialBinding(
|
||||
@@ -79,6 +90,7 @@ export function createAcpxSidecarHostEnvironment(
|
||||
}
|
||||
}
|
||||
if (agent === "pi") removePiIamCredentials(result);
|
||||
if (agent === "kimi") removeKimiRuntimeOverrides(result);
|
||||
if (agent === "antigravity") sanitizeAntigravityEnvironment(result);
|
||||
return result;
|
||||
}
|
||||
@@ -164,6 +176,7 @@ export function createSanitizedAcpxSpawnInput(
|
||||
result[key] = value;
|
||||
}
|
||||
if (agent === "pi") removePiIamCredentials(result);
|
||||
if (agent === "kimi") removeKimiRuntimeOverrides(result);
|
||||
if (agent === "antigravity") sanitizeAntigravityEnvironment(result);
|
||||
// The controller-to-sidecar document is staged under a lifetime fence. The
|
||||
// native child receives the private token file, never its launch document.
|
||||
|
||||
@@ -95,6 +95,19 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"agentRuntimeVersion": "0.160.0",
|
||||
"commandDigest": "sha256:443491f4e814501a5dc47347e4510e84faeec9c9382e2d4e53bfe69386d5aabc",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"kimi": {
|
||||
"driverKind": "acpx_runtime",
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "kimi",
|
||||
"agentProfileVersion": 17,
|
||||
"agentServerPackage": "@moonshot-ai/kimi-code",
|
||||
"agentServerVersion": "2.3.3",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:6ee5fb7ac18c1361ac63effe9117d08ed129add8349756a140e73e0c86ba5da7",
|
||||
"permissionPolicy": "interactive"
|
||||
}
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export const KIMI_RUNTIME_VERSION = "2.3.3" as const;
|
||||
export const KIMI_NODE_VERSION = "24.21.0" as const;
|
||||
export const KIMI_DISTRIBUTION_LOCK_SHA256 = "87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b";
|
||||
export const KIMI_NODE_DISTRIBUTIONS = Object.freeze({
|
||||
"darwin-arm64": Object.freeze({ archiveSha256: "bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057", executableSha256: "e4b5a3af0e05c75de2eae013904145f40fe7fc2a6e6f17510128bf45cca4e79b", executableSize: 122129232 }),
|
||||
"linux-x64": Object.freeze({ archiveSha256: "6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff", executableSha256: "7fde7b8afa198da66257f42ee2001d874c7355631e6d1579a5fb5ef1f246df4c", executableSize: 126595440 }),
|
||||
});
|
||||
|
||||
export const KIMI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({
|
||||
"darwin-arm64": "6038325abd0431c30097ff1f572a9b29816e1b4da372d8cb86b2ca5d18ea34e7",
|
||||
"linux-x64": "105d8394adbbc76a953a12cc51b5288eb940ad693291e584f6cdc6407213164d",
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
export function kimiManagedConfiguration(model: string): string {
|
||||
if (!model.trim() || model.includes("\0") || Buffer.byteLength(model) > 512) throw new Error("Kimi Code requires a bounded explicit model ID");
|
||||
const quote = JSON.stringify;
|
||||
return [
|
||||
`default_model = ${quote(model)}`,
|
||||
"telemetry = false",
|
||||
"builtin_product_skills = false",
|
||||
"merge_all_available_skills = false",
|
||||
"hooks = []",
|
||||
"[providers.paperclip]",
|
||||
'type = "kimi"',
|
||||
'base_url = "https://api.kimi.ai/coding/v1"',
|
||||
'api_key_env = "KIMI_API_KEY"',
|
||||
`[models.${quote(model)}]`,
|
||||
'provider = "paperclip"',
|
||||
`model = ${quote(model)}`,
|
||||
"max_context_size = 262144",
|
||||
'capabilities = ["tool_use"]',
|
||||
"[tools]",
|
||||
'disabled = ["Agent", "AgentSwarm", "TaskList", "TaskOutput", "TaskStop", "CronCreate", "CronList", "CronDelete", "CreateGoal", "GetGoal", "SetGoalBudget", "UpdateGoal", "AskUserQuestion", "EnterPlanMode", "ExitPlanMode"]',
|
||||
"[background]",
|
||||
"keep_alive_on_exit = false",
|
||||
"bash_auto_background_on_timeout = false",
|
||||
"[watch]",
|
||||
"enabled = false",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function kimiReadOnlyPermissionAllowed(request: unknown, runnerOwnedMcpServerNames: ReadonlySet<string>): boolean {
|
||||
if (!request || typeof request !== "object") return false;
|
||||
const raw = (request as { raw?: unknown }).raw;
|
||||
if (!raw || typeof raw !== "object") return false;
|
||||
const call = (raw as { toolCall?: unknown }).toolCall;
|
||||
if (!call || typeof call !== "object") return false;
|
||||
const title = (call as { title?: unknown }).title;
|
||||
const input = (call as { rawInput?: unknown }).rawInput;
|
||||
if (typeof title !== "string" || !input || typeof input !== "object" || Array.isArray(input)) return false;
|
||||
for (const name of runnerOwnedMcpServerNames) {
|
||||
const prefix = `mcp__${name}__`;
|
||||
if (title.startsWith(prefix) && title.length > prefix.length) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { assertKimiInstallationProfile, verifyKimiInstallation } from "./kimi-installation.js";
|
||||
import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js";
|
||||
import { isSupportedAcpxProfileVersion } from "./profile-compatibility.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
const previous = new Map<string, string | undefined>();
|
||||
function environment(key: string, value: string) {
|
||||
if (!previous.has(key)) previous.set(key, process.env[key]);
|
||||
process.env[key] = value;
|
||||
}
|
||||
afterEach(async () => {
|
||||
for (const [key, value] of previous) { if (value === undefined) delete process.env[key]; else process.env[key] = value; }
|
||||
previous.clear();
|
||||
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("Kimi installed runtime admission", () => {
|
||||
it("rejects caller-selected package/version/command changes before filesystem resolution", () => {
|
||||
const profile = QUALIFIED_ACPX_PROFILES.kimi;
|
||||
expect(() => assertKimiInstallationProfile(profile)).not.toThrow();
|
||||
for (const alteration of [{ agentServerVersion: "latest" }, { agentServerPackage: "foreign" }, { commandDigest: `sha256:${"f".repeat(64)}` }, { agentProfileVersion: profile.agentProfileVersion + 1 }]) {
|
||||
expect(() => assertKimiInstallationProfile({ ...profile, ...alteration })).toThrow(/trusted declaration/);
|
||||
}
|
||||
});
|
||||
|
||||
it("retains pending profile 10 while binding the merged entrypoint builder in profile 11", () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v10-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("b859f4aefcc36fc5e75b3aeeb02135ca9f7acc84866c856a3939effdd13cb358");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/kimi-profile-v11-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration.sources).filter(key => current.declaration.sources[key] !== prior.declaration.sources[key])).toEqual([
|
||||
"scripts/build-verified-provider-entrypoints.mjs",
|
||||
]);
|
||||
expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi,
|
||||
agentProfileVersion: 10, commandDigest: prior.commandDigest })).toThrow(/trusted declaration/);
|
||||
});
|
||||
|
||||
it("preserves historical profile 11 while binding the reconciled authority and event boundaries in 13", () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v11-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("17b3db5659cfbaa4d3f5dddd8a97ffeab38353795aa4dbfa6c6fe1520f415ab1");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/kimi-profile-v13-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.declaration.agentProfileVersion).toBe(11);
|
||||
expect(Object.keys(prior.declaration.sources)).toHaveLength(31);
|
||||
expect(current.declaration.agentProfileVersion).toBe(13);
|
||||
expect(Object.keys(current.declaration.sources)).toHaveLength(55);
|
||||
expect(Object.hasOwn(current.declaration.sources, "src/drivers/acpx/candidate-admission.ts")).toBe(true);
|
||||
expect(Object.hasOwn(current.declaration.sources, "runner/crates/runner-core/src/provider_events.rs")).toBe(true);
|
||||
expect(isSupportedAcpxProfileVersion("kimi", 11)).toBe(true);
|
||||
expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi,
|
||||
agentProfileVersion: 11, commandDigest: prior.commandDigest })).toThrow(/trusted declaration/);
|
||||
});
|
||||
|
||||
it("decodes pending14 history without installing it as the repaired pending15 runtime", () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v14-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("13ce8e301e6b7d622f4b3ffdca7981e18538e7a5ca2f1b2208ec466259469e7b");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/kimi-profile-v15-identity.json", import.meta.url), "utf8"));
|
||||
expect(current.declaration.agentProfileVersion).toBe(15);
|
||||
expect(Object.keys(current.declaration.sources)).toHaveLength(55);
|
||||
expect(Object.keys(current.declaration.sources).filter(key => current.declaration.sources[key] !== prior.declaration.sources[key])).toEqual([
|
||||
"runner/crates/runner-core/src/acpx_provider_backend.rs",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("kimi", 14)).toBe(true);
|
||||
expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi,
|
||||
agentProfileVersion: 14, commandDigest: prior.commandDigest })).toThrow(/trusted declaration/);
|
||||
expect(() => assertKimiInstallationProfile(QUALIFIED_ACPX_PROFILES.kimi)).not.toThrow();
|
||||
});
|
||||
|
||||
it("decodes pending15 history without installing it as merged pending16", () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v15-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("5221f6f9f64c02ca7ee68b185e08ed46b1c82a13037b001854065b9fad28ff87");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/kimi-profile-v16-identity.json", import.meta.url), "utf8"));
|
||||
expect(current.declaration.agentProfileVersion).toBe(16);
|
||||
expect(current.declaration.qualificationStatus).toBe("pending");
|
||||
expect(Object.keys(current.declaration.sources)).toHaveLength(58);
|
||||
expect(isSupportedAcpxProfileVersion("kimi", 15)).toBe(true);
|
||||
expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi,
|
||||
agentProfileVersion: 15, commandDigest: prior.commandDigest })).toThrow(/trusted declaration/);
|
||||
expect(() => assertKimiInstallationProfile(QUALIFIED_ACPX_PROFILES.kimi)).not.toThrow();
|
||||
});
|
||||
|
||||
it("decodes pending16 history without installing it as ordinary released17", () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v16-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("69e5fb7e7c445be7eee37bf7e6a8fa135cf2f88e0a997a95065ca79526e91470");
|
||||
const prior = JSON.parse(bytes.toString("utf8"));
|
||||
expect(prior.declaration.qualificationStatus).toBe("pending");
|
||||
expect(isSupportedAcpxProfileVersion("kimi", 16)).toBe(true);
|
||||
expect(QUALIFIED_ACPX_PROFILES.kimi.agentProfileVersion).toBe(17);
|
||||
expect(QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus).toBeUndefined();
|
||||
expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi,
|
||||
agentProfileVersion: 16, commandDigest: prior.commandDigest })).toThrow(/trusted declaration/);
|
||||
expect(() => assertKimiInstallationProfile(QUALIFIED_ACPX_PROFILES.kimi)).not.toThrow();
|
||||
});
|
||||
|
||||
it("preserves the source-bound released17 identity without accepting its old digest", () => {
|
||||
const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v17-identity.json", import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe("c3b6a1bf95e7de1abf7a07f042b5dbe9c10d1a3ae2007a886b6387d779a7a205");
|
||||
const historical = JSON.parse(bytes.toString("utf8"));
|
||||
expect(historical.declaration.agentProfileVersion).toBe(17);
|
||||
expect(QUALIFIED_ACPX_PROFILES.kimi.agentProfileVersion).toBe(17);
|
||||
expect(QUALIFIED_ACPX_PROFILES.kimi.commandDigest).not.toBe(historical.commandDigest);
|
||||
expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi,
|
||||
commandDigest: historical.commandDigest })).toThrow(/trusted declaration/);
|
||||
expect(() => assertKimiInstallationProfile(QUALIFIED_ACPX_PROFILES.kimi)).not.toThrow();
|
||||
});
|
||||
|
||||
it("gives the normal setup command and never falls back to ambient kimi when assets are missing", async () => {
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-installation-test-"))); roots.push(root);
|
||||
await writeFile(join(root, "package.json"), '{"name":"@paperclipai/paperclip-runner"}');
|
||||
environment("PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", root);
|
||||
environment("PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", join(root, "package.json"));
|
||||
await expect(verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi)).rejects.toThrow(/paperclipai runtime setup kimi/);
|
||||
const assets = join(root, "provider-assets/kimi", `${process.platform}-${process.arch}`);
|
||||
await mkdir(join(assets, "runtime"), { recursive: true });
|
||||
await writeFile(join(assets, "kimi-distribution.json"), JSON.stringify({ schema: "paperclip.kimi-distribution.v1", nativeClosureSha256: "f".repeat(64) }));
|
||||
await expect(verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi)).rejects.toThrow(/source-owned pin/);
|
||||
});
|
||||
});
|
||||
Loaded 100 of 484 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user