diff --git a/docs/manage-sandboxes/recover-rebuild-sandboxes.mdx b/docs/manage-sandboxes/recover-rebuild-sandboxes.mdx index 953feb985c..7fa0288a17 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 3f3ff7d003..1a5193eb06 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 376ab38374..bc5c5744b7 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 8692356a3a..8556ed87b2 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 1e207aef1c..d2d76f539f 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 f3c7ff36db..3b1a9b0633 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 f8bf99c19d..e43e29f3c8 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[][];