diff --git a/doc/plans/2026-10-06-default-agent-runner.md b/doc/plans/2026-10-06-default-agent-runner.md index 1a1030a4f0b..335498e4ac7 100644 --- a/doc/plans/2026-10-06-default-agent-runner.md +++ b/doc/plans/2026-10-06-default-agent-runner.md @@ -1681,3 +1681,9 @@ The corrected source passes the full existing E2E support suite:125 files/2697 t Current published Codex head1a0ca662 is draft with green historical checks. Actual master833769ab is newer than merged prerequisite d67 and release repair60bc. Prepared private onboarding/Storybook work will be reconciled by source, without claiming its private/historical checks as final qualification. Native Connect correction is already in the Codex slice. Final managed defaults require the saved immutable OCI environment probe owned by ordinary Connect. Existing suites/fixtures/assertions/reporting are reused; no duplicate grader or silent Legacy fallback is added. All heavy builds stay remote, Codex payloads remain external official npm dependencies, and saved agents retain runners. Next action: preserve/commit current evidence, integrate actual master plus reviewed prepared corrections, run targeted checks, freeze/push candidate, require fresh fullCI/review, then qualify ordinary creation/onboarding on Mac and the one staging target before protected squash merge. + +The frozen30bc candidate passes all current-head required CI and fresh5/5 review. Its existing remote release-smoke qualification passes all four producers and clean platform consumers. The actual managed onboarding cell38428601763 fails before agent creation: scoped discovery selects native, but Codex0.160 attempts a nested filesystem sandbox while scanning AGENTS.md for the read-only setup session. Both measured setup leases release successfully; the separate fixture environment deletion fails because the fixture leaves it selected as instance default. Preserve this failed attempt, unavailable provider billing and cleanup holds. No task artifact or follow-up is reached. + +The correction skips project instruction discovery only in the new empty setup workspace through the supported trusted Codex configuration. It retains the generated security arguments, read-only permission profile, approval-never, no dynamic tools, existing bounded hello turn and teardown deadlines. Ordinary task instruction discovery and saved agent configurations remain unchanged. Pinned official Codex0.160 source confirms the zero instruction limit precedes filesystem probes. Eight inert baseline failures become23 passing source/cleanup neighbors; direct runner/server TypeScript and maps pass. This does not establish hosted treatment. + +The existing managed first-task fixture now snapshots and restores its prior instance default and managed-only setting before deletion, including lost setup responses. Unconfirmed default restoration blocks deletion and remains red; other restoration/deletion errors remain inspectable and retry only outstanding cleanup. Three reproduced baseline failures become129 passing first-task controls, with direct E2E TypeScript passing. Independent source review finds no actionable runtime/permission P1/P2; the required source-comment cleanup is applied. Final source needs fresh CI/review, package artifacts and successful ordinary Mac/managed journeys before normal merge. The one owned Core preview deployment for30bc completes through its existing exact-source check/deploy path; actual readiness is independently checked next. No fleet default is changed. diff --git a/maps/agent-access-map/onboarding.md b/maps/agent-access-map/onboarding.md index 5530589d3f6..23fa9610253 100644 --- a/maps/agent-access-map/onboarding.md +++ b/maps/agent-access-map/onboarding.md @@ -102,6 +102,10 @@ target, as task execution does. This is an internal transport choice, not a request/configuration permission. Local and SSH setup keep Codex isolation; caller-supplied environment flags do not authorize an external sandbox. Existing company, account, probe and registration-cleanup gates still apply. +The bounded hello probe retains read-only permissions and no dynamic tools. Its +trusted launch skips project-instruction discovery in the new, empty setup +workspace. This grants no filesystem or networking permission and does not +change instruction discovery for ordinary task execution. Scoped source review: 2026-10-13, exact baseline `1a0ca66242121e39d88455aa2b52a07acc4a07d9` plus the privately composed onboarding diff --git a/maps/feature-map/onboarding.md b/maps/feature-map/onboarding.md index 6de1b9b52ff..e2782e56452 100644 --- a/maps/feature-map/onboarding.md +++ b/maps/feature-map/onboarding.md @@ -70,11 +70,14 @@ themselves allocate no target. A valid measured unsupported target follows the server's Legacy default. Named Cloud snapshots remain a separate unqualified boot-identity and fresh-signup gap. -Selected native setup must use the same isolation as task execution. A resolved -sandbox transport uses the provider's outer isolation; local and SSH transports -retain Codex isolation. An injected environment flag cannot establish that -transport identity. Verify these boundaries with the existing native setup -controls and a real managed task/follow-up; source checks alone are insufficient. +Selected native setup derives its target isolation as task execution does. The +hello probe retains its read-only permission profile, approval-never policy and +bounded provider turn. It skips Codex's project-instruction discovery in its new, +empty private workspace; ordinary tasks still load their project instructions. +Local and SSH transports retain Codex isolation. An injected environment flag +cannot establish an outer sandbox identity. Verify these boundaries with the +existing native setup controls and a real managed task/follow-up; source checks +alone are insufficient. The explicit manual Product E2E cell `confirmation-replies.runner-codex.daytona.task-card-accept` covers this fresh diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts index f58e018a809..39c57b8a064 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts @@ -2140,6 +2140,7 @@ it("denies the isolated Codex home without denying a remote execution workspace" readOnlyRoots: ["/usr/local"], }); const serialized = args.join("\n"); + expect(args).not.toContain("project_doc_max_bytes=0"); expect(serialized).toContain( '"/workspaces/task/.paperclip-runtime/paperclip-runner/sessions/session/filesystem/codex-home"="none"', @@ -2890,6 +2891,12 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper expect(registrationSignal?.aborted).toBe(true); const failure = await result; expect(template).toMatchObject({ provider: { externallySandboxed: mode === "launch-failure-sandbox", readOnly: true, approvalPolicy: "never", collaborationMode: "plan" } }); + const args = (template!.provider as Record).args as string[]; + expect(args).toContain("project_doc_max_bytes=0"); + expect(args[args.indexOf("project_doc_max_bytes=0") - 1]).toBe("-c"); + expect(args.indexOf("project_doc_max_bytes=0")).toBeLessThan(args.indexOf("app-server")); + expect(args).not.toContain("--dangerously-bypass-approvals-and-sandbox"); + expect(args).toContain('permissions.paperclip-runner-workspace-read-only.network.enabled=false'); expect(failure).toBeInstanceOf(Error); expect((failure as Error).message).toContain(launchFailure ? "owned launcher failed before execution" : "Native provider hello probe timed out."); if (mode === "stuck" || mode === "stuck-release" || mode === "late-release-error") { diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index 32d623cef3b..0ec163e20b7 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -1261,6 +1261,7 @@ export interface CapabilityRunnerdCodexTransportOptions { runnerBinary?: string; codexCommand?: string; codexArgs?: string[]; + disableCodexProjectInstructions?: boolean; /** Controller-visible Codex home used only to seed the isolated runner home. */ sourceCodexHome?: string | null; opencodeCommand?: string; @@ -3462,20 +3463,24 @@ export function createRunnerdCodexAppServerArgs(input: { codexCommand?: string; readOnlyRoots?: string[]; instructionWorkingCopyRoot?: string; + disableProjectInstructions?: boolean; }): string[] { // The filesystem policy denies HOME and CODEX_HOME to keep credentials and // runner state outside provider reach. Always bind those names to the actual // isolated runner home; a stale controller environment must never cause the // execution workspace itself to become an explicit deny root. - return createIsolatedCodexAppServerArgs( - { - ...input.environment, - HOME: input.codexHome, - CODEX_HOME: input.codexHome, - }, - [...(input.readOnlyRoots ?? []), ...codexExecutableReadOnlyRoots(input.environment ?? {}, input.codexCommand)], - input.instructionWorkingCopyRoot, - ); + return [ + ...(input.disableProjectInstructions ? ["-c", "project_doc_max_bytes=0"] : []), + ...createIsolatedCodexAppServerArgs( + { + ...input.environment, + HOME: input.codexHome, + CODEX_HOME: input.codexHome, + }, + [...(input.readOnlyRoots ?? []), ...codexExecutableReadOnlyRoots(input.environment ?? {}, input.codexCommand)], + input.instructionWorkingCopyRoot, + ), + ]; } export function unwrapToolResponse(response: Record, preserveEnvelope = false): { @@ -5028,6 +5033,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { codexHome, codexCommand: effectiveCodexCommand, instructionWorkingCopyRoot: runtimeContext?.instructions.workingCopy?.rootPath, + disableProjectInstructions: this.options.disableCodexProjectInstructions, readOnlyRoots: [ ...trustedRuntimeReadOnlyRoots( this.options.environment, @@ -5523,6 +5529,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { codexHome, codexCommand: typeof recordedCommand === "string" ? recordedCommand : this.options.codexCommand, instructionWorkingCopyRoot: runtimeContext?.instructions.workingCopy?.rootPath, + disableProjectInstructions: this.options.disableCodexProjectInstructions, readOnlyRoots: [ ...trustedRuntimeReadOnlyRoots(this.options.environment), ...(runtimeContext @@ -7211,6 +7218,7 @@ export async function probeNativeRunnerEnvironment(options: { stateDirectory: options.runtimeDirectory, environment, externallySandboxed: options.transportOptions?.externallySandboxed === true, + disableCodexProjectInstructions: true, closeGraceMs: 10_000, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null }, }); diff --git a/tests/runner-e2e/first-task-fixtures.ts b/tests/runner-e2e/first-task-fixtures.ts index e702a269004..dfa8e5ab2e5 100644 --- a/tests/runner-e2e/first-task-fixtures.ts +++ b/tests/runner-e2e/first-task-fixtures.ts @@ -38,6 +38,15 @@ export async function provisionFirstTaskFixtures(input: { } const required = managed ? [execution.profile.credential, "DAYTONA_API_KEY" as const] : [execution.profile.credential]; for (const name of required) if (!credentials[name]) throw new Error(`Missing credential ${name}`); + const previousSettings = managed ? await api.get<{ + defaultEnvironmentId: string | null; + experimental: { enableManagedSandboxOnly: boolean }; + }>("/api/instance/settings") : null; + if (managed && (!previousSettings || + (previousSettings.defaultEnvironmentId !== null && typeof previousSettings.defaultEnvironmentId !== "string") || + typeof previousSettings.experimental?.enableManagedSandboxOnly !== "boolean")) { + throw new Error("Managed onboarding fixture prior instance settings could not be read"); + } await api.patch(`/api/companies/${company.id}`, { budgetMonthlyCents: 500 }); let localEnvironment: LiveFixtureValues["environment"] | undefined; if (!managed) { @@ -59,9 +68,37 @@ export async function provisionFirstTaskFixtures(input: { await installFixtureDaytonaProvider(remoteApi, execution); environment = await remoteApi.post(`/api/companies/${company.id}/environments`, execution.environment.buildEnvironment({ secretRefs, daytonaImage: input.daytonaImage, executionId: nonce })); - teardown = () => deleteDaytonaEnvironment(remoteApi, environment.id); + let restoreDefault = false; + let restoreManagedOnly = false; + let environmentDeleted = false; + teardown = async () => { + const failures: unknown[] = []; + if (restoreManagedOnly) { + try { + await api.patch("/api/instance/settings/experimental", { enableManagedSandboxOnly: previousSettings!.experimental.enableManagedSandboxOnly }); + restoreManagedOnly = false; + } catch (error) { failures.push(error); } + } + if (restoreDefault) { + try { + await api.patch("/api/instance/settings", { defaultEnvironmentId: previousSettings!.defaultEnvironmentId }); + restoreDefault = false; + } catch (error) { failures.push(error); } + } + if (!restoreDefault && !environmentDeleted) { + try { + await deleteDaytonaEnvironment(remoteApi, environment.id); + environmentDeleted = true; + } catch (error) { failures.push(error); } + } + if (failures.length) throw new AggregateError(failures, restoreDefault + ? "Managed onboarding fixture cleanup failed; instance default restoration is unconfirmed and environment deletion was not attempted" + : "Managed onboarding fixture cleanup failed"); + }; try { + restoreDefault = true; await api.patch("/api/instance/settings", { defaultEnvironmentId: environment.id }); + restoreManagedOnly = true; await api.patch("/api/instance/settings/experimental", { enableManagedSandboxOnly: true }); } catch (error) { try { await teardown(); } catch (cleanup) { throw new AggregateError([error, cleanup], "Managed onboarding fixture setup and cleanup failed"); } diff --git a/tests/runner-e2e/first-task.test.ts b/tests/runner-e2e/first-task.test.ts index b8862648f43..7eab98151c4 100644 --- a/tests/runner-e2e/first-task.test.ts +++ b/tests/runner-e2e/first-task.test.ts @@ -1450,10 +1450,151 @@ describe("managed onboarding through the production wizard", () => { const image = `registry.example.test/runtime@sha256:${"a".repeat(64)}`; const execution = () => runnerMatrix.find(entry => entry.id === MANAGED_FIRST_TASK_CELL)!; + function managedFixtureApi(previousDefault: string | null = "previous-default", previousManagedOnly = false) { + const state = { defaultEnvironmentId: previousDefault, experimental: { enableManagedSandboxOnly: previousManagedOnly, enableAgentChat: true } }; + const calls: string[] = []; + const api = { + get: vi.fn(async () => structuredClone(state)), + patch: vi.fn(async (path: string, data: any) => { + calls.push(`PATCH ${path}`); + if (path === "/api/instance/settings") state.defaultEnvironmentId = data.defaultEnvironmentId; + if (path === "/api/instance/settings/experimental") Object.assign(state.experimental, data); + return structuredClone(state); + }), + postSensitive: vi.fn(async (_path: string, data: any) => ({ id: `encrypted-${data.key}` })), + post: vi.fn(async (path: string, data: any) => { + calls.push(`POST ${path}`); + return path === "/api/plugins/install" ? { id: "plugin", status: "ready" } : { id: "environment", driver: "sandbox", config: data.config }; + }), + delete: vi.fn(async (path: string) => { calls.push(`DELETE ${path}`); }), + }; + const provision = () => provisionFirstTaskFixtures({ api: api as any, execution: execution(), nonce: "private", company: { id: "company", name: "Fresh" }, + daytonaImage: image, credentials: { OPENAI_API_KEY: "synthetic-key", DAYTONA_API_KEY: "synthetic-platform" } }); + return { api, state, calls, provision }; + } + + it.each([[null, false], ["previous-default", false], ["previous-default", true]] as const)( + "restores prior default %s and managed-only %s before deleting the managed fixture", async (previousDefault, previousManagedOnly) => { + const h = managedFixtureApi(previousDefault, previousManagedOnly); + const fixtures = await h.provision(); + expect(h.state).toMatchObject({ defaultEnvironmentId: "environment", experimental: { enableManagedSandboxOnly: true } }); + let deletedState: typeof h.state | undefined; + h.api.delete.mockImplementation(async () => { deletedState = structuredClone(h.state); }); + await fixtures.teardown(); + expect(deletedState).toMatchObject({ defaultEnvironmentId: previousDefault, experimental: { enableManagedSandboxOnly: previousManagedOnly } }); + expect(h.api.get).toHaveBeenCalledExactlyOnceWith("/api/instance/settings"); + expect(h.api.patch).toHaveBeenLastCalledWith("/api/instance/settings", { defaultEnvironmentId: previousDefault }); + expect(h.state.experimental.enableAgentChat).toBe(true); + expect(h.api.delete).toHaveBeenCalledExactlyOnceWith("/api/environments/environment?destroyReusableSandboxLeases=true", { allowNotFound: true }); + }, + ); + + it.each(["default", "managed-only"])("restores a %s setup write whose response is lost after committing", async phase => { + const h = managedFixtureApi(); + const patch = h.api.patch.getMockImplementation()!; + let failed = false; + h.api.patch.mockImplementation(async (path, data) => { + const result = await patch(path, data); + if (!failed && (phase === "default" ? data.defaultEnvironmentId === "environment" : data.enableManagedSandboxOnly === true)) { + failed = true; + throw new Error("Setup response lost after commit"); + } + return result; + }); + await expect(h.provision()).rejects.toThrow("Setup response lost after commit"); + expect(h.state).toEqual({ defaultEnvironmentId: "previous-default", experimental: { enableManagedSandboxOnly: false, enableAgentChat: true } }); + expect(h.api.delete).toHaveBeenCalledTimes(1); + if (phase === "default") expect(h.api.patch.mock.calls.some(([path]) => path === "/api/instance/settings/experimental")).toBe(false); + }); + + it("retains setup and restoration failures instead of deleting an unconfirmed instance default", async () => { + const h = managedFixtureApi(); + const patch = h.api.patch.getMockImplementation()!; + h.api.patch.mockImplementation(async (path, data) => { + if (path === "/api/instance/settings/experimental" && data.enableManagedSandboxOnly === true) throw new Error("Managed mode setup failed"); + if (path === "/api/instance/settings" && data.defaultEnvironmentId === "previous-default") throw new Error("Default restoration failed"); + return patch(path, data); + }); + const error = await h.provision().catch(error => error); + expect(error).toBeInstanceOf(AggregateError); + expect(error.errors[0].message).toBe("Managed mode setup failed"); + expect(error.errors[1].errors[0].message).toBe("Default restoration failed"); + expect(error.errors[1].message).toContain("environment deletion was not attempted"); + expect(h.api.delete).not.toHaveBeenCalled(); + expect(h.state.defaultEnvironmentId).toBe("environment"); + }); + + it("keeps failed default restoration outstanding and only deletes after a later confirmed restoration", async () => { + const h = managedFixtureApi(); + const fixtures = await h.provision(); + const patch = h.api.patch.getMockImplementation()!; + h.api.patch.mockImplementation(async (path, data) => { + if (path === "/api/instance/settings") throw new Error("Default restoration failed"); + return patch(path, data); + }); + await expect(fixtures.teardown()).rejects.toThrow("environment deletion was not attempted"); + expect(h.state).toMatchObject({ defaultEnvironmentId: "environment", experimental: { enableManagedSandboxOnly: false } }); + expect(h.api.delete).not.toHaveBeenCalled(); + h.api.patch.mockImplementation(patch); + await fixtures.teardown(); + await fixtures.teardown(); + expect(h.state.defaultEnvironmentId).toBe("previous-default"); + expect(h.api.delete).toHaveBeenCalledTimes(1); + }); + + it("reports failed managed-mode restoration while still removing an environment that is no longer the default", async () => { + const h = managedFixtureApi(); + const fixtures = await h.provision(); + h.state.experimental.enableAgentChat = false; + const patch = h.api.patch.getMockImplementation()!; + h.api.patch.mockImplementation(async (path, data) => { + if (path === "/api/instance/settings/experimental") throw new Error("Managed mode restoration failed"); + return patch(path, data); + }); + const error = await fixtures.teardown().catch(error => error); + expect(error).toBeInstanceOf(AggregateError); + expect(error.errors.map((error: Error) => error.message)).toEqual(["Managed mode restoration failed"]); + expect(h.state.defaultEnvironmentId).toBe("previous-default"); + expect(h.api.delete).toHaveBeenCalledTimes(1); + h.api.patch.mockImplementation(patch); + await fixtures.teardown(); + expect(h.state.experimental).toEqual({ enableManagedSandboxOnly: false, enableAgentChat: false }); + expect(h.api.delete).toHaveBeenCalledTimes(1); + }); + + it("keeps failed environment deletion red after successful settings restoration", async () => { + const h = managedFixtureApi(); + const fixtures = await h.provision(); + h.api.delete.mockRejectedValue(new Error("Provider cleanup unconfirmed")); + vi.useFakeTimers(); + try { + const rejected = expect(fixtures.teardown()).rejects.toThrow("Managed onboarding fixture cleanup failed"); + await vi.runAllTimersAsync(); + await rejected; + expect(h.state).toMatchObject({ defaultEnvironmentId: "previous-default", experimental: { enableManagedSandboxOnly: false } }); + h.api.delete.mockResolvedValue(); + await fixtures.teardown(); + const deletes = h.api.delete.mock.calls.length; + await fixtures.teardown(); + expect(h.api.delete).toHaveBeenCalledTimes(deletes); + } finally { vi.useRealTimers(); } + }); + + it.each(["read-failure", "missing-default", "missing-mode"])("fails before fixture mutations when prior settings have %s", async mode => { + const h = managedFixtureApi(); + if (mode === "read-failure") h.api.get.mockRejectedValue(new Error("Settings read failed")); + else h.api.get.mockResolvedValue((mode === "missing-default" ? { experimental: { enableManagedSandboxOnly: false } } : { defaultEnvironmentId: null, experimental: {} }) as any); + await expect(h.provision()).rejects.toThrow(mode === "read-failure" ? "Settings read failed" : "prior instance settings could not be read"); + expect(h.api.patch).not.toHaveBeenCalled(); + expect(h.api.postSensitive).not.toHaveBeenCalled(); + expect(h.api.post).not.toHaveBeenCalled(); + expect(h.api.delete).not.toHaveBeenCalled(); + }); + it("prepares only encrypted credentials and an immutable managed default, with no fixture probe or agent", async () => { const calls: string[] = []; const api = { - get: vi.fn(async () => []), + get: vi.fn(async () => ({ defaultEnvironmentId: null, experimental: { enableManagedSandboxOnly: false } })), patch: vi.fn(async (path: string) => { calls.push(`PATCH ${path}`); return {}; }), postSensitive: vi.fn(async (_path: string, data: any) => ({ id: `encrypted-${data.key}` })), post: vi.fn(async (path: string, data: any) => {