From 82257e3990820225717e10724e5c5e674fa1432f Mon Sep 17 00:00:00 2001 From: Dongni-Yang Date: Mon, 17 Aug 2026 14:12:57 +0800 Subject: [PATCH] fix(connect): run probe-only recovery when absent authority cannot be created `connect --probe-only` exited 1 before any preflight or recovery when the launch-readiness authority and receipt were both securely absent and new authority creation failed (for example, no per-user OS runtime authority in the current session). The refusal message claimed authority creation was the failure, but the command never ran the recovery it exists for. The documented contract (docs/reference/commands.mdx) says this state lets ordinary `launch` run the complete preflight and makes probe-only exit nonzero only because it could not publish evidence afterwards. `launchSandbox` already implements that: it refuses only when `recoveryBlocked` is set. Align probe-only with the same rule: exit early only for a fenceable-prior-evidence failure (`recoveryBlocked`), and let the securely-absent case enter the mutation gate, which independently re-verifies that both stores are still missing before admitting the null-epoch recovery. Publication then reports honestly that recovery succeeded but evidence could not be published. Refs #9280 Signed-off-by: Dongni Yang --- src/lib/actions/sandbox/connect-flow.test.ts | 13 ++++++++----- src/lib/actions/sandbox/connect.ts | 16 ++++++++++++---- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/lib/actions/sandbox/connect-flow.test.ts b/src/lib/actions/sandbox/connect-flow.test.ts index 376ab383747..a298408bb01 100644 --- a/src/lib/actions/sandbox/connect-flow.test.ts +++ b/src/lib/actions/sandbox/connect-flow.test.ts @@ -609,7 +609,7 @@ describe("connectSandbox flow", () => { ); }); - it("probe-only reports failure to create new authority after secure absence is proven (#8942)", async () => { + it("probe-only completes recovery after secure absence is proven and reports unpublished evidence (#9280)", async () => { const harness = createConnectHarness({ readinessDecision: { kind: "fallback", @@ -620,18 +620,21 @@ describe("connectSandbox flow", () => { fenceFailed: true, recoveryBlocked: false, }, + readinessPublicationResult: { kind: "evidence-failed" }, }); await expect(harness.connectSandbox("alpha", { probeOnly: true })).rejects.toThrow( "process.exit(1)", ); - expect(harness.checkAndRecoverSpy).not.toHaveBeenCalled(); - expect(harness.errorSpy.mock.calls.flat().join("\n")).toContain( - "no prior launch-readiness evidence can be accepted, but new launch-readiness authority could not be created", + expect(harness.checkAndRecoverSpy).toHaveBeenCalledOnce(); + expect(harness.ensureLiveSandboxSpy).toHaveBeenCalled(); + expect(harness.publishLaunchReadinessSpy).toHaveBeenCalledOnce(); + expect(harness.errorSpy).toHaveBeenCalledWith( + " Probe failed: complete probe and recovery succeeded, but final launch-readiness evidence could not be verified or published.", ); expect(harness.errorSpy.mock.calls.flat().join("\n")).not.toContain( - "prior launch-readiness evidence could not be fenced", + "new launch-readiness authority could not be created", ); }); diff --git a/src/lib/actions/sandbox/connect.ts b/src/lib/actions/sandbox/connect.ts index 8692356a3a9..ab707c81e3f 100644 --- a/src/lib/actions/sandbox/connect.ts +++ b/src/lib/actions/sandbox/connect.ts @@ -1285,11 +1285,19 @@ export async function connectSandbox( console.log(` Probe complete: launch readiness is healthy for '${sandboxName}'.`); return; } - if (readiness.fenceFailed && readiness.authorityUnsupported !== true) { + // Refuse recovery only when a prior epoch might exist and could not be + // durably rotated. When the authority and receipt are both securely + // absent but new authority creation fails (fenceFailed without + // recoveryBlocked), the documented contract runs the complete preflight + // and recovery and reports the publication failure afterwards, exactly + // as `launch` does for the same decision (#9280). + if ( + readiness.fenceFailed && + readiness.authorityUnsupported !== true && + readiness.recoveryBlocked + ) { console.error( - readiness.recoveryBlocked - ? " Probe failed: complete probe and recovery did not run because prior launch-readiness evidence could not be fenced. Repair the current user's secure OS runtime authority and NemoClaw state permissions, then retry." - : " Probe failed: no prior launch-readiness evidence can be accepted, but new launch-readiness authority could not be created. Repair the current user's secure OS runtime authority and NemoClaw state permissions, then retry.", + " Probe failed: complete probe and recovery did not run because prior launch-readiness evidence could not be fenced. Repair the current user's secure OS runtime authority and NemoClaw state permissions, then retry.", ); process.exit(1); }