mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
fix(acp): expose staged skills and keep session environment ephemeral
Advertise bounded skill routing metadata with exact staged file paths. Keep current provider credentials in memory and out of persisted session records while preserving resume options and rotation. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
1eff46a6f3
commit
b54a456d40
5 files changed
+308
-25
No files matched your search
@@ -0,0 +1,165 @@
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createRuntimeStore, type AcpSessionRecord, type AcpSessionStore } from "acpx/runtime";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEphemeralSessionEnvironmentStore } from "./ephemeral-session-environment.js";
|
||||
|
||||
const currentEnvironment = Object.freeze({
|
||||
PAPERCLIP_API_KEY: "current-credential-fixture",
|
||||
PAPERCLIP_RUN_ID: "current-run-fixture",
|
||||
PATH: "/fixture/current-bin",
|
||||
});
|
||||
const oldEnvironment = { PAPERCLIP_API_KEY: "old-credential-fixture", OLD_RUN_PATH: "/fixture/old-run" };
|
||||
|
||||
function record(): AcpSessionRecord {
|
||||
return {
|
||||
schema: "acpx.session.v1",
|
||||
acpxRecordId: "fixture-record",
|
||||
acpSessionId: "fixture-session",
|
||||
agentSessionId: "fixture-agent-session",
|
||||
agentCommand: "fixture-provider",
|
||||
cwd: "/fixture/project",
|
||||
name: "fixture-name",
|
||||
createdAt: "2026-10-03T00:00:00.000Z",
|
||||
lastUsedAt: "2026-10-03T00:00:01.000Z",
|
||||
lastSeq: 3,
|
||||
eventLog: { active_path: "/fixture/events.jsonl", segment_count: 1, max_segment_bytes: 1024, max_segments: 2 },
|
||||
messages: [{ User: { id: "message-1", content: [{ Text: "Retained conversation" }] } }],
|
||||
updated_at: "2026-10-03T00:00:01.000Z",
|
||||
cumulative_token_usage: { input_tokens: 11, output_tokens: 7 },
|
||||
request_token_usage: { request_1: { input_tokens: 11 } },
|
||||
importedFrom: { recordId: "origin-record", cwdOriginal: "/fixture/origin", exportedBy: "fixture-caller", exportedAt: "2026-10-03T00:00:00.000Z" },
|
||||
acpx: {
|
||||
current_mode_id: "code",
|
||||
desired_config_options: { effort: "high" },
|
||||
session_options: { model: "fixture-model", allowed_tools: ["Read", "Write"], max_turns: 4, system_prompt: { append: "Retained caller option" }, env: { ...currentEnvironment } },
|
||||
},
|
||||
};
|
||||
}
|
||||
function freeze<T extends object>(value: T): T {
|
||||
for (const child of Object.values(value)) if (typeof child === "object" && child !== null) freeze(child);
|
||||
return Object.freeze(value);
|
||||
}
|
||||
function stubStore(loaded: AcpSessionRecord | undefined) {
|
||||
const load = vi.fn<AcpSessionStore["load"]>().mockResolvedValue(loaded);
|
||||
const save = vi.fn<AcpSessionStore["save"]>().mockResolvedValue(undefined);
|
||||
return { load, save };
|
||||
}
|
||||
const directories: string[] = [];
|
||||
afterEach(async () => { await Promise.all(directories.splice(0).map(directory => rm(directory, { recursive: true, force: true }))); });
|
||||
async function diskStore() {
|
||||
const stateDir = await mkdtemp(join(tmpdir(), "paperclip-session-env-")); directories.push(stateDir);
|
||||
return { store: createRuntimeStore({ stateDir }), file: join(stateDir, "sessions", "fixture-record.json") };
|
||||
}
|
||||
|
||||
describe("ephemeral ACPX session environment", () => {
|
||||
it("saves a fresh session to the actual file store without persisting its live environment", async () => {
|
||||
const disk = await diskStore(), input = freeze(record());
|
||||
const wrapper = createEphemeralSessionEnvironmentStore(disk.store, currentEnvironment);
|
||||
await wrapper.save(input);
|
||||
const text = await readFile(disk.file, "utf8"), saved = JSON.parse(text);
|
||||
expect(saved.acpx.session_options).not.toHaveProperty("env");
|
||||
for (const value of Object.values(currentEnvironment)) expect(text).not.toContain(value);
|
||||
expect(input.acpx?.session_options?.env).toEqual(currentEnvironment);
|
||||
expect(saved.acpx.current_mode_id).toBe("code");
|
||||
expect(saved.acpx.desired_config_options).toEqual(input.acpx?.desired_config_options);
|
||||
expect(saved.acpx.session_options).toEqual({ model: "fixture-model", allowed_tools: ["Read", "Write"], max_turns: 4, system_prompt: { append: "Retained caller option" } });
|
||||
expect(saved.messages).toEqual(input.messages);
|
||||
expect(saved.imported_from.record_id).toBe("origin-record");
|
||||
const loaded = await wrapper.load(input.acpxRecordId);
|
||||
expect(loaded?.acpx?.session_options?.env).toEqual(currentEnvironment);
|
||||
expect(loaded?.messages).toEqual(input.messages);
|
||||
});
|
||||
|
||||
it("loads an old env-bearing file with rotated credentials without writing, then saves neither environment", async () => {
|
||||
const disk = await diskStore(), legacy = record(); legacy.acpx!.session_options!.env = { ...oldEnvironment };
|
||||
await disk.store.save(legacy);
|
||||
const before = await readFile(disk.file, "utf8");
|
||||
const wrapper = createEphemeralSessionEnvironmentStore(disk.store, currentEnvironment);
|
||||
const loaded = await wrapper.load(legacy.acpxRecordId);
|
||||
expect(loaded?.acpx?.session_options?.env).toEqual(currentEnvironment);
|
||||
expect(loaded?.acpx?.session_options?.env).not.toHaveProperty("OLD_RUN_PATH");
|
||||
expect(await readFile(disk.file, "utf8")).toBe(before);
|
||||
expect(legacy.acpx?.session_options?.env).toEqual(oldEnvironment);
|
||||
await wrapper.save(loaded!);
|
||||
const after = await readFile(disk.file, "utf8");
|
||||
for (const value of [...Object.values(oldEnvironment), ...Object.values(currentEnvironment)]) expect(after).not.toContain(value);
|
||||
expect(JSON.parse(after).acpx.session_options).not.toHaveProperty("env");
|
||||
expect(loaded?.acpx?.session_options?.env).toEqual(currentEnvironment);
|
||||
});
|
||||
|
||||
it("loads an env-free persisted session into the current environment without writing", async () => {
|
||||
const disk = await diskStore(), input = record(); delete input.acpx!.session_options!.env;
|
||||
await disk.store.save(input);
|
||||
const before = await readFile(disk.file, "utf8");
|
||||
const loaded = await createEphemeralSessionEnvironmentStore(disk.store, currentEnvironment).load(input.acpxRecordId);
|
||||
expect(loaded?.acpx?.session_options?.env).toEqual(currentEnvironment);
|
||||
expect(loaded?.acpx?.session_options?.model).toBe("fixture-model");
|
||||
expect(await readFile(disk.file, "utf8")).toBe(before);
|
||||
});
|
||||
|
||||
it("copies loaded records, options and current env without mutating stored or caller-owned objects", async () => {
|
||||
const stored = record(); stored.acpx!.session_options!.env = { ...oldEnvironment }; freeze(stored);
|
||||
const backing = stubStore(stored), wrapper = createEphemeralSessionEnvironmentStore(backing, currentEnvironment);
|
||||
const loaded = await wrapper.load("fixture-record");
|
||||
expect(backing.load).toHaveBeenCalledWith("fixture-record"); expect(backing.save).not.toHaveBeenCalled();
|
||||
expect(loaded).not.toBe(stored); expect(loaded?.acpx).not.toBe(stored.acpx);
|
||||
expect(loaded?.acpx?.session_options).not.toBe(stored.acpx?.session_options);
|
||||
expect(loaded?.acpx?.session_options?.env).not.toBe(currentEnvironment);
|
||||
expect(stored.acpx?.session_options?.env).toEqual(oldEnvironment);
|
||||
loaded!.acpx!.session_options!.env!.PAPERCLIP_API_KEY = "isolated-loaded-fixture";
|
||||
expect(currentEnvironment.PAPERCLIP_API_KEY).toBe("current-credential-fixture");
|
||||
});
|
||||
|
||||
it("saves only a copied env-free projection, preserving unrelated and caller metadata", async () => {
|
||||
const input = freeze(Object.assign(record(), { caller_metadata: { keep: "caller-state", env: { preserve: "unrelated caller metadata" } } }));
|
||||
const backing = stubStore(undefined), wrapper = createEphemeralSessionEnvironmentStore(backing, currentEnvironment);
|
||||
await wrapper.save(input);
|
||||
const saved = backing.save.mock.calls[0]![0];
|
||||
const { env: _environment, ...expectedOptions } = input.acpx!.session_options!;
|
||||
expect(saved).toEqual({ ...input, acpx: { ...input.acpx, session_options: expectedOptions } });
|
||||
expect(saved).not.toBe(input); expect(saved.acpx).not.toBe(input.acpx);
|
||||
expect(saved.acpx?.session_options).not.toBe(input.acpx?.session_options);
|
||||
expect(saved.acpx?.session_options).not.toHaveProperty("env");
|
||||
expect(input.acpx?.session_options?.env).toEqual(currentEnvironment);
|
||||
});
|
||||
|
||||
for (const [name, acpx] of [
|
||||
["absent acpx", undefined],
|
||||
["explicit undefined acpx", undefined],
|
||||
["absent session options", { current_mode_id: "code" }],
|
||||
["undefined session options", { session_options: undefined }],
|
||||
["empty session options", { session_options: {} }],
|
||||
["undefined env", { session_options: { env: undefined, model: "fixture-model" } }],
|
||||
] as const) it(`preserves ${name} when saving and retains existing load injection semantics`, async () => {
|
||||
const input = record();
|
||||
if (name === "absent acpx") delete input.acpx; else input.acpx = acpx;
|
||||
freeze(input);
|
||||
const backing = stubStore(input), wrapper = createEphemeralSessionEnvironmentStore(backing, currentEnvironment);
|
||||
await wrapper.save(input);
|
||||
const saved = backing.save.mock.calls[0]![0];
|
||||
if (name === "undefined env") expect(saved.acpx?.session_options).toEqual({ model: "fixture-model" });
|
||||
else expect(saved).toEqual(input);
|
||||
expect(Object.hasOwn(saved, "acpx")).toBe(Object.hasOwn(input, "acpx"));
|
||||
if (saved.acpx && input.acpx) expect(Object.hasOwn(saved.acpx, "session_options")).toBe(Object.hasOwn(input.acpx, "session_options"));
|
||||
const loaded = await wrapper.load(input.acpxRecordId);
|
||||
expect(loaded?.acpx?.session_options?.env).toEqual(currentEnvironment);
|
||||
expect(backing.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns a missing record without saving or creating conversation state", async () => {
|
||||
const backing = stubStore(undefined);
|
||||
expect(await createEphemeralSessionEnvironmentStore(backing, currentEnvironment).load("missing")).toBeUndefined();
|
||||
expect(backing.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("propagates load and save errors unchanged", async () => {
|
||||
const failure = new Error("fixture store failure"), backing = stubStore(undefined);
|
||||
backing.load.mockRejectedValue(failure); backing.save.mockRejectedValue(failure);
|
||||
const wrapper = createEphemeralSessionEnvironmentStore(backing, currentEnvironment);
|
||||
await expect(wrapper.load("fixture-record")).rejects.toBe(failure);
|
||||
const input = freeze(record()); await expect(wrapper.save(input)).rejects.toBe(failure);
|
||||
expect(input.acpx?.session_options?.env).toEqual(currentEnvironment);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { AcpSessionStore } from "acpx/runtime";
|
||||
|
||||
/** Keep the current run's environment in memory, outside persisted conversation state. */
|
||||
export function createEphemeralSessionEnvironmentStore(
|
||||
persistedStore: AcpSessionStore,
|
||||
currentEnvironment: Readonly<Record<string, string>>,
|
||||
): AcpSessionStore {
|
||||
return {
|
||||
async load(id) {
|
||||
const record = await persistedStore.load(id);
|
||||
if (!record) return undefined;
|
||||
return {
|
||||
...record,
|
||||
acpx: {
|
||||
...record.acpx,
|
||||
session_options: {
|
||||
...record.acpx?.session_options,
|
||||
env: { ...currentEnvironment },
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
save(record) {
|
||||
const persisted = { ...record };
|
||||
if (record.acpx?.session_options !== undefined) {
|
||||
const { env: _environment, ...sessionOptions } = record.acpx.session_options;
|
||||
persisted.acpx = { ...record.acpx, session_options: sessionOptions };
|
||||
}
|
||||
return persistedStore.save(persisted);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AcpRuntimeOptions } from "acpx/runtime";
|
||||
import { createRuntimeStore, type AcpRuntimeOptions, type AcpSessionRecord, type AcpSessionStore } from "acpx/runtime";
|
||||
import type { AdapterExecutionContext, AdapterRuntimeMcpAccess } from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
DEFAULT_REMOTE_SANDBOX_ADAPTER_TIMEOUT_SEC,
|
||||
@@ -377,6 +377,42 @@ const ALLOWED_TURN_SPAN_ATTRIBUTE_KEYS = new Set<string>([
|
||||
]);
|
||||
|
||||
describe("shared ACPX engine runtime behavior", () => {
|
||||
it("integrates the env-free store while delivering current credentials and options to the provider", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const stateDir = path.join(root, "state");
|
||||
const first = await runExecutor({ agent: "claude", agentCommand: "node ./fake-acp.js", cwd: root, stateDir,
|
||||
env: { ANTHROPIC_API_KEY: "first-fixture-credential" } });
|
||||
const liveEnvironment = (first.sessionInputs[0]!.sessionOptions as { env: Record<string, string> }).env;
|
||||
expect(liveEnvironment.ANTHROPIC_API_KEY).toBe("first-fixture-credential");
|
||||
const store = first.runtimeOptions[0]!.sessionStore as AcpSessionStore;
|
||||
const record: AcpSessionRecord = {
|
||||
schema: "acpx.session.v1", acpxRecordId: "integration-record", acpSessionId: "integration-session",
|
||||
agentCommand: "fixture-provider", cwd: root, name: "fixture", createdAt: "2026-10-03T00:00:00.000Z",
|
||||
lastUsedAt: "2026-10-03T00:00:01.000Z", lastSeq: 0,
|
||||
eventLog: { active_path: path.join(stateDir, "fixture.jsonl"), segment_count: 1, max_segment_bytes: 1024, max_segments: 2 },
|
||||
messages: [], updated_at: "2026-10-03T00:00:01.000Z", cumulative_token_usage: {}, request_token_usage: {},
|
||||
acpx: { session_options: { env: { ...liveEnvironment }, model: "retained-model", max_turns: 4 } },
|
||||
};
|
||||
await store.save(record);
|
||||
const file = path.join(stateDir, "sessions", "integration-record.json");
|
||||
const saved = await fs.readFile(file, "utf8");
|
||||
expect(saved).not.toContain("first-fixture-credential");
|
||||
expect(JSON.parse(saved).acpx.session_options).toEqual({ model: "retained-model", max_turns: 4 });
|
||||
expect(record.acpx?.session_options?.env).toEqual(liveEnvironment);
|
||||
// A file written by the prior engine is loaded with the current run's env,
|
||||
// without altering the provider launch or deleting unrelated options.
|
||||
record.acpx!.session_options!.env = { ANTHROPIC_API_KEY: "legacy-fixture-credential" };
|
||||
await createRuntimeStore({ stateDir }).save(record);
|
||||
const second = await runExecutor({ agent: "claude", agentCommand: "node ./fake-acp.js", cwd: root, stateDir,
|
||||
env: { ANTHROPIC_API_KEY: "rotated-fixture-credential" } }, { runtime: { sessionParams: first.result.sessionParams } });
|
||||
expect((second.sessionInputs[0]!.sessionOptions as { env: Record<string, string> }).env.ANTHROPIC_API_KEY).toBe("rotated-fixture-credential");
|
||||
const rotatedStore = second.runtimeOptions[0]!.sessionStore as AcpSessionStore;
|
||||
const loaded = await rotatedStore.load(record.acpxRecordId);
|
||||
expect(loaded?.acpx?.session_options).toMatchObject({ env: { ANTHROPIC_API_KEY: "rotated-fixture-credential" }, model: "retained-model", max_turns: 4 });
|
||||
await rotatedStore.save(loaded!);
|
||||
const resaved = await fs.readFile(file, "utf8");
|
||||
for (const value of ["first-fixture-credential", "legacy-fixture-credential", "rotated-fixture-credential"]) expect(resaved).not.toContain(value);
|
||||
});
|
||||
it.each(["claude", "codex", "gemini", "kimi", "custom"])("defaults the legacy %s engine to full auto on fresh and resumed runs", async (agent) => {
|
||||
const root = await makeTempRoot();
|
||||
const config = {
|
||||
@@ -3784,9 +3820,54 @@ describe("ACPX engine Claude skill bundle staging (remote ACP lane)", () => {
|
||||
};
|
||||
}
|
||||
|
||||
it("advertises folded routing metadata and exact staged paths without injecting skill bodies", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cwd = path.join(root, "worktree");
|
||||
const stateDir = path.join(root, "state");
|
||||
await fs.mkdir(cwd, { recursive: true });
|
||||
const skill = await createSkill(path.join(root, "sources"), "paperclip--versioned", [
|
||||
"---", "description: >-", " Use for Paperclip-managed tasks", " and durable task documents.",
|
||||
"---", "PRIVATE_SKILL_BODY_RECIPE", "",
|
||||
].join("\n"));
|
||||
const { meta, result } = await runExecutor({
|
||||
agent: "claude", agentCommand: "node ./fake-acp.js", stateDir, cwd,
|
||||
paperclipRuntimeSkills: [skill], paperclipSkillSync: { desiredSkills: [skill.key] },
|
||||
});
|
||||
const bundle = await onlyChildDir(path.join(stateDir, "runtime-skills", "claude"));
|
||||
const stagedFile = path.join(bundle, ".claude", "skills", skill.runtimeName, "SKILL.md");
|
||||
const prompt = String(meta[0]?.prompt ?? "");
|
||||
expect(prompt).toContain(`- ${skill.runtimeName}: Use for Paperclip-managed tasks and durable task documents. (file: ${stagedFile})`);
|
||||
expect(prompt).not.toContain("PRIVATE_SKILL_BODY_RECIPE");
|
||||
expect(prompt).not.toContain(skill.source);
|
||||
expect(result.sessionParams?.skills).toMatchObject({ selectedSkills: [skill.runtimeName] });
|
||||
await expect(fs.readFile(stagedFile, "utf8")).resolves.toContain("PRIVATE_SKILL_BODY_RECIPE");
|
||||
});
|
||||
|
||||
it("bounds routing descriptions and keeps files discoverable when metadata is absent or malformed", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cwd = path.join(root, "worktree");
|
||||
const stateDir = path.join(root, "state");
|
||||
await fs.mkdir(cwd, { recursive: true });
|
||||
const sources = path.join(root, "sources");
|
||||
const bounded = await createSkill(sources, "bounded", `---\ndescription: ${"x".repeat(600)}\n---\nBOUNDED_BODY\n`);
|
||||
const absent = await createSkill(sources, "absent", "# ABSENT_BODY\n");
|
||||
const malformed = await createSkill(sources, "malformed", "---\ndescription: incomplete header\nMALFORMED_BODY\n");
|
||||
const skills = [bounded, absent, malformed];
|
||||
const { meta } = await runExecutor({
|
||||
agent: "claude", agentCommand: "node ./fake-acp.js", stateDir, cwd,
|
||||
paperclipRuntimeSkills: skills, paperclipSkillSync: { desiredSkills: skills.map((skill) => skill.key) },
|
||||
});
|
||||
const prompt = String(meta[0]?.prompt ?? "");
|
||||
expect(prompt).toContain(`- bounded: ${"x".repeat(512)} (file: `);
|
||||
expect(prompt).not.toContain("x".repeat(513));
|
||||
expect(prompt).toMatch(/- absent \(file: .*\/absent\/SKILL\.md\)/);
|
||||
expect(prompt).toMatch(/- malformed \(file: .*\/malformed\/SKILL\.md\)/);
|
||||
for (const body of ["BOUNDED_BODY", "ABSENT_BODY", "MALFORMED_BODY"]) expect(prompt).not.toContain(body);
|
||||
});
|
||||
|
||||
it("rewrites the prompt and skill identity onto the in-sandbox skill root once the bundle is staged", async () => {
|
||||
const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox();
|
||||
const skill = await createSkill(path.join(localCwd, "skills"), "review");
|
||||
const skill = await createSkill(path.join(localCwd, "skills"), "review", "---\ndescription: Review requested task documents.\n---\n# review\nREMOTE_BODY_ONLY\n");
|
||||
|
||||
const { meta, result } = await runExecutor(
|
||||
{
|
||||
@@ -3809,6 +3890,8 @@ describe("ACPX engine Claude skill bundle staging (remote ACP lane)", () => {
|
||||
|
||||
const inSandboxSkillsRoot = prompt.match(/Skill root: (\S+)/)![1]!;
|
||||
expect(inSandboxSkillsRoot).not.toBe(hostSkillsHome);
|
||||
expect(prompt).toContain(`- review: Review requested task documents. (file: ${path.join(inSandboxSkillsRoot, "review", "SKILL.md")})`);
|
||||
expect(prompt).not.toContain("REMOTE_BODY_ONLY");
|
||||
await expect(
|
||||
fs.readFile(path.join(inSandboxSkillsRoot, skill.runtimeName, "SKILL.md"), "utf8"),
|
||||
).resolves.toContain("# review");
|
||||
|
||||
@@ -7,6 +7,7 @@ import path from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { parseFrontmatterMarkdown } from "@paperclipai/shared";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type {
|
||||
AdapterBillingType,
|
||||
@@ -85,6 +86,7 @@ import {
|
||||
type PaperclipSkillEntry,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { shellQuote } from "@paperclipai/adapter-utils/ssh";
|
||||
import { createEphemeralSessionEnvironmentStore } from "./ephemeral-session-environment.js";
|
||||
import {
|
||||
createAcpRuntime,
|
||||
createAgentRegistry,
|
||||
@@ -100,7 +102,6 @@ import {
|
||||
type AcpRuntimeTurnResult,
|
||||
type AcpRuntimeUsageBreakdown,
|
||||
type AcpRuntimeUsageCost,
|
||||
type AcpSessionStore,
|
||||
} from "acpx/runtime";
|
||||
import {
|
||||
ACPX_DUPLEX_LOSS_CANCEL_DEADLINE_MS,
|
||||
@@ -1183,11 +1184,24 @@ async function prepareClaudeSkillRuntime(input: {
|
||||
}
|
||||
|
||||
const selectedNames = materializedNames.sort();
|
||||
const skillDescriptions = await Promise.all(selectedNames.map(async (name) => {
|
||||
const file = path.join(skillsHome, name, "SKILL.md");
|
||||
let description = "";
|
||||
try {
|
||||
const parsed = parseFrontmatterMarkdown(await fs.readFile(file, "utf8"));
|
||||
description = asString(parsed.frontmatter.description, "").replace(/\s+/g, " ").trim().slice(0, 512);
|
||||
} catch {
|
||||
// A readable skill without valid routing metadata remains available.
|
||||
// Do not substitute its body for a missing description.
|
||||
}
|
||||
return `- ${name}${description ? `: ${description}` : ""} (file: ${file})`;
|
||||
}));
|
||||
const promptInstructions = selectedNames.length > 0
|
||||
? [
|
||||
"Paperclip has materialized selected runtime skills for this ACPX Claude session.",
|
||||
`Skill root: ${skillsHome}`,
|
||||
`Selected skills: ${selectedNames.join(", ")}`,
|
||||
...skillDescriptions,
|
||||
"When a task calls for one of these skills, read its SKILL.md from that root and follow it.",
|
||||
].join("\n")
|
||||
: "";
|
||||
@@ -4249,26 +4263,9 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
|
||||
flushChildStderr(childStderrState);
|
||||
childStderrState.logPath = prepared.childStderrLogPath;
|
||||
const persistedRuntimeStore = createRuntimeStore({ stateDir: prepared.stateDir });
|
||||
const runtimeStore: AcpSessionStore = {
|
||||
async load(id) {
|
||||
const record = await persistedRuntimeStore.load(id);
|
||||
if (!record) return undefined;
|
||||
// ACPX resumes from the stored session options rather than the
|
||||
// options passed to ensureSession. Keep conversation state, but
|
||||
// launch the provider with this run's credentials and scratch paths.
|
||||
return {
|
||||
...record,
|
||||
acpx: {
|
||||
...record.acpx,
|
||||
session_options: {
|
||||
...record.acpx?.session_options,
|
||||
env: { ...prepared.env },
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
save: (record) => persistedRuntimeStore.save(record),
|
||||
};
|
||||
// Resume with this run's launch environment; keep it out of the saved
|
||||
// conversation record without mutating the live runtime's options.
|
||||
const runtimeStore = createEphemeralSessionEnvironmentStore(persistedRuntimeStore, prepared.env);
|
||||
const runtimeOptions: PaperclipAcpRuntimeOptions = {
|
||||
cwd: prepared.cwd,
|
||||
// Host-only spawn cwd for the relay proxy on the remote process-session
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { captureNativeDefault, gradeNativeDefault, nativeCompletionProfile, nativeCompletionWorkspaceDigest, NATIVE_MASTER_DEFAULT_SHA256, type NativeDefaultReceipt } from "./native-completion-defaults.js";
|
||||
import { mkdtemp, mkdir, writeFile, symlink, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -58,7 +59,7 @@ describe("native production defaults", () => {
|
||||
expect(original.instructionsBundle).toBeDefined();
|
||||
expect(() => nativeCompletionProfile({ ...profile, generation: "legacy" })).toThrow();
|
||||
});
|
||||
it("hashes actual public bundle bytes before execution without publishing the body", async () => {
|
||||
it("captures current default configuration independently of the frozen native qualification contract", async () => {
|
||||
const content = readFileSync(new URL("../../server/src/onboarding-assets/default/AGENTS.md", import.meta.url), "utf8");
|
||||
const called: string[] = [];
|
||||
const api = { async get<T>(path: string): Promise<T> {
|
||||
@@ -67,7 +68,12 @@ describe("native production defaults", () => {
|
||||
: path.includes("instructions-bundle/file") ? { content } : { budgetMonthlyCents: 1000 }) as T;
|
||||
} };
|
||||
const actual = await captureNativeDefault({ api, agentId: "agent", companyId: "company" });
|
||||
expect(gradeNativeDefault(actual).passed).toBe(true);
|
||||
const currentSha256 = createHash("sha256").update(content).digest("hex");
|
||||
expect(actual.files).toEqual([{ path: "AGENTS.md", sha256: currentSha256, bytes: Buffer.byteLength(content) }]);
|
||||
// The archived native comparison intentionally pins its original master
|
||||
// context. Accurate capture of a later configuration does not qualify it
|
||||
// against those historical live results.
|
||||
expect(gradeNativeDefault(actual).passed).toBe(currentSha256 === NATIVE_MASTER_DEFAULT_SHA256);
|
||||
expect(called).toContain("/api/agents/agent/instructions-bundle/file?path=AGENTS.md");
|
||||
expect(JSON.stringify(actual)).not.toContain(content);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user