diff --git a/docs/manage-sandboxes/recover-rebuild-sandboxes.mdx b/docs/manage-sandboxes/recover-rebuild-sandboxes.mdx index 953feb985c3..7fa0288a17f 100644 --- a/docs/manage-sandboxes/recover-rebuild-sandboxes.mdx +++ b/docs/manage-sandboxes/recover-rebuild-sandboxes.mdx @@ -173,7 +173,7 @@ For missing, unsafe, malformed, expired, mismatched, changed, or unhealthy evide Ordinary launch continues only when NemoClaw proves that no old authority or evidence can exist, or durably rotates the runtime epoch. If an old epoch might exist and cannot be durably rotated, `launch` and `connect --probe-only` stop before complete preflight or recovery. Their redacted guidance asks you to repair the current user's secure OS runtime authority and NemoClaw state permissions, then retry. -If NemoClaw securely proves that both the authority and receipt are absent but cannot create new authority, ordinary `launch` can run the complete preflight without optimization; `connect --probe-only` exits nonzero because it could not publish evidence. +If NemoClaw securely proves that both the authority and receipt are absent but cannot create new authority, ordinary `launch` can run the complete preflight without optimization; on Linux, `connect --probe-only` exits nonzero because it could not publish evidence. If that preflight succeeds before the lease expires, replacement evidence keeps the original start and expiry time. After expiry, a successful complete preflight starts a new 24-hour lease only when publication succeeds. @@ -190,8 +190,9 @@ Lease acceptance and publication are currently Linux-only and require a secure, It never uses caller-provided environment variables to select this authority. On macOS, `launch` runs the complete preflight every time and does not publish a launch-readiness lease. -`connect --probe-only` also runs the complete preflight, including recovery and probes, but exits nonzero because it cannot publish authoritative launch-readiness evidence. -The publication-failure diagnostic is redacted and does not print filesystem paths or environment values. +`connect --probe-only` also runs the complete preflight, including recovery and probes. +After a successful probe and recovery, it prints a note that launch-readiness evidence is unavailable on this platform and exits zero. +On Linux, the publication-failure diagnostic is redacted and does not print filesystem paths or environment values. Infrastructure must run `connect --probe-only` as the same final numeric user that later runs `launch`. Run it after the final durable home and state volume is mounted and after policy and network provisioning is complete. diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 3f3ff7d0036..1a5193eb066 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -1273,16 +1273,18 @@ It validates a usable lease and exits without duplicate recovery. Otherwise, it fences prior evidence, waits for the sandbox, verifies or repairs its in-sandbox agent process and host-side forwards, and publishes evidence only after every probe succeeds. It rechecks the sandbox on its recorded OpenShell gateway after the readiness wait and never restarts the shared host gateway. If an old runtime epoch might exist and cannot be durably rotated, the command exits nonzero before complete preflight or recovery and gives redacted repair guidance. -A securely absent runtime authority and receipt let ordinary `launch` run the complete preflight without optimization if new authority creation fails, but `connect --probe-only` still exits nonzero because it could not publish launch-readiness evidence. -A runtime failure and a failure to publish evidence for an otherwise healthy runtime also exit nonzero with different diagnostics. +A securely absent runtime authority and receipt let ordinary `launch` run the complete preflight without optimization if new authority creation fails, but on Linux `connect --probe-only` still exits nonzero because it could not publish launch-readiness evidence. +A runtime failure and, on Linux, a failure to publish evidence for an otherwise healthy runtime also exit nonzero with different diagnostics. Infrastructure must run the command as the same final numeric user that later runs `launch`. Run it only after the final durable home and state volume is mounted and after policy and network provisioning is complete. On Linux, that user also needs a secure, independently writable OS per-user runtime authority under `/run/user/`. Do not redirect this authority with caller environment variables. Do not use a graphical or login-session identifier as the deployment ordering boundary. -On macOS, `connect --probe-only` runs the complete preflight, including recovery and probes, but exits nonzero because it cannot publish authoritative launch-readiness evidence. -The publication-failure diagnostic is redacted and does not print filesystem paths or environment values. +On macOS, `connect --probe-only` runs the complete preflight, including recovery and probes. +After a successful probe and recovery, it prints a note that launch-readiness evidence is unavailable on this platform and exits zero. +The next `launch` runs the complete preflight. +On Linux, the publication-failure diagnostic is redacted and does not print filesystem paths or environment values. Run it for health checks and scripted readiness probes; users continue to run only `$$nemoclaw launch `. Use [`$$nemoclaw launch `](#$$nemoclaw-launch-name) when you want launch-readiness validation, an automatic fallback that runs the complete preflight, and then the agent instead of a sandbox shell. diff --git a/src/lib/actions/sandbox/connect-flow.test.ts b/src/lib/actions/sandbox/connect-flow.test.ts index 376ab383747..bc5c5744b71 100644 --- a/src/lib/actions/sandbox/connect-flow.test.ts +++ b/src/lib/actions/sandbox/connect-flow.test.ts @@ -635,7 +635,7 @@ describe("connectSandbox flow", () => { ); }); - it("probe-only completes macOS recovery before reporting unavailable evidence (#8942)", async () => { + it("probe-only completes macOS recovery and exits zero when evidence is unavailable (#9278)", async () => { const harness = createConnectHarness({ readinessDecision: { kind: "fallback", @@ -650,16 +650,16 @@ describe("connectSandbox flow", () => { readinessPublicationResult: { kind: "evidence-failed" }, }); - await expect(harness.connectSandbox("alpha", { probeOnly: true })).rejects.toThrow( - "process.exit(1)", - ); + await expect(harness.connectSandbox("alpha", { probeOnly: true })).resolves.toBeUndefined(); expect(harness.checkAndRecoverSpy).toHaveBeenCalledOnce(); expect(harness.ensureLiveSandboxSpy).toHaveBeenCalled(); expect(harness.publishLaunchReadinessSpy).toHaveBeenCalledOnce(); - expect(harness.errorSpy).toHaveBeenCalledWith( - " Probe failed: complete probe and recovery succeeded, but launch-readiness evidence is unavailable on this platform.", + expect(exitSpy).not.toHaveBeenCalled(); + expect(harness.logSpy).toHaveBeenCalledWith( + " Note: launch-readiness evidence is unavailable on this platform; the next launch runs the complete preflight.", ); + expect(harness.errorSpy.mock.calls.flat().join("\n")).not.toContain("Probe failed"); }); it("lets a public lifecycle command continue after recovery when evidence publication is unavailable (#8942)", async () => { diff --git a/src/lib/actions/sandbox/connect.ts b/src/lib/actions/sandbox/connect.ts index 8692356a3a9..8556ed87b24 100644 --- a/src/lib/actions/sandbox/connect.ts +++ b/src/lib/actions/sandbox/connect.ts @@ -1332,10 +1332,19 @@ export async function connectSandbox( } if (publication.kind === "evidence-failed") { if (!requireLaunchReadinessPublication) return; + // A platform without a per-user runtime authority (macOS) can never + // store launch-readiness evidence. The probe and recovery still + // succeeded, and `launch` runs the complete preflight without the + // evidence, so a permanent platform gap must not turn a successful + // probe into a nonzero exit (#9278). + if (readiness.kind === "fallback" && readiness.authorityUnsupported === true) { + console.log( + " Note: launch-readiness evidence is unavailable on this platform; the next launch runs the complete preflight.", + ); + return; + } console.error( - readiness.kind === "fallback" && readiness.authorityUnsupported === true - ? " Probe failed: complete probe and recovery succeeded, but launch-readiness evidence is unavailable on this platform." - : " Probe failed: complete probe and recovery succeeded, but final launch-readiness evidence could not be verified or published.", + " Probe failed: complete probe and recovery succeeded, but final launch-readiness evidence could not be verified or published.", ); process.exit(1); } diff --git a/test/cli/connect-recovery.test.ts b/test/cli/connect-recovery.test.ts index 1e207aef1cd..d2d76f539fc 100644 --- a/test/cli/connect-recovery.test.ts +++ b/test/cli/connect-recovery.test.ts @@ -47,12 +47,13 @@ const launchReadinessObservationStubLines = [ "fi", ]; -const expectedProbeOnlyExitCode = process.platform === "darwin" ? 1 : 0; const PLATFORM_EVIDENCE_UNAVAILABLE = "launch-readiness evidence is unavailable on this platform"; function expectProbeOnlyPublicationOutcome(result: { code: number; out: string }): void { - expect(result.code, result.out).toBe(expectedProbeOnlyExitCode); + // Evidence unavailability on macOS is a note, not a failure (#9278). + expect(result.code, result.out).toBe(0); expect(result.out.includes(PLATFORM_EVIDENCE_UNAVAILABLE)).toBe(process.platform === "darwin"); + expect(result.out.includes("Probe failed")).toBe(false); } function writeGatewayControlDockerStub( diff --git a/test/cli/connect-terminal-agent.test.ts b/test/cli/connect-terminal-agent.test.ts index f3c7ff36db8..3b1a9b06336 100644 --- a/test/cli/connect-terminal-agent.test.ts +++ b/test/cli/connect-terminal-agent.test.ts @@ -70,7 +70,8 @@ describe("CLI dispatch for terminal agents", () => { PATH: `${localBin}:${process.env.PATH || ""}`, }); - expect(r.code).toBe(process.platform === "darwin" ? 1 : 0); + // Evidence unavailability on macOS is a note, not a failure (#9278). + expect(r.code).toBe(0); expect(r.out.includes(PLATFORM_EVIDENCE_UNAVAILABLE)).toBe(process.platform === "darwin"); expect(r.out).toContain("terminal smoke checks passed"); const calls = fs.readFileSync(markerFile, "utf8").trim().split("\n").filter(Boolean); diff --git a/test/sandbox-connect-inference/auto-pair-approval.test.ts b/test/sandbox-connect-inference/auto-pair-approval.test.ts index f8bf99c19d1..e43e29f3c87 100644 --- a/test/sandbox-connect-inference/auto-pair-approval.test.ts +++ b/test/sandbox-connect-inference/auto-pair-approval.test.ts @@ -19,7 +19,8 @@ import { setupFixture, } from "./helpers"; -const expectedProbeOnlyExitCode = process.platform === "darwin" ? 1 : 0; +// Evidence unavailability on macOS is a note, not a failure (#9278). +const expectedProbeOnlyExitCode = 0; function findApprovalExec(state: { sandboxExecCalls: string[][];