diff --git a/src/lib/build-context.ts b/src/lib/build-context.ts index 2407f2ebba3..e67a7e693a6 100644 --- a/src/lib/build-context.ts +++ b/src/lib/build-context.ts @@ -72,6 +72,12 @@ export function printSandboxCreateRecoveryHints(output = ""): void { ); return; } + if (failure.kind === "tls_cert_mismatch") { + console.error(" Hint: TLS certificate mismatch — the gateway certificate changed since the CLI last trusted it."); + console.error(" Fix: openshell gateway trust -g nemoclaw"); + console.error(" Then: nemoclaw onboard --resume"); + return; + } console.error(" Recovery: nemoclaw onboard --resume"); console.error(" Or: nemoclaw onboard"); } diff --git a/src/lib/validation.test.ts b/src/lib/validation.test.ts index 9bd4a521879..a81639387c0 100644 --- a/src/lib/validation.test.ts +++ b/src/lib/validation.test.ts @@ -182,6 +182,49 @@ describe("classifySandboxCreateFailure", () => { const result = classifySandboxCreateFailure("something else happened"); expect(result.kind).toBe("unknown"); }); + + it("detects TLS cert error from 'invalid peer certificate: BadSignature'", () => { + const result = classifySandboxCreateFailure("invalid peer certificate: BadSignature"); + expect(result.kind).toBe("tls_cert_mismatch"); + }); + + it("detects TLS cert error from 'handshake verification failed'", () => { + const result = classifySandboxCreateFailure("handshake verification failed"); + expect(result.kind).toBe("tls_cert_mismatch"); + }); + + it("detects TLS cert error from 'certificate verify failed'", () => { + const result = classifySandboxCreateFailure("certificate verify failed"); + expect(result.kind).toBe("tls_cert_mismatch"); + }); + + it("detects TLS cert error from 'SSL certificate problem'", () => { + const result = classifySandboxCreateFailure("SSL certificate problem: unable to get local issuer certificate"); + expect(result.kind).toBe("tls_cert_mismatch"); + }); + + it("detects TLS cert error from 'x509: certificate'", () => { + const result = classifySandboxCreateFailure("x509: certificate signed by unknown authority"); + expect(result.kind).toBe("tls_cert_mismatch"); + }); + + it("detects TLS cert error from 'unknown authority'", () => { + const result = classifySandboxCreateFailure("failed: unknown authority"); + expect(result.kind).toBe("tls_cert_mismatch"); + }); + + it("does NOT classify generic TLS transport errors as tls_cert_mismatch", () => { + expect(classifySandboxCreateFailure("TLS error: connection refused by proxy").kind).toBe("unknown"); + expect(classifySandboxCreateFailure("SSL error: unsupported protocol version").kind).toBe("unknown"); + expect(classifySandboxCreateFailure("TLS error later during notify").kind).toBe("unknown"); + expect(classifySandboxCreateFailure("ssl error: peer closed connection").kind).toBe("unknown"); + }); + + it("classifies tls_cert_mismatch even when 'Created sandbox:' is also present", () => { + const output = "Created sandbox: test-sandbox\nError: handshake verification failed"; + const result = classifySandboxCreateFailure(output); + expect(result.kind).toBe("tls_cert_mismatch"); + }); }); describe("validateNvidiaApiKeyValue", () => { diff --git a/src/lib/validation.ts b/src/lib/validation.ts index 7125abc77cf..5e030f5d5c6 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -13,7 +13,7 @@ export interface ValidationClassification { } export interface SandboxCreateFailure { - kind: "image_transfer_timeout" | "image_transfer_reset" | "sandbox_create_incomplete" | "unknown"; + kind: "image_transfer_timeout" | "image_transfer_reset" | "sandbox_create_incomplete" | "tls_cert_mismatch" | "unknown"; uploadedToGateway: boolean; } @@ -72,6 +72,9 @@ export function classifySandboxCreateFailure(output = ""): SandboxCreateFailure if (/Connection reset by peer/i.test(text)) { return { kind: "image_transfer_reset", uploadedToGateway }; } + if (/invalid peer certificate|BadSignature|handshake verification failed|certificate verify failed|SSL certificate problem|x509: certificate|unknown authority/i.test(text)) { + return { kind: "tls_cert_mismatch", uploadedToGateway }; + } if (/Created sandbox:/i.test(text)) { return { kind: "sandbox_create_incomplete", uploadedToGateway: true }; }