Files
PaperClipAI/server/src/__tests__/agent-instructions-service.test.ts
T
DottaandDev Agent 25cf079ec5 feat(runner): add Codex-native application integration (#12591)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The runner package is useful only when the application can start,
observe, and recover a native Codex run safely.
> - Existing direct adapters must keep their current execution and
finalization paths.
> - The application boundary therefore needs additive persistence,
authorization, coordination, and recovery behind an explicit
experimental adapter.
> - This pull request adds that Codex-only boundary without activating
generalized providers, remote environments, or the later task/SDK
surfaces.

## Linked Issues or Issue Description

**Subsystem affected**

Shared contracts, database persistence, adapter utilities, server
native-runtime services, and the experimental Paperclip Runner adapter.

**Problem or motivation**

The already-landed runner package has a qualified Codex path, but the
application needs durable native-run state, guarded runtime selection,
authenticated coordination, tool security, finalization, and recovery
before the experimental adapter can be exercised safely.

**Proposed solution**

Add a Codex-only `paperclip_runner` application path behind the existing
default-off native-runner setting. Bind native state and coordination to
company/run identity, preserve persisted-run recovery, and leave every
direct adapter on its existing legacy execution path.

**Alternatives considered**

The earlier stack boundary introduced a generalized executor and
remote-environment lifecycle here. That made this PR depend on
implementations in higher PRs and changed reusable sandbox behavior
globally. Those pieces are now deferred together to #12592.

**Roadmap alignment**

ROADMAP.md does not list a conflicting native-runner integration
project. This change adds the application boundary for the existing
Runner architecture.

## What Changed

- Added native run/result/finalization/provider-trace persistence,
shared validators, and idempotent migration/replay coverage.
- Added guarded Codex-only runtime selection, authenticated PRP
coordination, recovery, finalization, and interaction services.
- Added run/company-bound tool-gateway authorization, credential
redaction, SSRF protections, and replay-safe behavior.
- Added the explicit `paperclip_runner` adapter behind the default-off
rollout setting.
- Preserved legacy answered-question wake projection and direct-adapter
execution/finalization paths.
- Hardened cancellation so only owned in-memory child processes are
signaled; persisted recycled PIDs/process groups are never trusted.
- Retained the narrow Claude ACPX isolated-context security follow-up
discovered after #12590.
- Deferred the generalized executor, provider ingress, remote lifecycle,
SDK/lab/eval work, release-process changes, and lockfile.

## Verification

- Changed-file delta against `master`: 133 files.
- GitHub Actions is the authoritative verification environment for this
PR.
- Full CI, security, and Greptile review will run on this lowest
unmerged stack PR.
- Local tests/build/typecheck were not run because this checkout is
resource constrained.
- Static diff/reference checks pass, and `pnpm-lock.yaml` is unchanged.

## Risks

- This touches central heartbeat and agent-route code, so legacy
compatibility is the primary risk.
- Runtime selection remains Codex-only and explicit; direct Codex,
Claude, OpenCode, process, HTTP, and plugin adapters remain on their
existing paths.
- Fresh native starts fail closed while the rollout flag is off;
persisted native records remain readable and recoverable.
- Cancellation, company/run binding, tool calls, status decisions, and
completion writes are guarded or replay-safe.

> For core feature work, check [ROADMAP.md](ROADMAP.md) first and
discuss it in #dev before opening the PR. Feature PRs that overlap with
planned core work may need to be redirected.

## Model Used

OpenAI Codex, GPT-5.6, with repository tools, code execution, and
parallel agent review.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either linked existing issues or described the issue in-PR
following the relevant issue template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [ ] I have run tests locally and they pass — GitHub Actions is
authoritative for this resource-constrained checkout
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented risks above
- [ ] All Paperclip CI and security gates are green
- [ ] Greptile is 5/5 with no open actionable findings
- [x] I will address all Greptile and reviewer comments before merge

## Stack

- Position: 3 of 5 overall; lowest of 3 currently unmerged
- Base: `master`
- Previous:
[#12590](https://github.com/paperclipai/paperclip/pull/12590), qualified
Claude ACPX runtime — merged
- Next: [#12592](https://github.com/paperclipai/paperclip/pull/12592),
generalized Codex executor, task experience, and developer SDKs

---------

Co-authored-by: Dev Agent <dev@paperclip.ing>
2026-08-31 14:38:38 -05:00

385 lines
15 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { agentInstructionsService } from "../services/agent-instructions.js";
type TestAgent = {
id: string;
companyId: string;
name: string;
adapterConfig: Record<string, unknown>;
};
async function makeTempDir(prefix: string) {
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
}
function makeAgent(adapterConfig: Record<string, unknown>): TestAgent {
return {
id: "agent-1",
companyId: "company-1",
name: "Agent 1",
adapterConfig,
};
}
describe("agent instructions service", () => {
const originalPaperclipHome = process.env.PAPERCLIP_HOME;
const originalPaperclipInstanceId = process.env.PAPERCLIP_INSTANCE_ID;
const cleanupDirs = new Set<string>();
afterEach(async () => {
if (originalPaperclipHome === undefined) delete process.env.PAPERCLIP_HOME;
else process.env.PAPERCLIP_HOME = originalPaperclipHome;
if (originalPaperclipInstanceId === undefined) delete process.env.PAPERCLIP_INSTANCE_ID;
else process.env.PAPERCLIP_INSTANCE_ID = originalPaperclipInstanceId;
await Promise.all([...cleanupDirs].map(async (dir) => {
await fs.rm(dir, { recursive: true, force: true });
cleanupDirs.delete(dir);
}));
});
it("copies the existing bundle into the managed root when switching to managed mode", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-home-");
const externalRoot = await makeTempDir("paperclip-agent-instructions-external-");
cleanupDirs.add(paperclipHome);
cleanupDirs.add(externalRoot);
process.env.PAPERCLIP_HOME = paperclipHome;
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
await fs.writeFile(path.join(externalRoot, "AGENTS.md"), "# External Agent\n", "utf8");
await fs.mkdir(path.join(externalRoot, "docs"), { recursive: true });
await fs.writeFile(path.join(externalRoot, "docs", "TOOLS.md"), "## Tools\n", "utf8");
const svc = agentInstructionsService();
const agent = makeAgent({
instructionsBundleMode: "external",
instructionsRootPath: externalRoot,
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: path.join(externalRoot, "AGENTS.md"),
});
const result = await svc.updateBundle(agent, { mode: "managed" });
expect(result.bundle.mode).toBe("managed");
expect(result.bundle.managedRootPath).toBe(
path.join(
paperclipHome,
"instances",
"test-instance",
"companies",
"company-1",
"agents",
"agent-1",
"instructions",
),
);
expect(result.bundle.files.map((file) => file.path)).toEqual(["AGENTS.md", "docs/TOOLS.md"]);
await expect(fs.readFile(path.join(result.bundle.managedRootPath, "AGENTS.md"), "utf8")).resolves.toBe("# External Agent\n");
await expect(fs.readFile(path.join(result.bundle.managedRootPath, "docs", "TOOLS.md"), "utf8")).resolves.toBe("## Tools\n");
});
it("creates the target entry file when switching to a new external root", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-home-");
const managedRoot = path.join(
paperclipHome,
"instances",
"test-instance",
"companies",
"company-1",
"agents",
"agent-1",
"instructions",
);
const externalRoot = await makeTempDir("paperclip-agent-instructions-new-external-");
cleanupDirs.add(paperclipHome);
cleanupDirs.add(externalRoot);
process.env.PAPERCLIP_HOME = paperclipHome;
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
await fs.mkdir(managedRoot, { recursive: true });
await fs.writeFile(path.join(managedRoot, "AGENTS.md"), "# Managed Agent\n", "utf8");
const svc = agentInstructionsService();
const agent = makeAgent({
instructionsBundleMode: "managed",
instructionsRootPath: managedRoot,
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: path.join(managedRoot, "AGENTS.md"),
});
const result = await svc.updateBundle(agent, {
mode: "external",
rootPath: externalRoot,
entryFile: "docs/AGENTS.md",
});
expect(result.bundle.mode).toBe("external");
expect(result.bundle.rootPath).toBe(externalRoot);
await expect(fs.readFile(path.join(externalRoot, "docs", "AGENTS.md"), "utf8")).resolves.toBe("# Managed Agent\n");
});
it("filters junk files, dependency bundles, and python caches from bundle listings and exports", async () => {
const externalRoot = await makeTempDir("paperclip-agent-instructions-ignore-");
cleanupDirs.add(externalRoot);
await fs.writeFile(path.join(externalRoot, "AGENTS.md"), "# External Agent\n", "utf8");
await fs.writeFile(path.join(externalRoot, ".gitignore"), "node_modules/\n", "utf8");
await fs.writeFile(path.join(externalRoot, ".DS_Store"), "junk", "utf8");
await fs.mkdir(path.join(externalRoot, "docs"), { recursive: true });
await fs.writeFile(path.join(externalRoot, "docs", "TOOLS.md"), "## Tools\n", "utf8");
await fs.writeFile(path.join(externalRoot, "docs", "module.pyc"), "compiled", "utf8");
await fs.writeFile(path.join(externalRoot, "docs", "._TOOLS.md"), "appledouble", "utf8");
await fs.mkdir(path.join(externalRoot, "node_modules", "pkg"), { recursive: true });
await fs.writeFile(path.join(externalRoot, "node_modules", "pkg", "index.js"), "export {};\n", "utf8");
await fs.mkdir(path.join(externalRoot, "python", "__pycache__"), { recursive: true });
await fs.writeFile(
path.join(externalRoot, "python", "__pycache__", "module.cpython-313.pyc"),
"compiled",
"utf8",
);
await fs.mkdir(path.join(externalRoot, ".pytest_cache"), { recursive: true });
await fs.writeFile(path.join(externalRoot, ".pytest_cache", "README.md"), "cache", "utf8");
const svc = agentInstructionsService();
const agent = makeAgent({
instructionsBundleMode: "external",
instructionsRootPath: externalRoot,
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: path.join(externalRoot, "AGENTS.md"),
});
const bundle = await svc.getBundle(agent);
const exported = await svc.exportFiles(agent);
expect(bundle.files.map((file) => file.path)).toEqual([".gitignore", "AGENTS.md", "docs/TOOLS.md"]);
expect(Object.keys(exported.files).sort((left, right) => left.localeCompare(right))).toEqual([
".gitignore",
"AGENTS.md",
"docs/TOOLS.md",
]);
});
it.skipIf(process.platform === "win32")("rejects instruction symlinks for immutable runner snapshots without changing legacy exports", async () => {
const externalRoot = await makeTempDir("paperclip-agent-instructions-symlink-");
const outsideRoot = await makeTempDir("paperclip-agent-instructions-outside-");
cleanupDirs.add(externalRoot);
cleanupDirs.add(outsideRoot);
await fs.writeFile(path.join(externalRoot, "AGENTS.md"), "Read sibling.md\n", "utf8");
await fs.writeFile(path.join(outsideRoot, "secret.md"), "must not enter the bundle\n", "utf8");
await fs.symlink(path.join(outsideRoot, "secret.md"), path.join(externalRoot, "sibling.md"));
const agent = makeAgent({
instructionsBundleMode: "external",
instructionsRootPath: externalRoot,
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: path.join(externalRoot, "AGENTS.md"),
});
const svc = agentInstructionsService();
await expect(svc.exportFiles(agent)).resolves.toMatchObject({
files: { "AGENTS.md": "Read sibling.md\n" },
});
await expect(svc.exportFiles(agent, { rejectSymlinks: true }))
.rejects.toThrow("Instructions bundle may not contain symlinks: sibling.md");
});
it("recovers a managed bundle from disk when bundle config metadata is missing", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-recover-");
cleanupDirs.add(paperclipHome);
process.env.PAPERCLIP_HOME = paperclipHome;
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
const managedRoot = path.join(
paperclipHome,
"instances",
"test-instance",
"companies",
"company-1",
"agents",
"agent-1",
"instructions",
);
await fs.mkdir(managedRoot, { recursive: true });
await fs.writeFile(path.join(managedRoot, "AGENTS.md"), "# Recovered Agent\n", "utf8");
const svc = agentInstructionsService();
const agent = makeAgent({});
const bundle = await svc.getBundle(agent);
const exported = await svc.exportFiles(agent);
expect(bundle.mode).toBe("managed");
expect(bundle.rootPath).toBe(managedRoot);
expect(bundle.files.map((file) => file.path)).toEqual(["AGENTS.md"]);
expect(exported.files).toEqual({ "AGENTS.md": "# Recovered Agent\n" });
});
it("prefers the managed bundle on disk when managed metadata points at a stale root", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-stale-managed-");
const staleRoot = await makeTempDir("paperclip-agent-instructions-stale-root-");
cleanupDirs.add(paperclipHome);
cleanupDirs.add(staleRoot);
process.env.PAPERCLIP_HOME = paperclipHome;
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
const managedRoot = path.join(
paperclipHome,
"instances",
"test-instance",
"companies",
"company-1",
"agents",
"agent-1",
"instructions",
);
await fs.mkdir(managedRoot, { recursive: true });
await fs.writeFile(path.join(managedRoot, "AGENTS.md"), "# Managed Agent\n", "utf8");
const svc = agentInstructionsService();
const agent = makeAgent({
instructionsBundleMode: "managed",
instructionsRootPath: staleRoot,
instructionsEntryFile: "docs/MISSING.md",
instructionsFilePath: path.join(staleRoot, "docs", "MISSING.md"),
});
const bundle = await svc.getBundle(agent);
const exported = await svc.exportFiles(agent);
expect(bundle.mode).toBe("managed");
expect(bundle.rootPath).toBe(managedRoot);
expect(bundle.entryFile).toBe("AGENTS.md");
expect(bundle.files.map((file) => file.path)).toEqual(["AGENTS.md"]);
expect(bundle.warnings).toEqual([
`Recovered managed instructions from disk at ${managedRoot}; ignoring stale configured root ${staleRoot}.`,
"Recovered managed instructions entry file from disk as AGENTS.md; previous entry docs/MISSING.md was missing.",
]);
expect(exported.files).toEqual({ "AGENTS.md": "# Managed Agent\n" });
});
it("heals stale managed metadata when writing bundle files", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-heal-write-");
const staleRoot = await makeTempDir("paperclip-agent-instructions-heal-write-stale-");
cleanupDirs.add(paperclipHome);
cleanupDirs.add(staleRoot);
process.env.PAPERCLIP_HOME = paperclipHome;
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
const managedRoot = path.join(
paperclipHome,
"instances",
"test-instance",
"companies",
"company-1",
"agents",
"agent-1",
"instructions",
);
await fs.mkdir(path.join(managedRoot, "docs"), { recursive: true });
await fs.writeFile(path.join(managedRoot, "AGENTS.md"), "# Managed Agent\n", "utf8");
const svc = agentInstructionsService();
const agent = makeAgent({
instructionsBundleMode: "managed",
instructionsRootPath: staleRoot,
instructionsEntryFile: "docs/MISSING.md",
instructionsFilePath: path.join(staleRoot, "docs", "MISSING.md"),
});
const result = await svc.writeFile(agent, "docs/TOOLS.md", "## Tools\n");
expect(result.adapterConfig).toMatchObject({
instructionsBundleMode: "managed",
instructionsRootPath: managedRoot,
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: path.join(managedRoot, "AGENTS.md"),
});
await expect(fs.readFile(path.join(managedRoot, "docs", "TOOLS.md"), "utf8")).resolves.toBe("## Tools\n");
});
it("heals stale managed metadata when deleting bundle files", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-heal-delete-");
const staleRoot = await makeTempDir("paperclip-agent-instructions-heal-delete-stale-");
cleanupDirs.add(paperclipHome);
cleanupDirs.add(staleRoot);
process.env.PAPERCLIP_HOME = paperclipHome;
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
const managedRoot = path.join(
paperclipHome,
"instances",
"test-instance",
"companies",
"company-1",
"agents",
"agent-1",
"instructions",
);
await fs.mkdir(path.join(managedRoot, "docs"), { recursive: true });
await fs.writeFile(path.join(managedRoot, "AGENTS.md"), "# Managed Agent\n", "utf8");
await fs.writeFile(path.join(managedRoot, "docs", "TOOLS.md"), "## Tools\n", "utf8");
const svc = agentInstructionsService();
const agent = makeAgent({
instructionsBundleMode: "managed",
instructionsRootPath: staleRoot,
instructionsEntryFile: "docs/MISSING.md",
instructionsFilePath: path.join(staleRoot, "docs", "MISSING.md"),
});
const result = await svc.deleteFile(agent, "docs/TOOLS.md");
expect(result.adapterConfig).toMatchObject({
instructionsBundleMode: "managed",
instructionsRootPath: managedRoot,
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: path.join(managedRoot, "AGENTS.md"),
});
await expect(fs.stat(path.join(managedRoot, "docs", "TOOLS.md"))).rejects.toThrow();
expect(result.bundle.files.map((file) => file.path)).toEqual(["AGENTS.md"]);
});
it("recovers the managed bundle when stale root metadata is present but mode is missing", async () => {
const paperclipHome = await makeTempDir("paperclip-agent-instructions-partial-managed-");
const staleRoot = await makeTempDir("paperclip-agent-instructions-partial-root-");
cleanupDirs.add(paperclipHome);
cleanupDirs.add(staleRoot);
process.env.PAPERCLIP_HOME = paperclipHome;
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
const managedRoot = path.join(
paperclipHome,
"instances",
"test-instance",
"companies",
"company-1",
"agents",
"agent-1",
"instructions",
);
await fs.mkdir(managedRoot, { recursive: true });
await fs.writeFile(path.join(managedRoot, "AGENTS.md"), "# Managed Agent\n", "utf8");
const svc = agentInstructionsService();
const agent = makeAgent({
instructionsRootPath: staleRoot,
instructionsEntryFile: "docs/MISSING.md",
});
const bundle = await svc.getBundle(agent);
const exported = await svc.exportFiles(agent);
expect(bundle.mode).toBe("managed");
expect(bundle.rootPath).toBe(managedRoot);
expect(bundle.entryFile).toBe("AGENTS.md");
expect(bundle.files.map((file) => file.path)).toEqual(["AGENTS.md"]);
expect(bundle.warnings).toEqual([
`Recovered managed instructions from disk at ${managedRoot}; ignoring stale configured root ${staleRoot}.`,
"Recovered managed instructions entry file from disk as AGENTS.md; previous entry docs/MISSING.md was missing.",
]);
expect(exported.files).toEqual({ "AGENTS.md": "# Managed Agent\n" });
});
});