From f67d2f3200c9180d0d6929f6fdb9375a97a98975 Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Mon, 14 Sep 2026 09:35:23 -0400 Subject: [PATCH] Report safe bridge failure details during runner startup --- runner/src/RepositoryWorkspace.ts | 32 ++++++++++++++++++++++----- runner/test/BridgeDiagnostics.test.ts | 29 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 runner/test/BridgeDiagnostics.test.ts diff --git a/runner/src/RepositoryWorkspace.ts b/runner/src/RepositoryWorkspace.ts index 0555d9e4..2b398ca0 100644 --- a/runner/src/RepositoryWorkspace.ts +++ b/runner/src/RepositoryWorkspace.ts @@ -10,6 +10,19 @@ import { Publication, type CredentialPermission, type RepositoryCredential } fro /** The native shell's default command deadline; explicit finite timeouts may exceed it. */ export const DEFAULT_TOOL_TIMEOUT_MS = 120000 + +// Only forward fixed bridge diagnostics. Arbitrary response bodies can contain +// repository data or credentials, including responses from the Sandbox transport. +const bridgeDiagnostics = new Set([ + "stale generation", + "stale epoch; reconcile before retry", + "clone outcome requires reconciliation", + "repository destination exists", + "repository clone failed", + "preparation outcome requires reconciliation", + "publication active", + "processes active", +]) export const REPOSITORY_TOOLS: ReadonlyArray = [ "read", "glob", @@ -262,11 +275,20 @@ export class RepositoryWorkspace { (path.startsWith("/git/") && new RunnerStorage(this.storage).disconnection !== undefined) ) throw new ProtocolError("stale_generation", "Late workspace response fenced") - if (!response.ok) - throw new ProtocolError( - response.status >= 500 ? "transport" : "blocked", - `Bridge refused operation (${response.status})`, - ) + if (!response.ok) { + const body: unknown = await response.json().catch(() => null) + const reason = + typeof body === "object" && + body !== null && + "error" in body && + typeof body.error === "string" && + bridgeDiagnostics.has(body.error) + ? `: ${body.error}` + : "" + const message = `Bridge refused ${input === undefined && !archive ? "GET" : "POST"} ${path} (${response.status})${reason}` + console.warn(message) + throw new ProtocolError(response.status >= 500 ? "transport" : "blocked", message) + } return response } private async raw(binding: Binding, path: string, input?: unknown) { diff --git a/runner/test/BridgeDiagnostics.test.ts b/runner/test/BridgeDiagnostics.test.ts new file mode 100644 index 00000000..408f3177 --- /dev/null +++ b/runner/test/BridgeDiagnostics.test.ts @@ -0,0 +1,29 @@ +import { expect, it } from "vitest" +import { Harness } from "./support/Harness.ts" + +it("session creation reports known bridge conflicts without forwarding arbitrary response text", async () => { + let error = "stale generation" + const harness = await Harness.start({ + secret: "test-model-secret", + bindings: { REPOSITORY_SERVICE_TOKEN: "test-authority-secret" }, + serviceBindings: { + REPOSITORY_AUTHORITY: async () => Response.json({ owner: "fixture", repo: "fixture" }), + REPOSITORY_TEST_TRANSPORT: async (request) => { + const path = new URL(request.url).pathname + if (path === "/running") return Response.json({ running: true }) + if (path === "/meta") return Response.json({ error }, { status: 409 }) + throw new Error(`Unexpected test request: ${path}`) + }, + }, + }) + try { + const session = harness.session("bridge-diagnostics") + const known = await session.create({ repositoryId: "123" }, 423) + expect(known.message).toBe("Bridge refused GET /meta (409): stale generation") + error = "unexpected response containing test-model-secret and test-authority-secret" + const unknown = await session.create({ repositoryId: "123" }, 423) + expect(unknown.message).toBe("Bridge refused GET /meta (409)") + } finally { + await harness.dispose() + } +})