Add execution-owned Runner launch preparation (#16997)

## Thinking Path

> - Paperclip manages work for AI agents.
> - Runner can execute in a different environment from Core.
> - Core owns provider selection, policy, and resource assignments.
> - The execution environment must prepare its own files and paths.
> - A shared contract must keep these responsibilities separate.
> - This PR adds that contract to the Runner execution driver.

## Linked Issues or Issue Description

**Problem or motivation**
Runner transport prepares provider files on the controller before
launch. An execution owner cannot prepare those files in its own
environment through the existing driver.

**Proposed solution**
Add `prepareLaunch` to the execution driver. Supply versioned Core
requirements. Accept a configuration ID, requirements digest, and
ordinary provider descriptor. Validate Core policy and assignments
before launch. Keep existing provider session checks.

**Alternatives considered**
Provider-specific ownership flags would spread the boundary across
backends. Shell preparation would expose process details in the plugin
API.

Refs #16993, #16977. Both prerequisites are merged. This PR now targets
master. Native plugin integration and execution-host preparation follow
separately.

PR #16978 contains an earlier, unmerged prototype. Its
`persistentAgentHome` mode and skill path-map call will be replaced by
this preparation contract before activation. This PR preserves current
master callers of the skill helper.

## What Changed

- Add the optional preparation callback and an explicit Runner artifact
identity.
- Keep the built-in preparation path as the default.
- Validate provider policy and resource assignment identity. Preserve
instruction text when resource locations change.
- Persist the attachment seed after an authority epoch rotation, so a
later controller restart can recover the same run.
- Preserve explicit context removal on a new run. Only an omitted
context can reuse the saved context. Remove its registered instruction
framing when context is removed.
- Save exact requirements and configurations in a private file outside
Runner commands. Retain the active record across failed attachments. Use
current grants for new runs. Keep incompatible session changes rejected.
- Join delayed preparation during startup and warm attachment cleanup.
Retain an unsettled checkpoint after a timeout. Prevent work after
cancellation.
- Use the supplied Runner artifact for interrupted handoff recovery.
Restore the exact target configuration at each handoff crash boundary.
Check the Runner state when the final ACK may be lost.
- Bind the changed shared sources to Copilot profile v51. Preserve the
v50 receipt and current integrity checks.

## Verification

- Workspace typecheck passed.
- All 487 affected Runner transport, control-plane, preparation store,
builder, policy, and profile tests passed on commit `eaf8ae8d1`.
- Workspace build and typecheck passed again on commit
`eaf8ae8d12c7ef2d22d226e4b3f05fdbf79ef4cb`.
- Both warm attachment cleanup regressions fail before the fix. They
pass after the fix. The tests cover delayed preparation, timeout
retention, and concurrent attachment rejection.
- The routed handoff regression fails with a missing controller binary
before the fix. It passes after the fix. The execution owner launches
the real fixture Runner.
- The 200,000-byte instruction regression fails before the fix and
passes after it. Four prepared handoff crash cases verify exact request
and configuration recovery. The store tests verify private file mode,
conflicting records, corrupt data, and active-run retention.
- A real Runner with a controlled fake provider verifies execution-host
paths, exact recovery, warm reuse, incompatible profile rejection, and
cancellation. It does not contact an external provider.
- The regression found undefined settings in strict PRP command hashing.
Requirements now contain only JSON values. The warm transition test
passes without changing PRP validation.
- The broader root suite on the prerequisite finished with 22,420
passing tests, seven failed tests in unchanged Git and diagnostic code,
and two suite setup errors because Cargo was absent from its PATH. This
PR does not claim a passing full root suite.
- The CI Build job passed compilation and production runtime boot, then
its runner received a shutdown signal during the clean public install
check. Other jobs reported lost communication with their self-hosted
runners. The failed jobs passed on retry on the same commit; successful
jobs kept their results.
- The current Superagent security scan is inconclusive because its
pinned merge-base checkout failed or timed out. GitHub rejected the
check rerun request through the available API. This is not a passing
security scan.
- Standard Greptile and Greptile Apex each returned 5/5 on `eaf8ae8d1`.
All review threads are resolved. CI passed on attempt 2. Current checks
have 55 successes, four optional skips, and the inconclusive security
scan described above. The branch is ready, mergeable, and current with
master.
- No live provider qualification or deployment was run.

## Risks

- A preparation owner must store configurations and keep retries bound
to the same task and requirements.
- Copilot source binding requires a new profile identity. This PR does
not claim fresh live qualification.
- The callback is an SDK extension. This PR does not connect it to a
plugin task process owner or add a Launcher endpoint.

## Model Used

- OpenAI Codex, GPT-6. Reasoning and repository tools. The exact serving
model identifier and context size are not exposed in this session.

## 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 (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [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 any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip authored and GitHub committed 2026-10-20 14:24:13 -07:00
1 parent 15176963b1
commit 2a5a323e57
21 files changed
+1048 -152

No files matched your search

+44 -3
View File
@@ -7,6 +7,47 @@ The built-in TypeScript implementation constructs the provider descriptor in
`createRunnerProviderLaunch`. The transport sends this descriptor with
`run.prepare`. Provider backends keep their existing validation and session rules.
This separation does not change file preparation. The SDK still prepares local
files and stages remote files through the existing execution driver.
It does not enable plugin-owned launch preparation.
By default, the SDK prepares local files. It stages remote files through the
existing execution driver.
## Environment-owned preparation
An execution driver can supply `prepareLaunch`.
The callback receives `RunnerProviderLaunchRequirements`.
It returns `PreparedRunnerProviderLaunch`.
Requirements contain the selected provider, model, policy, run identity, and
runtime context. They do not contain task text, process callbacks, provider
credentials, or controller executable choices. Resource paths identify source
locations. They do not grant filesystem access on the execution host.
The environment resolves its resources and prepares the provider descriptor.
It can use `bindRunnerLaunchResources` to replace registered context locations.
It can use `createRunnerProviderLaunch` for the existing descriptor projection.
The transport does not perform its built-in file preparation in this mode.
The process owner supplies the Runner artifact and process lifecycle.
The result contains a configuration ID, a requirements digest, and a provider
descriptor. The transport checks Core's selected profile and policy before
registration and launch. Core still supplies authorized tools and completion
criteria through PRP. A failed preparation does not enable a fallback launch.
The transport saves the exact requirements and prepared configuration in its
private `launch-preparation.json` file. The file holds at most two records.
A new attachment keeps the active run's record. Runner commands contain the
provider descriptor once. They do not contain these recovery records.
Recovery within the same run must return the same configuration. This rule also
applies during an interrupted warm attachment. A new run uses its current
requirements. For a new run, `runtimeContext: null` removes the assigned context.
Only an omitted context can reuse the previous context.
Provider session compatibility checks remain in their existing
backends. Close waits for preparation within its deadline. A timeout retains an
unsettled checkpoint and prevents reuse.
## Plugin task protocol
The [environment task API](plugins/PLUGIN_ENVIRONMENT_TASKS.md#launch-preparation)
defines the plugin preparation exchange. This callback does not implement a
plugin task process owner or a Launcher endpoint. Those integrations are
separate changes.
+6
View File
@@ -46,6 +46,12 @@ specifications are internal to built-in execution; they are not a plugin wire
contract. Existing flat transport options remain compatible through this shared
interface. This refactor does not add Launcher APIs or change harness capabilities.
An execution owner can supply `prepareLaunch`, a Runner artifact identity, and
process and connection hooks. Core supplies the launch requirements. The owner
returns a provider descriptor bound to those requirements. Core checks the policy
and assigned resources. See [Runner launch preparation](../../doc/runner-launch-preparation.md)
for the contract and recovery record.
Connection continuations inspect the harness descriptor's optional
`toolRefreshOnResume` capability. Native Codex, Claude Managed Agents, AgentCore,
OpenCode, and qualified Claude/Codex/Grok ACPX profiles expose it; unqualified
+2 -2
View File
@@ -32,12 +32,12 @@
"requiresProviderPolicy": true
},
"copilot": {
"agentProfileVersion": 50,
"agentProfileVersion": 51,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:d8f89c8e75883a793122ed908016b7730ededa6c3df741f23298c2445d7e6fc4",
"commandDigest": "sha256:8c1cddce201eb0e3f2792cc5a8c79edf36b0ce823e728255c1897f95c74a3866",
"requiresProviderPolicy": true
},
"claude": {
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
agent_runtime_package: None,
agent_runtime_version: None,
command_digest:
"sha256:d8f89c8e75883a793122ed908016b7730ededa6c3df741f23298c2445d7e6fc4",
"sha256:8c1cddce201eb0e3f2792cc5a8c79edf36b0ce823e728255c1897f95c74a3866",
requires_provider_policy: true,
},
"claude" => AcpxReleaseProfile {
@@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const fixture = resolve(root, "test/fixtures/copilot-profile-v50-identity.json");
const fixture = resolve(root, "test/fixtures/copilot-profile-v51-identity.json");
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
const declaration = { ...historical.declaration, agentProfileVersion: 50 };
const declaration = { ...historical.declaration, agentProfileVersion: 51 };
delete declaration.permissionIdentitySourceSha256;
const sources = {
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
@@ -32,6 +32,7 @@ const sources = {
nativeBackendFactorySourceSha256: "src/backends/native-backend-factory.ts",
nativeInstructionsSourceSha256: "src/backends/runtime-context.ts",
providerLaunchSourceSha256: "src/live/runner-provider-launch.ts",
launchPreparationStoreSourceSha256: "src/live/runner-launch-preparation-store.ts",
runtimeRequestBridgeSourceSha256: "src/live/runnerd-codex-transport.ts",
runtimeRequestFacadeSourceSha256: "src/drivers/codex/codex-session-notifications.ts",
runtimeRequestNotificationSourceSha256: "src/drivers/codex/codex-thread-normalization.ts",
@@ -188,6 +188,27 @@ export function composeNativeSystemInstructions(context: NativeRuntimeContextSna
].filter(Boolean).join("\n\n");
}
export function retargetNativeSystemInstructions(
instructions: string,
prior: NativeRuntimeContextSnapshot,
current: NativeRuntimeContextSnapshot | null,
): string {
const priorPrefix = prior.prompt.text;
const priorSuffix = composeNativeSystemInstructions(prior, "").slice(priorPrefix.length);
// Legacy callers can supply opaque system instructions. Only the composer's
// exact framing identifies a trusted asset block; never rewrite their text.
if (!instructions.startsWith(priorPrefix) || !instructions.endsWith(priorSuffix)) {
return instructions;
}
const custom = instructions.slice(priorPrefix.length, -priorSuffix.length);
if (custom && !custom.startsWith("\n\n")) return instructions;
// Keep custom entry bytes intact, including historical path examples or an
// identical paragraph quoted inside the entry. Only replace the final block.
return (current?.prompt.text ?? priorPrefix) + custom + (current
? composeNativeSystemInstructions(current, "").slice(current.prompt.text.length)
: "");
}
/** Explicit per-turn selection; availability alone never invokes a skill. */
export interface NativeSkillInput {
type: "skill";
@@ -2987,9 +2987,11 @@ describe("DurablePrpControlPlane", () => {
const unusedTicket = core.issueBootstrapTicket();
peer = await authenticateVersion(core, core.issueBootstrapTicket());
const oldToken = peer!.leaseToken!;
core.persistRunAttachTemplate({ provider: { kind: "mock", run: "prior" } });
const command = core.queueCommand(
"run.attach",
{
provider: { kind: "mock", run: "next" },
paperclipNextAuthority: {
identity: nextIdentity,
connection: { mode: "connect", connectUrl: core.connectUrl },
@@ -3334,6 +3336,7 @@ describe("DurablePrpControlPlane", () => {
);
expect(successor).not.toBeNull();
expect(core.store.state.identity).toEqual(nextIdentity);
expect(core.store.state.runAttachTemplate).toEqual({ provider: { kind: "mock", run: "next" } });
expect(
(successor!.welcome.payload as Record<string, unknown>)
.pendingCommands,
@@ -1133,7 +1133,7 @@ function verifyPrivateRegularFile(file: Stats, path: string): void {
}
}
function readPrivateFile(path: string): string | null {
export function readPrivateFile(path: string): string | null {
let descriptor: number;
try {
descriptor = openSync(
@@ -1171,7 +1171,7 @@ function syncParentDirectory(path: string): void {
}
}
function atomicPrivateWrite(path: string, contents: string): void {
export function atomicPrivateWrite(path: string, contents: string): void {
const temporary = resolve(
dirname(path),
`.${path.split(/[\\/]/).at(-1)}.${randomUUID()}.tmp`,
@@ -2736,7 +2736,8 @@ export class DurablePrpControlPlane {
phase: "activated",
};
candidate.leases = { [lease.credentialId]: structuredClone(lease) };
candidate.runAttachTemplate = this.#store.state.runAttachTemplate;
const { paperclipNextAuthority: _authority, ...runAttachTemplate } = transition.command.payload;
candidate.runAttachTemplate = structuredClone(runAttachTemplate);
this.#store.commit(candidate);
this.#identity = structuredClone(candidate.identity);
lease = this.#store.state.leases[lease.credentialId]!;
@@ -1,13 +1,16 @@
import type { RunnerLaunchPreparer } from "../live/runner-provider-launch.js";
import type { DurablePrpControlPlane, RunnerProcessConnection, RunnerProcessHandle, RunnerProcessLaunchSpec } from "./durable-prp-control-plane.js";
import type { DurableRecoveryIdentity } from "../contracts/durable-recovery.js";
/** Environment ownership shared by local, SSH, and sandbox Runner execution.
* Preparation belongs to connection registration, before launch; activation
* happens after dispatch. PRP authority and session semantics stay with Core.
* Filesystem paths and process specifications are private to the built-in driver,
* not a serializable plugin API.
* Provider preparation can be owned by the execution environment. Process
* specifications stay private to the driver; plugins receive versioned requirements.
*/
export interface RunnerExecutionDriver {
/** Own provider configuration and file preparation in the execution environment. */
prepareLaunch?: RunnerLaunchPreparer;
/** Prepare resources and register or acquire the connection before launch. */
controlPlaneRegistration?: (
authority: DurablePrpControlPlane,
@@ -86,10 +86,10 @@ async function withCopilotTestPlatform<T>(arch: "x64" | "arm64", run: () => Prom
vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi.fn() }));
describe("Copilot build-owned installation", () => {
it("admits the current profile after launch preparation extraction", () => {
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(50);
it("admits the current profile with environment launch preparation", () => {
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(51);
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBeUndefined();
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:d8f89c8e75883a793122ed908016b7730ededa6c3df741f23298c2445d7e6fc4");
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:8c1cddce201eb0e3f2792cc5a8c79edf36b0ce823e728255c1897f95c74a3866");
});
it("preserves v49 while binding the extracted provider launch builder", () => {
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "copilot",
"agentProfileVersion": 50,
"agentProfileVersion": 51,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:d8f89c8e75883a793122ed908016b7730ededa6c3df741f23298c2445d7e6fc4",
"commandDigest": "sha256:8c1cddce201eb0e3f2792cc5a8c79edf36b0ce823e728255c1897f95c74a3866",
"permissionPolicy": "interactive"
},
"claude": {
@@ -2,13 +2,13 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
/** Decodable historical profile revisions, not permission to launch them.
* Exact current profile/package/digest admission is checked separately. */
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49;
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50;
type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"];
export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion;
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49],
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50],
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
};
+3
View File
@@ -96,3 +96,6 @@ export { RunnerdMuseDriver, describeRunnerdMuseDriver, type RunnerdMuseDriverOpt
export { RunnerdDotDriver, type RunnerdDotDriverOptions } from "./drivers/dot/runnerd-dot-driver.js";
export type { RunnerExecutionDriver } from "./control-plane/runner-execution-driver.js";
export { bindRunnerLaunchResources, createRunnerProviderLaunch, runnerProviderLaunchRequirements, runnerLaunchRequirementsDigest, validatePreparedRunnerLaunch } from "./live/runner-provider-launch.js";
export type { RunnerProviderLaunchRequirements, RunnerProviderLaunchEnvironment, PreparedRunnerProviderLaunch, RunnerLaunchPreparer } from "./live/runner-provider-launch.js";
@@ -0,0 +1,80 @@
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it } from "vitest";
import { RunnerLaunchPreparationStore } from "./runner-launch-preparation-store.js";
import { createRunnerProviderLaunch, runnerLaunchRequirementsDigest, runnerProviderLaunchRequirements } from "./runner-provider-launch.js";
function preparation(runId: string) {
const requirements = runnerProviderLaunchRequirements({ provider: "codex" },
{ runnerInstanceId: "runner", environmentLeaseId: "lease", normalizedSessionId: "session", runId,
turnId: `turn-${runId}`, itemId: `item-${runId}` }, { model: "selected" }, null, "Core instructions");
return { requirements, prepared: { configurationId: runId, requirementsDigest: runnerLaunchRequirementsDigest(requirements),
provider: createRunnerProviderLaunch(requirements, { options: { codexArgs: [] }, root: "/guest/state", cwd: "/guest/work",
codexHome: "/guest/.codex", codexCommand: "/guest/codex", providerNodeCommand: "", opencodeProxyPath: "", acpxSidecarPath: "" }) } };
}
it("retains only the two latest exact preparation records across restart", () => {
const root = mkdtempSync(join(tmpdir(), "runner-preparation-store-"));
try {
const store = new RunnerLaunchPreparationStore(root);
const first = preparation("first"), second = preparation("second"), third = preparation("third");
for (const [entry, active] of [[first, first], [second, first], [third, second]] as const)
store.save(entry.requirements, entry.prepared, active.requirements.identity);
const restored = new RunnerLaunchPreparationStore(root);
expect(restored.load(first.requirements.identity)).toBeUndefined();
expect(restored.load(second.requirements.identity)).toEqual(second);
expect(restored.load(third.requirements.identity)).toEqual(third);
expect(restored.load({ ...third.requirements.identity, environmentLeaseId: "other" })).toBeUndefined();
if (process.platform !== "win32") expect(statSync(join(root, "launch-preparation.json")).mode & 0o777).toBe(0o600);
} finally { rmSync(root, { recursive: true, force: true }); }
});
it("rejects changed requirements or prepared configurations for the same identity", () => {
const root = mkdtempSync(join(tmpdir(), "runner-preparation-conflict-"));
try {
const store = new RunnerLaunchPreparationStore(root);
const { requirements, prepared } = preparation("run");
store.save(requirements, prepared, requirements.identity);
expect(() => store.save(requirements, { ...prepared, configurationId: "changed" }, requirements.identity))
.toThrow("runner_prepared_launch_recovery_changed");
const changed = { ...requirements, instructions: "Changed instructions" };
expect(() => store.save(changed, { ...prepared, requirementsDigest: runnerLaunchRequirementsDigest(changed),
provider: { ...prepared.provider, instructions: "Changed instructions" } }, requirements.identity))
.toThrow("runner_prepared_launch_recovery_changed");
expect(store.load(requirements.identity)).toEqual({ requirements, prepared });
} finally { rmSync(root, { recursive: true, force: true }); }
});
it("fails closed on corrupt or unsupported recovery metadata", () => {
const root = mkdtempSync(join(tmpdir(), "runner-preparation-corrupt-"));
try {
const store = new RunnerLaunchPreparationStore(root);
const { requirements, prepared } = preparation("run");
store.save(requirements, prepared, requirements.identity);
const path = join(root, "launch-preparation.json");
const contents = JSON.parse(readFileSync(path, "utf8"));
contents.entries[0].prepared.requirementsDigest = `sha256:${"0".repeat(64)}`;
writeFileSync(path, JSON.stringify(contents));
expect(() => store.load(requirements.identity)).toThrow();
writeFileSync(path, JSON.stringify({ ...contents, schema: "unsupported" }));
expect(() => store.load(requirements.identity)).toThrow("runner_launch_recovery_requirements_unavailable");
writeFileSync(path, "{");
expect(() => store.load(requirements.identity)).toThrow();
} finally { rmSync(root, { recursive: true, force: true }); }
});
it("keeps the active run when multiple new attachment attempts fail", () => {
const root = mkdtempSync(join(tmpdir(), "runner-preparation-active-"));
try {
const store = new RunnerLaunchPreparationStore(root);
const active = preparation("active");
store.save(active.requirements, active.prepared, active.requirements.identity);
for (const runId of ["failed-first", "failed-second", "failed-third"]) {
const entry = preparation(runId);
store.save(entry.requirements, entry.prepared, active.requirements.identity);
}
expect(store.load(active.requirements.identity)).toEqual(active);
expect(JSON.parse(readFileSync(join(root, "launch-preparation.json"), "utf8")).entries).toHaveLength(2);
} finally { rmSync(root, { recursive: true, force: true }); }
});
@@ -0,0 +1,54 @@
import { resolve } from "node:path";
import type { DurableRecoveryIdentity } from "../contracts/durable-recovery.js";
import { atomicPrivateWrite, readPrivateFile, durableRecoveryInternals } from "../control-plane/durable-prp-control-plane.js";
import { runnerLaunchRequirementsDigest, validatePreparedRunnerLaunch,
type PreparedRunnerProviderLaunch, type RunnerProviderLaunchRequirements } from "./runner-provider-launch.js";
interface LaunchPreparation {
requirements: RunnerProviderLaunchRequirements;
prepared: PreparedRunnerProviderLaunch;
}
const schema = "paperclip.runner.launch-preparation-store.v1";
const sameIdentity = (left: DurableRecoveryIdentity, right: DurableRecoveryIdentity) =>
Object.keys(right).every(key => left[key as keyof DurableRecoveryIdentity] === right[key as keyof DurableRecoveryIdentity]);
/** Keep the current and previous run through an interrupted authority handoff. */
export class RunnerLaunchPreparationStore {
readonly #path: string;
constructor(directory: string) { this.#path = resolve(directory, "launch-preparation.json"); }
#read(): LaunchPreparation[] {
const contents = readPrivateFile(this.#path);
if (contents === null) return [];
const value = JSON.parse(contents);
if (value?.schema !== schema || !Array.isArray(value.entries) || value.entries.length > 2) {
throw new Error("runner_launch_recovery_requirements_unavailable");
}
for (const entry of value.entries) {
if (entry?.requirements?.schema !== "paperclip.runner.launch-requirements.v1"
|| !entry.requirements.identity) throw new Error("runner_launch_recovery_requirements_unavailable");
validatePreparedRunnerLaunch(entry.requirements, entry.prepared);
}
return value.entries;
}
load(identity: DurableRecoveryIdentity): LaunchPreparation | undefined {
return this.#read().find(entry => sameIdentity(entry.requirements.identity, identity));
}
save(requirements: RunnerProviderLaunchRequirements, prepared: PreparedRunnerProviderLaunch, activeIdentity: DurableRecoveryIdentity): void {
validatePreparedRunnerLaunch(requirements, prepared);
const entries = this.#read();
const existing = entries.find(entry => sameIdentity(entry.requirements.identity, requirements.identity));
if (existing && (runnerLaunchRequirementsDigest(existing.requirements) !== runnerLaunchRequirementsDigest(requirements)
|| existing.prepared.configurationId !== prepared.configurationId
|| durableRecoveryInternals.canonicalJson(existing.prepared) !== durableRecoveryInternals.canonicalJson(prepared))) {
throw new Error("runner_prepared_launch_recovery_changed");
}
const active = entries.find(entry => sameIdentity(entry.requirements.identity, activeIdentity));
atomicPrivateWrite(this.#path, JSON.stringify({ schema,
entries: existing ? entries : [{ requirements, prepared }, ...(active ? [active] : [])],
}));
}
}
@@ -0,0 +1,98 @@
import { createHash } from "node:crypto";
import { canonicalNativeRuntimeContextDigest, composeNativeSystemInstructions, NATIVE_RUNTIME_ASSET_SCHEMA } from "../contracts/runtime-context.js";
import { describe, expect, it } from "vitest";
import { bindRunnerLaunchResources, createRunnerProviderLaunch, runnerLaunchRequirementsDigest, runnerProviderLaunchRequirements,
validatePreparedRunnerLaunch, type RunnerProviderLaunchRequirements } from "./runner-provider-launch.js";
const identity = { runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "run",
normalizedSessionId: "session", turnId: "turn", itemId: "item" };
const requirements = () => runnerProviderLaunchRequirements({ provider: "codex" }, identity,
{ model: "selected", permissions: "paperclip-runner-workspace-read-only", approvalPolicy: "on-request" }, null, "Core instructions");
const prepare = (request = requirements()) => ({ configurationId: "configuration-1",
requirementsDigest: runnerLaunchRequirementsDigest(request),
provider: createRunnerProviderLaunch(request, { options: { codexArgs: ["app-server"] },
root: "/guest/state", cwd: "/guest/workspace", codexHome: "/guest/home/.codex",
providerNodeCommand: "/guest/node", codexCommand: "/guest/codex",
opencodeProxyPath: "/guest/opencode.cjs", acpxSidecarPath: "/guest/acpx.cjs" }),
});
describe("environment-owned launch contract", () => {
it("does not send process callbacks, host secrets, or executable choices in the requirements", () => {
const request = runnerProviderLaunchRequirements({ provider: "codex", environment: { OPENAI_API_KEY: "fixture-credential" },
codexCommand: "/controller/executable", runnerProcessLauncher: () => { throw new Error("unused"); } },
identity, { model: "selected" }, null, "Core instructions");
expect(JSON.stringify(request)).not.toContain("fixture-credential");
expect(JSON.stringify(request)).not.toContain("/controller/executable");
expect(request.settings).not.toHaveProperty("runnerProcessLauncher");
});
it("accepts a descriptor with execution-host paths and the exact Core policy", () => {
const request = requirements();
expect(() => validatePreparedRunnerLaunch(request, prepare(request))).not.toThrow();
});
it("hashes equivalent JSON independently of property order", () => {
const request = requirements();
const reordered = Object.fromEntries(Object.entries(request).reverse()) as unknown as RunnerProviderLaunchRequirements;
expect(runnerLaunchRequirementsDigest(reordered)).toBe(runnerLaunchRequirementsDigest(request));
expect(runnerLaunchRequirementsDigest({ ...request, identity: { ...identity, runId: "other" } }))
.not.toBe(runnerLaunchRequirementsDigest(request));
});
it.each([
["request binding", { requirementsDigest: `sha256:${"a".repeat(64)}` }],
["provider", { provider: { ...prepare().provider, kind: "acpx" } }],
["unassigned runtime context", { provider: { ...prepare().provider, runtimeContext: {} } }],
["model", { provider: { ...prepare().provider, model: "other" } }],
["read-only policy", { provider: { ...prepare().provider, readOnly: false } }],
["approval policy", { provider: { ...prepare().provider, approvalPolicy: "never" } }],
])("rejects a changed %s", (_name, update) => {
expect(() => validatePreparedRunnerLaunch(requirements(), { ...prepare(), ...update }))
.toThrow("runner_prepared_launch_requirements_mismatch");
});
it("binds only registered resource framing and preserves literal instruction content", () => {
const zero = "0".repeat(64);
const value = {
prompt: { revision: "fixture", text: "Core policy", digest: createHash("sha256").update("Core policy").digest("hex") },
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA,
rootPath: "/controller/instructions", digest: zero, manifestDigest: zero, fileCount: 1, totalBytes: 1 } },
skills: [{ key: "assigned", runtimeName: "assigned", versionId: "v1", bundle: {
schema: NATIVE_RUNTIME_ASSET_SCHEMA, rootPath: "/controller/skill", digest: zero, manifestDigest: zero, fileCount: 1, totalBytes: 1 } }],
mcp: { assignmentSetId: "assigned", digest: zero, bindingId: "current-binding" },
};
const context = { ...value, aggregateDigest: canonicalNativeRuntimeContextDigest(value) };
const request = { ...requirements(), runtimeContext: context,
instructions: composeNativeSystemInstructions(context, "A historical example: /controller/instructions") };
const target = structuredClone(context);
target.instructions.bundle.rootPath = "/guest/.agents";
target.skills[0]!.bundle.rootPath = "/guest/.agents/skills/assigned";
const bound = bindRunnerLaunchResources(request, target);
expect(bound.instructions).toContain("Read-only instruction sibling root: /guest/.agents");
expect(bound.instructions).toContain("A historical example: /controller/instructions");
expect(bound.runtimeContext?.skills[0]?.bundle.rootPath).toBe("/guest/.agents/skills/assigned");
const prepared = { ...prepare(bound), requirementsDigest: runnerLaunchRequirementsDigest(request) };
expect(() => validatePreparedRunnerLaunch(request, prepared)).not.toThrow();
expect(() => validatePreparedRunnerLaunch(request, { ...prepared,
provider: { ...prepared.provider, instructions: "Core policy" } })).toThrow();
const changed = structuredClone(target); changed.mcp.bindingId = "other-binding";
expect(() => bindRunnerLaunchResources(request, changed)).toThrow("assignment_changed");
expect(() => validatePreparedRunnerLaunch(request, { ...prepared,
provider: { ...prepared.provider, runtimeContext: changed } })).toThrow();
const unassigned = structuredClone(target); unassigned.skills[0]!.key = "unassigned";
unassigned.aggregateDigest = canonicalNativeRuntimeContextDigest(unassigned);
expect(() => bindRunnerLaunchResources(request, unassigned)).toThrow("assignment_changed");
});
it("checks ACPX identity and read-only policy as part of the same contract", () => {
const request = { ...requirements(), provider: "acpx" as const,
settings: { acpxAgent: "claude" as const, acpxPermissionMode: "deny-all" as const } };
const prepared = prepare(request);
expect(() => validatePreparedRunnerLaunch(request, prepared)).not.toThrow();
for (const update of [{ runId: "other" }, { normalizedSessionId: "other" }, { permissionMode: "approve-all" },
{ providerPolicy: { readOnly: false } }, { agent: "pi" }]) {
expect(() => validatePreparedRunnerLaunch(request, { ...prepared, provider: { ...prepared.provider, ...update } }))
.toThrow("runner_prepared_launch_requirements_mismatch");
}
});
});
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { createRunnerProviderLaunch, type RunnerProviderLaunchEnvironment, type RunnerProviderLaunchRequirements } from "./runner-provider-launch.js";
const requirements: RunnerProviderLaunchRequirements = {
schema: "paperclip.runner.launch-requirements.v1",
identity: { runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "run",
normalizedSessionId: "session", turnId: "turn", itemId: "item" },
provider: "codex", model: "selected-model", approvalPolicy: "on-request",
@@ -1,3 +1,5 @@
import { createHash } from "node:crypto";
import { parseNativeRuntimeContext, retargetNativeSystemInstructions } from "../contracts/runtime-context.js";
import { resolve } from "node:path";
import { codexExecutableReadOnlyRoots } from "../drivers/codex/codex-security-config.js";
import { createIsolatedCodexAppServerArgs } from "../drivers/codex/codex-app-server-driver.js";
@@ -10,6 +12,7 @@ import type { CapabilityRunnerdCodexTransportOptions } from "./runnerd-codex-tra
/** Core's provider inputs. Executable paths and filesystem layout are supplied separately. */
export interface RunnerProviderLaunchRequirements {
schema: "paperclip.runner.launch-requirements.v1";
identity: DurableRecoveryIdentity;
provider: NonNullable<CapabilityRunnerdCodexTransportOptions["provider"]>;
model: string | null;
@@ -19,11 +22,120 @@ export interface RunnerProviderLaunchRequirements {
includeCollaborationModeInstructions: boolean;
settings: Pick<CapabilityRunnerdCodexTransportOptions,
"acpxAgent" | "acpxMode" | "acpxPermissionMode" | "acpxPermissionModePinned" |
"piThinkingLevel" | "claudeEffort" | "managedProfile" | "agentCoreProfile">;
"piThinkingLevel" | "claudeEffort" | "managedProfile" | "agentCoreProfile" | "opencodePermissionMode">;
instructions: string;
runtimeContext: NativeRuntimeContextSnapshot | null;
}
/** A prepared descriptor belongs to one exact request and contains no launch secrets. */
export interface PreparedRunnerProviderLaunch {
configurationId: string;
requirementsDigest: string;
provider: Record<string, unknown>;
}
export type RunnerLaunchPreparer = (
requirements: RunnerProviderLaunchRequirements,
recovery?: PreparedRunnerProviderLaunch,
) => Promise<PreparedRunnerProviderLaunch>;
export function runnerProviderLaunchRequirements(
options: CapabilityRunnerdCodexTransportOptions,
identity: DurableRecoveryIdentity,
params: Record<string, unknown>,
runtimeContext: NativeRuntimeContextSnapshot | null,
instructions: string,
): RunnerProviderLaunchRequirements {
const requirements: RunnerProviderLaunchRequirements = {
schema: "paperclip.runner.launch-requirements.v1",
identity, provider: options.provider ?? "codex",
model: typeof params.model === "string" ? params.model : null,
instructions, runtimeContext,
settings: {
acpxAgent: options.acpxAgent, acpxMode: options.acpxMode,
acpxPermissionMode: options.acpxPermissionMode,
acpxPermissionModePinned: options.acpxPermissionModePinned,
piThinkingLevel: options.piThinkingLevel, claudeEffort: options.claudeEffort,
managedProfile: options.managedProfile, agentCoreProfile: options.agentCoreProfile,
opencodePermissionMode: options.opencodePermissionMode,
},
approvalPolicy: params.approvalPolicy === "on-request" || params.approvalPolicy === "untrusted"
? params.approvalPolicy : "never",
readOnly: params.permissions === "paperclip-runner-workspace-read-only",
preparedConversation: params.conversationMode === "prepared",
includeCollaborationModeInstructions: (params.config as Record<string, unknown> | undefined)?.include_collaboration_mode_instructions !== false,
};
// PRP hashes strict JSON; absent settings must not remain undefined properties.
return JSON.parse(JSON.stringify(requirements)) as RunnerProviderLaunchRequirements;
}
/** Resolve assigned resource locations without changing their policy or content identities. */
export function bindRunnerLaunchResources(
requirements: RunnerProviderLaunchRequirements,
context: NativeRuntimeContextSnapshot,
): RunnerProviderLaunchRequirements {
const source = requirements.runtimeContext;
const target = parseNativeRuntimeContext(context);
if (!source || target.aggregateDigest !== source.aggregateDigest
|| target.mcp.bindingId !== source.mcp.bindingId) {
throw new Error("runner_launch_resource_assignment_changed");
}
return { ...requirements,
instructions: retargetNativeSystemInstructions(requirements.instructions, source, target),
runtimeContext: target,
};
}
export function runnerLaunchRequirementsDigest(requirements: RunnerProviderLaunchRequirements): string {
const canonical = JSON.stringify(requirements, (_key, value: unknown) =>
value && typeof value === "object" && !Array.isArray(value)
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : value);
return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
}
/** The environment may resolve paths, but it cannot change Core's selected policy. */
export function validatePreparedRunnerLaunch(
requirements: RunnerProviderLaunchRequirements,
prepared: PreparedRunnerProviderLaunch,
): void {
const reject = () => { throw new Error("runner_prepared_launch_requirements_mismatch"); };
if (!prepared || typeof prepared.configurationId !== "string" || !prepared.configurationId.trim()
|| prepared.configurationId.length > 1024
|| prepared.requirementsDigest !== runnerLaunchRequirementsDigest(requirements)
|| !prepared.provider || typeof prepared.provider !== "object") reject();
const descriptor = prepared.provider;
if (!requirements.runtimeContext && descriptor.runtimeContext != null) reject();
if (descriptor.kind !== requirements.provider || descriptor.model !== requirements.model) reject();
if (requirements.provider === "codex" && (descriptor.approvalPolicy !== requirements.approvalPolicy
|| Boolean(descriptor.readOnly) !== requirements.readOnly)) reject();
if (requirements.provider === "acpx") {
const policy = descriptor.providerPolicy as Record<string, unknown> | undefined;
if (descriptor.agent !== (requirements.settings.acpxAgent ?? "codex")
|| descriptor.permissionMode !== resolveRunnerdAcpxPermissionMode(requirements.settings.acpxPermissionMode)
|| policy?.readOnly !== requirements.readOnly
|| descriptor.runId !== requirements.identity.runId
|| descriptor.normalizedSessionId !== requirements.identity.normalizedSessionId) reject();
}
const expected = createRunnerProviderLaunch(requirements, {
options: { codexArgs: [] }, root: "/", cwd: "/", codexHome: "/",
providerNodeCommand: "", opencodeProxyPath: "", acpxSidecarPath: "", codexCommand: "",
});
const hostFields = new Set(["command", "args", "sidecarCommand", "sidecarArgs", "runtimeDirectory", "cwd",
"preconfiguredHome", "externallySandboxed", "runtimeContext", "instructions"]);
for (const [key, value] of Object.entries(expected)) {
if (!hostFields.has(key) && JSON.stringify(value) !== JSON.stringify(descriptor[key])) reject();
}
let requiredInstructions = requirements.instructions;
if (requirements.runtimeContext) {
const context = parseNativeRuntimeContext(descriptor.runtimeContext);
if (context.aggregateDigest !== requirements.runtimeContext.aggregateDigest
|| context.mcp.bindingId !== requirements.runtimeContext.mcp.bindingId) reject();
requiredInstructions = retargetNativeSystemInstructions(requiredInstructions, requirements.runtimeContext, context);
}
if (requiredInstructions.trim()
&& (typeof descriptor.instructions !== "string" || !descriptor.instructions.includes(requiredInstructions.trim()))) reject();
}
/** Locations and provider settings resolved by the built-in execution environment. */
export interface RunnerProviderLaunchEnvironment {
options: Pick<CapabilityRunnerdCodexTransportOptions,
@@ -1,3 +1,4 @@
import { bindRunnerLaunchResources, createRunnerProviderLaunch, runnerLaunchRequirementsDigest, type PreparedRunnerProviderLaunch, type RunnerProviderLaunchRequirements } from "./runner-provider-launch.js";
import { coldAdmissionTimeoutMs, waitForRunnerCommand } from "./runnerd-codex-transport.js";
import {
chmod,
@@ -2458,6 +2459,237 @@ it.each(["installed", "explicit"] as const)("records the selected %s Codex comma
}
});
it.each(["reuse", "large-instructions", "rotated-recovery", "removed-context", "omitted-context", "close-attachment", "timeout-attachment"] as const)("runs an environment-prepared descriptor without Core filesystem preparation (%s)", async mode => {
const root = await mkdtemp(join(tmpdir(), "runner-environment-launch-"));
const binary = defaultCapabilityRunnerdBinary();
const artifact = { version: "0.3.0", digest: `sha256:${createHash("sha256").update(await readFile(binary)).digest("hex")}` };
const context = assignedRuntimeContext("/controller-only/skill", "/controller-only/instructions");
const coreInstructions = composeNativeSystemInstructions(context, mode === "large-instructions" ? "x".repeat(200_000) : "Agent instructions");
const resolved = structuredClone(context);
resolved.instructions.bundle.rootPath = "/guest/.agents";
resolved.skills[0]!.bundle.rootPath = "/guest/.agents/skills/assigned";
const materialize = vi.spyOn(runtimeMaterializer, "prepareIsolatedCodexHome").mockRejectedValue(new Error("unexpected Core materialization"));
const discover = vi.spyOn(codexCommandRuntime, "resolveCodexCommand").mockImplementation(() => { throw new Error("unexpected Core discovery"); });
let configuration: Record<string, unknown> | undefined;
let recovered = false;
let requestedRun = "";
let requestedInstructions = "";
let requestedContext: NativeRuntimeContextSnapshot | null | undefined;
const registered = new WeakSet<object>();
let enterPreparation!: () => void;
let finishPreparation!: () => void;
const preparationStarted = new Promise<void>(resolve => { enterPreparation = resolve; });
const preparationGate = new Promise<void>(resolve => { finishPreparation = resolve; });
const checkpoint = vi.fn();
const releaseRoute = vi.fn();
let currentRun = () => "";
const create = (closeGraceMs = 5_000, runId = "run-prepared") => createCapabilityRunnerdCodexTransport({ provider: "codex", runnerArtifact: artifact,
closeGraceMs,
stateDirectory: root, runtimeContext: runId === "run-prepared" ? context
: mode === "removed-context" ? null : mode === "omitted-context" ? undefined : context,
...(mode === "rotated-recovery" || mode === "removed-context" || mode === "omitted-context" ? {
readRunnerState: async () => JSON.parse(await readFile(join(root, "runner/runner-state.json"), "utf8")),
prepareExternalRunnerState: async () => undefined,
archiveExternalRunnerState: async () => {
const path = join(root, "runner/runner-state.json");
const prior = JSON.parse(await readFile(path, "utf8"));
await rename(path, join(root, "prior-runner-state.json"));
return prior;
},
} : {}),
prpIdentity: { runnerInstanceId: "runner-prepared", environmentLeaseId: "lease-prepared", runId,
normalizedSessionId: "session-prepared", turnId: "turn-prepared", itemId: "item-prepared" },
prepareLaunch: async (requirements, recovery) => {
requestedRun = requirements.identity.runId;
requestedInstructions = requirements.instructions;
requestedContext = requirements.runtimeContext;
if (requirements.identity.runId === "run-prepared-next" && (mode === "close-attachment" || mode === "timeout-attachment")) {
enterPreparation();
await preparationGate;
}
if (recovery) { recovered = true; return recovery; }
return { configurationId: `owned-${requirements.identity.runId}`,
requirementsDigest: runnerLaunchRequirementsDigest(requirements),
provider: createRunnerProviderLaunch(requirements.runtimeContext === null ? requirements : bindRunnerLaunchResources(requirements, resolved), {
options: { codexArgs: fakeCodexArgs(root) }, root, cwd: root, codexHome: "/guest/.codex",
codexCommand: fakeCodex, providerNodeCommand: process.execPath,
opencodeProxyPath: "", acpxSidecarPath: "",
}),
}; },
controlPlaneRegistration: async authority => {
configuration = authority.store.state.runAttachTemplate;
currentRun = () => authority.store.state.identity.runId;
if (!registered.has(authority)) {
await authority.start();
registered.add(authority);
}
return { release: releaseRoute, checkpoint };
},
runnerProcessLauncher: spec => {
const child = spawn(binary, [...spec.args], { env: spec.environment, stdio: "ignore" });
return { child, completion: new Promise((resolveExit, reject) => {
child.once("error", reject);
child.once("exit", (code, signal) => resolveExit({ code, signal, stdout: "", stderr: "" }));
}) };
},
});
const { transport } = create();
try {
const opened = await transport.request("thread/start", { cwd: "/controller-only/workspace", model: "selected-model",
baseInstructions: coreInstructions, dynamicTools: codexSemanticToolSpecs() });
expect(opened.thread).toHaveProperty("id");
expect(configuration).toMatchObject({ provider: { command: fakeCodex, cwd: root,
runtimeContext: { instructions: { bundle: { rootPath: "/guest/.agents" } } } } });
expect(configuration).not.toHaveProperty("preparedLaunch");
expect(configuration).not.toHaveProperty("launchRequirements");
expect(JSON.parse(await readFile(join(root, "launch-preparation.json"), "utf8")))
.toMatchObject({ entries: [{ prepared: { configurationId: "owned-run-prepared" },
requirements: { instructions: coreInstructions } }] });
expect(JSON.stringify(configuration?.provider)).not.toContain("/controller-only/");
expect(materialize).not.toHaveBeenCalled();
expect(discover).not.toHaveBeenCalled();
await expect(stat(join(root, "codex-home"))).rejects.toHaveProperty("code", "ENOENT");
await expect(stat(join(root, "runtime-context.json"))).rejects.toHaveProperty("code", "ENOENT");
await transport.close();
if (mode === "removed-context") {
const rotated = create(5_000, "run-prepared-removed");
try {
await expect(rotated.transport.request("thread/read", { threadId: (opened.thread as { id: string }).id }))
.rejects.toThrow("cannot change the durable Codex provider profile");
expect(requestedContext).toBeNull();
expect(requestedInstructions).toContain("Agent instructions");
expect(requestedInstructions).not.toContain("Read-only instruction sibling root:");
expect(requestedInstructions).not.toContain("/controller-only/");
expect(configuration).toHaveProperty("provider.runtimeContext", null);
const saved = JSON.parse(await readFile(join(root, "launch-preparation.json"), "utf8"));
expect(saved.entries[0]).toMatchObject({ requirements: { runtimeContext: null },
prepared: { provider: { runtimeContext: null } } });
} finally { await rotated.transport.close(); }
return;
}
if (mode === "rotated-recovery" || mode === "omitted-context") {
for (let attempt = 0; attempt < 2; attempt++) {
const rotated = create(5_000, "run-prepared-rotated");
try {
await rotated.transport.request("thread/read", { threadId: (opened.thread as { id: string }).id });
expect(requestedRun).toBe("run-prepared-rotated");
expect(requestedContext).toEqual(context);
expect(requestedInstructions).toBe(coreInstructions);
expect(recovered).toBe(attempt > 0);
} finally { await rotated.transport.close(); }
}
return;
}
const resumed = create(mode === "timeout-attachment" ? 500 : 5_000);
try {
await resumed.transport.request("thread/read", { threadId: (opened.thread as { id: string }).id });
expect(recovered).toBe(true);
checkpoint.mockClear();
releaseRoute.mockClear();
const attaching = resumed.transport.attachRun!({
runId: "run-prepared-next", turnId: "turn-prepared-next", itemId: "item-prepared-next",
});
void attaching.catch(() => undefined);
if (mode === "close-attachment" || mode === "timeout-attachment") {
await preparationStarted;
await expect(resumed.transport.attachRun!({
runId: "overlapping-run", turnId: "overlapping-turn", itemId: "overlapping-item",
})).rejects.toThrow("native_runner_run_attachment_in_progress");
let closed = false;
const closing = resumed.transport.close().finally(() => { closed = true; });
void closing.catch(() => undefined);
if (mode === "timeout-attachment") {
await expect(closing).rejects.toThrow();
expect(checkpoint).toHaveBeenCalledExactlyOnceWith("unsettled");
expect((await stat(root)).isDirectory()).toBe(true);
} else {
await new Promise(resolve => setTimeout(resolve, 300));
expect(closed).toBe(false);
expect(releaseRoute).not.toHaveBeenCalled();
expect(checkpoint).not.toHaveBeenCalled();
}
finishPreparation();
await expect(attaching).rejects.toThrow(/closed/);
if (mode === "close-attachment") {
await closing;
expect(checkpoint).toHaveBeenCalledExactlyOnceWith("settled");
}
expect(currentRun()).toBe("run-prepared");
return;
}
await attaching;
expect(requestedRun).toBe("run-prepared-next");
expect(requestedInstructions).toBe(coreInstructions);
await expect(resumed.transport.attachRun!({
runId: "run-prepared-changed", turnId: "turn-prepared-changed", itemId: "item-prepared-changed",
currentRunGrant: { runtimeContext: context, instructions: composeNativeSystemInstructions(context, "Next run instructions") },
})).rejects.toThrow("cannot change the durable Codex provider profile");
expect(requestedRun).toBe("run-prepared-changed");
expect(requestedInstructions).toContain("Next run instructions");
} finally {
finishPreparation();
if (mode === "timeout-attachment") await resumed.transport.close().catch(() => undefined);
else await resumed.transport.close();
}
} finally {
finishPreparation();
await transport.close();
materialize.mockRestore(); discover.mockRestore();
await rm(root, { recursive: true, force: true });
}
}, 30_000);
it("rejects an environment-prepared policy change before registration or process launch", async () => {
const root = await mkdtemp(join(tmpdir(), "runner-environment-policy-"));
const registration = vi.fn();
const launch = vi.fn();
const { transport } = createCapabilityRunnerdCodexTransport({ provider: "codex", stateDirectory: root,
runnerArtifact: { version: "fixture", digest: `sha256:${"a".repeat(64)}` },
runnerProcessLauncher: launch, controlPlaneRegistration: registration,
prepareLaunch: async requirements => ({ configurationId: "wrong-policy",
requirementsDigest: runnerLaunchRequirementsDigest(requirements),
provider: { kind: "codex", model: requirements.model, approvalPolicy: "never", readOnly: false },
}),
});
try {
await expect(transport.request("thread/start", { model: "selected", approvalPolicy: "on-request", dynamicTools: [] }))
.rejects.toThrow("runner_prepared_launch_requirements_mismatch");
expect(registration).not.toHaveBeenCalled(); expect(launch).not.toHaveBeenCalled();
} finally { await transport.close(); await rm(root, { recursive: true, force: true }); }
});
it("joins delayed environment preparation before close and prevents a late launch", async () => {
const root = await mkdtemp(join(tmpdir(), "runner-environment-cancel-"));
let started!: () => void;
const preparationStarted = new Promise<void>(resolve => { started = resolve; });
let release!: () => void;
const released = new Promise<void>(resolve => { release = resolve; });
const registration = vi.fn(); const launch = vi.fn();
const { transport } = createCapabilityRunnerdCodexTransport({ provider: "codex", stateDirectory: root,
runnerArtifact: { version: "fixture", digest: `sha256:${"a".repeat(64)}` },
runnerProcessLauncher: launch, controlPlaneRegistration: registration,
prepareLaunch: async requirements => {
started(); await released;
return { configurationId: "cancelled-preparation", requirementsDigest: runnerLaunchRequirementsDigest(requirements),
provider: createRunnerProviderLaunch(requirements, { options: { codexArgs: [] }, root, cwd: root,
codexHome: root, providerNodeCommand: "", codexCommand: "/unused/codex", opencodeProxyPath: "", acpxSidecarPath: "" }) };
},
});
const opening = transport.request("thread/start", { model: "selected", dynamicTools: [] });
void opening.catch(() => undefined);
let closed = false;
try {
await preparationStarted;
const closing = transport.close().then(() => { closed = true; });
await new Promise<void>(resolve => setImmediate(resolve));
expect(closed).toBe(false);
release();
await expect(opening).rejects.toThrow(/closed/);
await closing;
expect(registration).not.toHaveBeenCalled(); expect(launch).not.toHaveBeenCalled();
} finally { release(); await opening.catch(() => undefined); await transport.close(); await rm(root, { recursive: true, force: true }); }
});
it("does not invoke deferred startup preparation after immediate close", async () => {
const prepare = vi.spyOn(runtimeMaterializer, "prepareIsolatedCodexHome").mockResolvedValue(undefined);
const registration = vi.fn(async () => ({ connectUrl: "ws://127.0.0.1:1", release: () => undefined }));
@@ -6087,6 +6319,15 @@ it.each([
},
]
: []),
...["before-result", "after-result", "after-activation", "after-confirmation"].map(lossPoint => ({
lossPoint, routed: true, recoveryFault: "none", executionPrepared: true,
})),
{
lossPoint: "after-result",
routed: true,
recoveryFault: "none",
externalArtifact: true,
},
{
lossPoint: "after-confirmation",
routed: true,
@@ -6106,9 +6347,12 @@ it.each([
ordinaryFollowup,
})),
])(
"recovers a warm attachment with a fresh controller and runner ($lossPoint, routed=$routed, fault=$recoveryFault, followup=$ordinaryFollowup)",
"recovers a warm attachment with a fresh controller and runner ($lossPoint, routed=$routed, fault=$recoveryFault, followup=$ordinaryFollowup, external=$externalArtifact, prepared=$executionPrepared)",
async (testCase) => {
const { lossPoint, routed, recoveryFault } = testCase;
const externalArtifact = "externalArtifact" in testCase && testCase.externalArtifact;
const executionPrepared = "executionPrepared" in testCase && testCase.executionPrepared;
const configurations = new Map<string, { requirements: RunnerProviderLaunchRequirements; prepared: PreparedRunnerProviderLaunch }>();
const ordinaryFollowup =
"ordinaryFollowup" in testCase && testCase.ordinaryFollowup;
const snapshotFault =
@@ -6231,7 +6475,10 @@ it.each([
const launchSpy = vi
.spyOn(durableControlPlane, "spawnRunner")
.mockImplementation((options) => {
const handle = launch(options);
const handle = launch({ ...options,
...(executionPrepared ? { processLauncher: undefined } : {}),
...(externalArtifact && recovering ? { runnerBinaryPath: defaultCapabilityRunnerdBinary() } : {}),
});
handles.push(handle);
return handle;
});
@@ -6348,6 +6595,30 @@ it.each([
warmTransitionRegistrationMode: "routed_connect" as const,
}
: {}),
...(executionPrepared ? {
runnerArtifact: readRunnerdArtifactBinding(defaultCapabilityRunnerdBinary()),
readRunnerState: async () => JSON.parse(await readFile(join(stateDirectory, "runner/runner-state.json"), "utf8")),
// The fixture's spawn spy supplies the real local process owner.
runnerProcessLauncher: () => { throw new Error("fixture process owner was bypassed"); },
prepareLaunch: async (requirements: RunnerProviderLaunchRequirements, recovery?: PreparedRunnerProviderLaunch) => {
const existing = configurations.get(requirements.identity.runId);
if (existing) {
expect(requirements).toEqual(existing.requirements);
if (recovering) expect(recovery).toEqual(existing.prepared);
return existing.prepared;
}
const prepared = { configurationId: `prepared-${requirements.identity.runId}`,
requirementsDigest: runnerLaunchRequirementsDigest(requirements),
provider: createRunnerProviderLaunch(requirements, {
options: { codexArgs: fakeCodexArgs(stateDirectory, "--call-log", callsPath, "--record-process-start") },
root: stateDirectory, cwd: tmpdir(), codexHome: join(stateDirectory, "codex-home"),
codexCommand: fakeCodex, providerNodeCommand: process.execPath, opencodeProxyPath: "", acpxSidecarPath: "",
}),
};
configurations.set(requirements.identity.runId, { requirements: structuredClone(requirements), prepared });
return prepared;
},
} : {}),
};
const first = createCapabilityRunnerdCodexTransport(options);
let resumed:
@@ -6681,6 +6952,10 @@ it.each([
);
resumed = createCapabilityRunnerdCodexTransport({
...options,
...(externalArtifact ? {
runnerBinary: "/controller-missing-runner",
runnerArtifact: readRunnerdArtifactBinding(options.runnerBinary),
} : {}),
...(ordinaryFollowup
? {
authorizeWarmTransitionRecovery: async () => {
@@ -1,4 +1,6 @@
import { createRunnerProviderLaunch, createRunnerdCodexAppServerArgs, trustedRuntimeReadOnlyRoots } from "./runner-provider-launch.js";
import { RunnerLaunchPreparationStore } from "./runner-launch-preparation-store.js";
import type { PreparedRunnerProviderLaunch, RunnerProviderLaunchRequirements } from "./runner-provider-launch.js";
import { validatePreparedRunnerLaunch, runnerProviderLaunchRequirements, createRunnerProviderLaunch, createRunnerdCodexAppServerArgs, trustedRuntimeReadOnlyRoots } from "./runner-provider-launch.js";
export { createRunnerdCodexAppServerArgs, resolveRunnerdAcpxPermissionMode, trustedRuntimeReadOnlyRoots, withCodexCollaborationRuntimeInstructions } from "./runner-provider-launch.js";
import { applyProviderObservabilityEnvironment, redactObservabilityDiagnostic } from "../drivers/agent-observability.js";
import type { RunnerExecutionDriver } from "../control-plane/runner-execution-driver.js";
@@ -79,7 +81,7 @@ import {
createSanitizedAwsAgentCoreEnvironment,
createSanitizedClaudeManagedEnvironment,
} from "../drivers/claude-managed/environment.js";
import { composeNativeSystemInstructions, type NativeRunRuntimeGrant, type NativeRuntimeContextSnapshot } from "../contracts/runtime-context.js";
import { retargetNativeSystemInstructions as retargetComposedInstructions, composeNativeSystemInstructions, type NativeRunRuntimeGrant, type NativeRuntimeContextSnapshot } from "../contracts/runtime-context.js";
import type {
NativeAcpxPermissionMode,
NativeOpenCodePermissionMode,
@@ -466,27 +468,6 @@ function rotatedRunAttachPayload(
);
}
function retargetComposedInstructions(
instructions: string,
prior: NativeRuntimeContextSnapshot,
current: NativeRuntimeContextSnapshot | null,
): string {
const priorPrefix = prior.prompt.text;
const priorSuffix = composeNativeSystemInstructions(prior, "").slice(priorPrefix.length);
// Legacy callers can supply opaque system instructions. Only the composer's
// exact framing identifies a trusted asset block; never rewrite their text.
if (!instructions.startsWith(priorPrefix) || !instructions.endsWith(priorSuffix)) {
return instructions;
}
const custom = instructions.slice(priorPrefix.length, -priorSuffix.length);
if (custom && !custom.startsWith("\n\n")) return instructions;
// Keep custom entry bytes intact, including historical path examples or an
// identical paragraph quoted inside the entry. Only replace the final block.
return (current?.prompt.text ?? priorPrefix) + custom + (current
? composeNativeSystemInstructions(current, "").slice(current.prompt.text.length)
: "");
}
function retargetRunAttachPayload(
seedPayload: Record<string, unknown>,
desired: DurableRecoveryIdentity,
@@ -1295,6 +1276,8 @@ export interface CapabilityRunnerdCodexTransportOptions extends RunnerExecutionD
model: string;
};
runnerBinary?: string;
/** Identity reported by an execution environment that owns its Runner artifact. */
runnerArtifact?: { version: string; digest: string };
codexCommand?: string;
codexArgs?: string[];
disableCodexProjectInstructions?: boolean;
@@ -3575,6 +3558,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
#resolveClosedSignal!: () => void;
#closePromise: Promise<void> | null = null;
#startupOperation: Promise<unknown> | null = null;
#attachmentOperation: Promise<void> | null = null;
#startupDrainTimedOut = false;
#controllerDetachedForRestart = false;
#failure: Error | null = null;
@@ -3596,6 +3580,9 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
readonly #bridgedRuntimeInputs = new Map<string, { durableTurnId: string; permission?: boolean }>();
constructor(readonly options: CapabilityRunnerdCodexTransportOptions) {
if (options.prepareLaunch && (!options.runnerProcessLauncher || !options.controlPlaneRegistration || !options.runnerArtifact)) {
throw new Error("runner_launch_preparation_requires_process_owner_and_artifact");
}
resolveClaudeEffort(options.provider === "acpx" ? options.acpxAgent ?? "codex" : "", options.claudeEffort);
resolvePiThinkingLevel(options.provider === "acpx" ? options.acpxAgent ?? "codex" : "", options.piThinkingLevel);
if (options.acpxMode !== undefined && options.provider !== "acpx") {
@@ -4062,7 +4049,24 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
);
}
async attachRun(input: {
attachRun(input: {
runId: string;
turnId: string;
itemId: string;
currentRunGrant?: NativeRunRuntimeGrant;
}): Promise<void> {
if (this.#attachmentOperation !== null) {
return Promise.reject(new Error("native_runner_run_attachment_in_progress"));
}
const operation = Promise.resolve().then(() => {
this.#assertStartupOpen();
return this.#attachRun(input);
}).finally(() => { this.#attachmentOperation = null; });
this.#attachmentOperation = operation;
return operation;
}
async #attachRun(input: {
runId: string;
turnId: string;
itemId: string;
@@ -4076,6 +4080,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
throw new Error("native_runner_prp_run_rotation_unavailable");
}
await this.#awaitWarmRunAttachmentReady();
this.#assertStartupOpen();
const currentRunGrant = input.currentRunGrant
? projectCopilotRunGrant(input.currentRunGrant, this.options.runnerFilesystemRoot)
: undefined;
@@ -4096,6 +4101,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
let previousReleased = false;
let activationStarted = false;
try {
this.#assertStartupOpen();
const connection: RunnerProcessConnection =
registration?.connection ??
(registration?.connectUrl
@@ -4122,6 +4128,15 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
currentRunGrant?.runtimeContext,
currentRunGrant ? { text: currentRunGrant.instructions, context: currentRunGrant.runtimeContext } : undefined,
);
if (this.options.prepareLaunch) {
const priorProvider = record(runAttachTemplate.provider);
const priorRequirements = new RunnerLaunchPreparationStore(this.#root).load(prior)?.requirements;
await this.#prepareOwnedProvider(runAttachTemplate, desired,
currentRunGrant ? currentRunGrant.runtimeContext : priorRequirements?.runtimeContext
?? priorProvider.runtimeContext as NativeRuntimeContextSnapshot | null,
currentRunGrant?.instructions ?? priorRequirements?.instructions ?? String(priorProvider.instructions ?? ""));
}
this.#assertStartupOpen();
this.#runAttachTemplate = structuredClone(runAttachTemplate);
const payload = {
...runAttachTemplate,
@@ -4181,7 +4196,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
// The future route is ours from registration onward, including failures
// in template construction, capability admission, and result waiting.
// Keep the prior release owned by close until its handoff is confirmed.
this.#controlPlaneRelease = previousReleased ? null : previousRelease;
if (!this.#closed) this.#controlPlaneRelease = previousReleased ? null : previousRelease;
await Promise.resolve()
.then(() => registration?.release())
.catch(() => undefined);
@@ -4299,7 +4314,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
}
| "unreadable"
| null {
if (this.options.runnerFilesystemRoot !== undefined) return null;
if (this.options.runnerFilesystemRoot !== undefined || this.options.prepareLaunch) return null;
const provider = this.options.provider ?? "codex";
const filename =
provider === "acpx"
@@ -4487,18 +4502,19 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
this.#closed = true;
this.#resolveClosedSignal();
const { preparationDeadline, closeDeadline } = runnerCloseDeadlines(Date.now(), runnerCloseGraceMs(this.options));
let startupConfirmed = true;
if (this.#startupOperation !== null) {
let preparationConfirmed = true;
const attachmentPending = this.#attachmentOperation !== null;
if (this.#startupOperation !== null || attachmentPending) {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
this.#startupOperation.catch(() => undefined),
Promise.allSettled([this.#startupOperation, this.#attachmentOperation]),
new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => reject(new Error("Native startup drain timed out")), Math.max(0, preparationDeadline - Date.now()));
timer = setTimeout(() => reject(new Error("Native preparation drain timed out")), Math.max(0, preparationDeadline - Date.now()));
}),
]);
} catch {
startupConfirmed = false;
preparationConfirmed = false;
this.#startupDrainTimedOut = true;
} finally { clearTimeout(timer); }
}
@@ -4653,7 +4669,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
// an incomplete or identity-conflicting state remains fail-closed.
const finalProviderState = this.#providerDrainState();
const runnerSettled =
startupConfirmed && runnerSuspended &&
preparationConfirmed && runnerSuspended &&
providerDrained &&
this.#core?.semanticToolResultsSettled() === true &&
(finalProviderState === null ||
@@ -4705,8 +4721,8 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
}
throw new NativeSessionCloseUnrecoverableError(settlement);
}
if (!startupConfirmed) {
throw new Error("Native runner startup cleanup is unconfirmed; recovery state retained.");
if (!preparationConfirmed) {
throw new Error(`Native runner ${attachmentPending ? "attachment" : "startup"} cleanup is unconfirmed; recovery state retained.`);
}
if (this.#ownsRoot && !adoptedRunner) {
rmSync(this.#root, { recursive: true, force: true });
@@ -4714,59 +4730,56 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
this.#publish();
}
async #start(
params: Record<string, unknown>,
): Promise<Record<string, unknown>> {
if (this.#core !== null)
throw new Error("PRP provider thread is already started");
if (this.options.adoptExistingRunner) {
// A crash can precede the first driver checkpoint even though runnerd
// already opened the provider. Exact process adoption must reuse that
// authority instead of enqueueing another run.prepare/session.open pair.
await this.#resume();
return this.#openedThreadResponse(params);
async #prepareOwnedProvider(
template: Record<string, unknown>, identity: DurableRecoveryIdentity,
runtimeContext: NativeRuntimeContextSnapshot | null, instructions: string,
recovery?: PreparedRunnerProviderLaunch,
recordedRequirements?: RunnerProviderLaunchRequirements,
): Promise<PreparedRunnerProviderLaunch> {
const prior = record(template.provider);
const requirements = recordedRequirements ?? runnerProviderLaunchRequirements(this.options, identity, {
model: prior.model, approvalPolicy: prior.approvalPolicy,
permissions: prior.readOnly === true || record(prior.providerPolicy).readOnly === true || prior.collaborationMode === "plan"
? "paperclip-runner-workspace-read-only" : "paperclip-runner-workspace-only",
conversationMode: prior.conversationMode,
config: { include_collaboration_mode_instructions: prior.includeCollaborationModeInstructions !== false },
}, runtimeContext, instructions);
if (requirements.schema !== "paperclip.runner.launch-requirements.v1"
|| commandDigest(requirements.identity) !== commandDigest(identity)) {
throw new Error("runner_launch_recovery_requirements_unavailable");
}
const token = randomUUID().replaceAll("-", "");
const identity = this.options.prpIdentity ?? {
runnerInstanceId: `runner_lab_${token}`,
environmentLeaseId: `lease_lab_${token}`,
runId: `run_lab_${token}`,
normalizedSessionId: `session_lab_${token}`,
turnId: `turn_lab_${token}`,
itemId: `item_lab_${token}`,
};
const runnerBinaryPath =
this.options.runnerBinary ?? defaultCapabilityRunnerdBinary();
const runnerArtifact = approvedRunnerArtifact(runnerBinaryPath);
this.#durableTurnId = identity.turnId;
this.#eventIdentity = structuredClone(identity);
const dynamicTools = Array.isArray(params.dynamicTools)
? params.dynamicTools.map(record)
: [];
const core = new DurablePrpControlPlane({
stateDirectory: resolve(this.#root, "control-plane"),
identity,
expectedRunnerVersion: runnerArtifact.version,
expectedRunnerDigest: runnerArtifact.digest,
onProtocolIntegrityError: (error) => this.#failTransport(error),
onSemanticToolInput: (call) => this.#handleSemanticToolInput(call),
onCommittedEvent: async (event) => {
if (event.eventType === "auth.credentials.updated") {
await this.options.onCredentialsUpdated?.(event);
}
},
connectionLeaseTtlMs: 60 * 60 * 1_000,
});
this.#core = core;
// Externally launched runners own their state directory (for example in a
// Daytona sandbox). Do not create an empty controller-side placeholder:
// prior-run authority checks must be able to distinguish absent remote
// state from malformed direct state.
if (this.options.runnerStateDirectory === undefined) {
mkdirSync(resolve(this.#root, "runner"), {
recursive: true,
mode: 0o700,
});
this.#assertStartupOpen();
const prepared = await this.options.prepareLaunch!(requirements, recovery);
validatePreparedRunnerLaunch(requirements, prepared);
this.#assertStartupOpen();
template.provider = prepared.provider;
new RunnerLaunchPreparationStore(this.#root).save(requirements, prepared, this.#core!.store.state.identity);
return prepared;
}
async #prepareLaunch(
params: Record<string, unknown>, identity: DurableRecoveryIdentity,
dynamicTools: Record<string, unknown>[],
) {
if (this.options.prepareLaunch) {
const provider = this.options.provider ?? "codex";
this.#authorizedTools = authorizedToolSetForProvider(provider, dynamicTools);
const requirements = runnerProviderLaunchRequirements(this.options, identity, params,
this.options.runtimeContext ?? null,
String(params.developerInstructions ?? params.baseInstructions ?? "You are a Paperclip agent."));
const prepared = await this.options.prepareLaunch(requirements);
validatePreparedRunnerLaunch(requirements, prepared);
this.#assertStartupOpen();
new RunnerLaunchPreparationStore(this.#root).save(requirements, prepared, this.#core!.store.state.identity);
const completionContract = record(params.completionContract);
return {
runAttachTemplate: { observeCredentials: this.options.observeCredentials !== false,
authorizedTools: this.#authorizedTools,
...(completionContract.revision && Array.isArray(completionContract.criterionIds) ? { completionContract } : {}),
provider: prepared.provider },
runnerAcpxLaunchProfile: undefined, runnerOpenCodeLaunchProfile: undefined,
environment: {},
};
}
const provider = this.options.provider ?? "codex";
const sourceRuntimeContext = this.options.runtimeContext ?? null;
@@ -4908,20 +4921,90 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
authorizedTools: this.#authorizedTools,
...(completionContract.revision && Array.isArray(completionContract.criterionIds)
? { completionContract } : {}),
provider: createRunnerProviderLaunch({
identity, provider, model: typeof params.model === "string" ? params.model : null,
instructions: baseInstructions, runtimeContext, settings: this.options,
approvalPolicy: params.approvalPolicy === "on-request" || params.approvalPolicy === "untrusted"
? params.approvalPolicy : "never",
readOnly: params.permissions === "paperclip-runner-workspace-read-only",
preparedConversation: params.conversationMode === "prepared",
includeCollaborationModeInstructions: record(params.config).include_collaboration_mode_instructions !== false,
}, {
provider: createRunnerProviderLaunch(runnerProviderLaunchRequirements(
this.options, identity, params, runtimeContext, baseInstructions,
), {
options: this.options, root: this.#root, cwd: String(params.cwd ?? tmpdir()),
codexHome, providerNodeCommand, codexCommand: effectiveCodexCommand,
opencodeProxyPath, acpxSidecarPath,
}),
};
return { runAttachTemplate, runnerAcpxLaunchProfile, runnerOpenCodeLaunchProfile,
environment: withRunnerdProviderTrace(
createCapabilityRunnerdProviderEnvironment({
provider,
options: {
...this.options,
stateDirectory: this.#root,
acpxAgent: acpxAgent ?? undefined,
},
identity,
codexHome,
runtimeContextPath,
hasRuntimeContext: runtimeContext !== null,
acpxSidecarPath,
}),
this.options.environment,
),
};
}
async #start(
params: Record<string, unknown>,
): Promise<Record<string, unknown>> {
if (this.#core !== null)
throw new Error("PRP provider thread is already started");
if (this.options.adoptExistingRunner) {
// A crash can precede the first driver checkpoint even though runnerd
// already opened the provider. Exact process adoption must reuse that
// authority instead of enqueueing another run.prepare/session.open pair.
await this.#resume();
return this.#openedThreadResponse(params);
}
const token = randomUUID().replaceAll("-", "");
const identity = this.options.prpIdentity ?? {
runnerInstanceId: `runner_lab_${token}`,
environmentLeaseId: `lease_lab_${token}`,
runId: `run_lab_${token}`,
normalizedSessionId: `session_lab_${token}`,
turnId: `turn_lab_${token}`,
itemId: `item_lab_${token}`,
};
const runnerBinaryPath =
this.options.runnerBinary ?? (this.options.prepareLaunch ? "paperclip-runnerd" : defaultCapabilityRunnerdBinary());
const runnerArtifact = this.options.runnerArtifact ?? approvedRunnerArtifact(runnerBinaryPath);
this.#durableTurnId = identity.turnId;
this.#eventIdentity = structuredClone(identity);
const dynamicTools = Array.isArray(params.dynamicTools)
? params.dynamicTools.map(record)
: [];
const core = new DurablePrpControlPlane({
stateDirectory: resolve(this.#root, "control-plane"),
identity,
expectedRunnerVersion: runnerArtifact.version,
expectedRunnerDigest: runnerArtifact.digest,
onProtocolIntegrityError: (error) => this.#failTransport(error),
onSemanticToolInput: (call) => this.#handleSemanticToolInput(call),
onCommittedEvent: async (event) => {
if (event.eventType === "auth.credentials.updated") {
await this.options.onCredentialsUpdated?.(event);
}
},
connectionLeaseTtlMs: 60 * 60 * 1_000,
});
this.#core = core;
// Externally launched runners own their state directory (for example in a
// Daytona sandbox). Do not create an empty controller-side placeholder:
// prior-run authority checks must be able to distinguish absent remote
// state from malformed direct state.
if (this.options.runnerStateDirectory === undefined && !this.options.prepareLaunch) {
mkdirSync(resolve(this.#root, "runner"), {
recursive: true,
mode: 0o700,
});
}
const prepared = await this.#prepareLaunch(params, identity, dynamicTools);
const { runAttachTemplate } = prepared;
// Preserve the first generation's provider attachment seed independently
// of bounded command history. The in-memory copy serves a live warm
// continuation; the control-plane copy serves a controller/runner resume.
@@ -4963,24 +5046,9 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
runnerBinaryPath,
runnerVersion: runnerArtifact.version,
runnerDigest: runnerArtifact.digest,
acpxLaunchProfile: runnerAcpxLaunchProfile,
opencodeLaunchProfile: runnerOpenCodeLaunchProfile,
environment: withRunnerdProviderTrace(
createCapabilityRunnerdProviderEnvironment({
provider,
options: {
...this.options,
stateDirectory: this.#root,
acpxAgent: acpxAgent ?? undefined,
},
identity,
codexHome,
runtimeContextPath,
hasRuntimeContext: runtimeContext !== null,
acpxSidecarPath,
}),
this.options.environment,
),
acpxLaunchProfile: prepared.runnerAcpxLaunchProfile,
opencodeLaunchProfile: prepared.runnerOpenCodeLaunchProfile,
environment: prepared.environment,
diagnosticsDirectory: resolve(this.#root, "diagnostics"),
processLauncher: this.options.runnerProcessLauncher,
});
@@ -5046,7 +5114,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
const localStateOwner =
this.options.readRunnerState === undefined &&
this.options.runnerStateDirectory === undefined &&
this.options.runnerFilesystemRoot === undefined;
this.options.runnerFilesystemRoot === undefined && !this.options.prepareLaunch;
const localRunnerStatePath = resolve(
this.#root,
"runner",
@@ -5078,6 +5146,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
this.options.readRunnerState &&
controlPlaneState &&
(controlPlaneState.warmTransition !== undefined ||
controlPlaneState.completedWarmTransition !== undefined ||
controlPlaneState.schema ===
"paperclip.runner.durable.control-plane-state.warm-transition.v1" ||
(Array.isArray(controlPlaneState.commands) &&
@@ -5124,7 +5193,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
"native_runner_warm_transition_requires_exact_owned_endpoint",
);
}
const artifact = approvedRunnerArtifact(
const artifact = this.options.runnerArtifact ?? approvedRunnerArtifact(
this.options.runnerBinary ?? defaultCapabilityRunnerdBinary(),
);
const proof = inspectWarmRunTransition({
@@ -5264,12 +5333,12 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
);
}
const runnerBinaryPath =
this.options.runnerBinary ?? defaultCapabilityRunnerdBinary();
const runnerArtifact = approvedRunnerArtifact(runnerBinaryPath);
this.options.runnerBinary ?? (this.options.prepareLaunch ? "paperclip-runnerd" : defaultCapabilityRunnerdBinary());
const runnerArtifact = this.options.runnerArtifact ?? approvedRunnerArtifact(runnerBinaryPath);
this.#durableTurnId = identity.turnId;
this.#eventIdentity = structuredClone(identity);
const provider = this.options.provider ?? "codex";
if (provider === "codex" && (this.options.homeMode === "preconfigured" || record(record(controlPlaneState!.runAttachTemplate).provider).preconfiguredHome !== undefined)) {
if (!this.options.prepareLaunch && provider === "codex" && (this.options.homeMode === "preconfigured" || record(record(controlPlaneState!.runAttachTemplate).provider).preconfiguredHome !== undefined)) {
const seed = rotatedRunAttachPayload(controlPlaneState!, desiredIdentity, this.#authorizedTools, this.options.resumeCompletionContract);
const priorHome = record(seed.provider).preconfiguredHome;
const expectedHome = this.options.homeMode === "preconfigured"
@@ -5290,14 +5359,14 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
? resolve(this.options.runnerFilesystemRoot, "codex-home")
: localCodexHome;
const opencodeProxyPath =
this.options.opencodeProxyPath ??
(this.options.prepareLaunch ? "" : undefined) ?? this.options.opencodeProxyPath ??
(provider === "opencode" && !this.options.runnerFilesystemRoot
? resolveBuildOwnedCliArtifact("opencode-app-server-proxy.cjs")
: fileURLToPath(
new URL("../cli/opencode-app-server-proxy.cjs", import.meta.url),
));
const acpxSidecarPath =
this.options.acpxSidecarPath ??
(this.options.prepareLaunch ? "" : undefined) ?? this.options.acpxSidecarPath ??
(provider === "acpx" && !this.options.runnerFilesystemRoot
? resolveBuildOwnedCliArtifact("acpx-runtime-sidecar.cjs")
: fileURLToPath(
@@ -5306,11 +5375,11 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
const providerNodeCommand =
this.options.providerNodeCommand ?? process.execPath;
const opencodeExecutable =
provider === "opencode"
provider === "opencode" && !this.options.prepareLaunch
? resolveRunnerOpenCodeExecutable(this.options)
: null;
const runnerAcpxLaunchProfile =
provider === "acpx"
provider === "acpx" && !this.options.prepareLaunch
? acpxRunnerLaunchProfile(
this.options,
providerNodeCommand,
@@ -5318,7 +5387,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
)
: undefined;
const runnerOpenCodeLaunchProfile =
provider === "opencode"
provider === "opencode" && !this.options.prepareLaunch
? opencodeRunnerLaunchProfile(
this.options,
providerNodeCommand,
@@ -5360,6 +5429,39 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
connectionLeaseTtlMs: 60 * 60 * 1_000,
});
this.#core = core;
let environmentPrepared: PreparedRunnerProviderLaunch | undefined;
if (this.options.prepareLaunch) {
const preparations = new RunnerLaunchPreparationStore(this.#root);
const saved = preparations.load(desiredIdentity);
const previous = preparations.load(controlPlaneIdentity(controlPlaneState!));
if ((exactAuthority || warmRecovery !== null) && !saved) {
throw new Error("runner_launch_recovery_requirements_unavailable");
}
const { paperclipNextAuthority: _authority, ...savedAttachment } =
warmRecovery ? core.getCommand(warmRecovery.commandId)!.payload : {};
const template = warmRecovery ? structuredClone(savedAttachment)
: rotatedRunAttachPayload(controlPlaneState!, desiredIdentity, this.#authorizedTools,
this.options.resumeCompletionContract);
if (saved) {
const recordedProvider = warmRecovery
? core.getCommand(warmRecovery.commandId)?.payload.provider
: exactAuthority ? record(controlPlaneState!.runAttachTemplate).provider : undefined;
if ((exactAuthority || warmRecovery !== null)
&& (recordedProvider === undefined || commandDigest(recordedProvider) !== commandDigest(saved.prepared.provider))) {
throw new Error("runner_prepared_launch_recovery_changed");
}
template.provider = saved.prepared.provider;
}
const preparedContext = this.options.runtimeContext === undefined
? previous?.requirements.runtimeContext ?? null : this.options.runtimeContext;
const preparedInstructions = this.options.baseInstructions
?? (previous?.requirements.runtimeContext
? retargetComposedInstructions(previous.requirements.instructions, previous.requirements.runtimeContext, preparedContext)
: previous?.requirements.instructions ?? String(record(template.provider).instructions ?? ""));
environmentPrepared = await this.#prepareOwnedProvider(template, desiredIdentity,
preparedContext, preparedInstructions, saved?.prepared, saved?.requirements);
this.#runAttachTemplate = structuredClone(template);
}
if (rotatedAuthority) {
if (provider === "acpx" && this.options.acpxAgent === "copilot"
&& typeof this.options.baseInstructions !== "string") {
@@ -5375,7 +5477,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
? undefined
: { text: this.options.baseInstructions, context: sourceRuntimeContext },
);
if (provider === "codex") {
if (!this.options.prepareLaunch && provider === "codex") {
// These controller-owned, token-free paths belong to the new run.
// Keep the durable provider profile and thread identity unchanged.
const recordedCommand = record(runAttachTemplate.provider).command;
@@ -5403,7 +5505,11 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
],
});
}
if (environmentPrepared) {
runAttachTemplate.provider = environmentPrepared.provider;
}
this.#runAttachTemplate = structuredClone(runAttachTemplate);
core.persistRunAttachTemplate(runAttachTemplate);
core.queueCommand("run.attach", runAttachTemplate);
}
const committedEvents = core.store.state.committedEvents;
@@ -5556,16 +5662,16 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
this.#assertStartupOpen();
// Route and immutable endpoint admission precedes every launch-material
// write. A refused transition leaves its retained home/context untouched.
if (runtimeContext !== null) {
if (runtimeContext !== null && !this.options.prepareLaunch) {
writeFileSync(
localRuntimeContextPath,
`${JSON.stringify(runtimeContext)}\n`,
{ mode: 0o600 },
);
}
if (provider === "aws_agentcore")
if (provider === "aws_agentcore" && !this.options.prepareLaunch)
mkdirSync(codexHome, { recursive: true, mode: 0o700 });
if (provider === "codex" && this.options.homeMode !== "preconfigured") {
if (!this.options.prepareLaunch && provider === "codex" && this.options.homeMode !== "preconfigured") {
await releaseMaterializedNativeRuntimeSkills(
resolve(localCodexHome, "skills"),
);
@@ -5626,7 +5732,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
runnerDigest: runnerArtifact.digest,
acpxLaunchProfile: runnerAcpxLaunchProfile,
opencodeLaunchProfile: runnerOpenCodeLaunchProfile,
environment: withRunnerdProviderTrace(
environment: this.options.prepareLaunch ? {} : withRunnerdProviderTrace(
createCapabilityRunnerdProviderEnvironment({
provider,
options: {
@@ -5864,6 +5970,9 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
input.filter((item) => item.type === "skill"),
this.options.runtimeContext ?? null,
this.options.homeMode === "preconfigured" ? preconfiguredCodexHome(this.options.environment) : resolve(this.options.runnerFilesystemRoot ?? this.#root, "codex-home"),
this.options.prepareLaunch
? record((this.#runAttachTemplate ?? this.#core?.store.state.runAttachTemplate)?.provider).runtimeContext as NativeRuntimeContextSnapshot | undefined
: undefined,
);
if (skills.length && (this.options.provider ?? "codex") !== "codex") {
throw new Error("Explicit skill inputs are supported only by Codex");
@@ -7295,6 +7404,7 @@ export function resolveRunnerdCodexSkillInputs(
inputs: Record<string, unknown>[],
context: NativeRuntimeContextSnapshot | null,
codexHome: string,
preparedContext?: NativeRuntimeContextSnapshot,
): Array<{ type: "skill"; name: string; path: string }> {
if (inputs.length > 64) throw new Error("Too many explicit skill inputs");
const seen = new Set<string>();
@@ -7308,10 +7418,13 @@ export function resolveRunnerdCodexSkillInputs(
throw new Error("Explicit skill input must reference a unique assigned runtime skill");
}
seen.add(assigned.runtimeName);
const preparedSkill = preparedContext?.skills.find(skill => skill.key === assigned.key && skill.runtimeName === assigned.runtimeName);
if (preparedContext && !preparedSkill) throw new Error("Prepared launch omitted an assigned explicit skill");
return {
type: "skill",
name: assigned.runtimeName,
path: resolve(codexHome, "skills", assigned.runtimeName, "SKILL.md"),
path: preparedSkill ? resolve(preparedSkill.bundle.rootPath, "SKILL.md")
: resolve(codexHome, "skills", assigned.runtimeName, "SKILL.md"),
};
});
}
@@ -0,0 +1,84 @@
{
"commandDigest": "sha256:8c1cddce201eb0e3f2792cc5a8c79edf36b0ce823e728255c1897f95c74a3866",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 51,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "89e3dde25ff35bfd4d7f520fd9d14af3cb9027f182a2d51431d298f1015be580",
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "cc90b59bdfe6a82f7fa3e819e8671f71f7b14047e68942e8086618ffbaacf59f",
"semanticDirectDriverSourceSha256": "ff572638f2cb5d9f8249ff8e2f412311c1e3af209ef0bf7e5ea9f10a21d91103",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "f8b381e1ac2a880558f3e81968f0f8f05de3ded573d044fafc89f198f2ec0433",
"semanticValidatorsSourceSha256": "f22d9caf2035b600abc1a6625ae23ca97f1409d7e29ed5b7264bb8f9784c7dc9",
"semanticSchemaBundleSourceSha256": "016391a087cc217aafed4ba1f4efafc6e4881a7525952dce824b7f4d24530603",
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
"configuredEnvironmentSourceSha256": "5c9008d121979a3a8987e6b8a5752172eafdc4a2b5d725a73f3bbb8fb83be8ff",
"configuredEnvironmentRustSourceSha256": "85c87fae62b8a9377e4f0a4c00302c7519fae4397b1c48253f8f2b7ba7faf994",
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
"profileInstallationSourceSha256": "e4aacb08015fa9104dce2b0c7d150cc432231c8a3e07d2cec2a325b388f7f382",
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
"nativeSessionRuntimeSourceSha256": "e8a3bee66a1ff1c6f251b5ae57453ffb908336da9c4a6d98f41f94f7ee94a8e8",
"nativeRuntimeContextSourceSha256": "df93bc8bcfa9dfdcf26ad6a69ea0bd3e56ef186c5e962b243fb2288f7074584b",
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
"harnessSessionContractSourceSha256": "03f4cd7f76f34f6d82e009b66aa51f18f6e23dc235b660778060e35b6ea07412",
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
"transportContractSourceSha256": "4fa9593226ff3280bc12140bfcbc2c0986e4cc8d4733381c334138dd03b08f46",
"nativeBackendSourceSha256": "2406483fd35a564971c22e10fc2bad26bae16ff5a3fb6c65690fcf156522ab6d",
"nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44",
"nativeInstructionsSourceSha256": "ee8df5d85b263d5d2d0dc9ede8602cb7daf6388687108fe05e2371f5f56c3492",
"providerLaunchSourceSha256": "cba71368f82ae8d300123efa09f23744e19e82142788fa9ad238338fefec9624",
"launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296",
"runtimeRequestBridgeSourceSha256": "291b73a9f62793c41eb269bcbcf463140ee7d2d8dc841c6ea2474a46843d1f47",
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
"guardedInstallationSourceSha256": "f6d0c33de38259efefa0f4e0f9737cd30cfd3996a8e875e3fbdb8c72dc133dca",
"nativeExecutionSourceSha256": "fc7c6568feb0d8d709e9baa51839420c9b90336b4865a9b8405aba06e4d1e9c4",
"profileCompatibilitySourceSha256": "8b52128857f6f5aee77735c14454b0563e8d5b062238235b374b0a27393df659",
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
"assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c",
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
"credentialBindingSourceSha256": "54fd09ad430dac04df98d829061f6701e73b5bf2a0f4de836d2ac36ee511009f",
"sidecarErrorSourceSha256": "b09af4577f8bf42eee7036b2ad9842202cfc763e72b53c3287f82c6090cdad80",
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c",
"runtimeHostSourceSha256": "60e5454f030e87f2afa8b1fb120b26adb126b6b17d0e5e82f49c77f76f466cbb",
"runtimeAdapterSourceSha256": "e4c764c2c3c7dd655a965b39472bd1ad8d577d1e7258b25594b171c9429752b8",
"runtimeSandboxSourceSha256": "92dfa53f5a3ed0062913ae417eeccec243b430d444495efde2a8e9c4c02e78ea",
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
}
}