Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions runner/src/RepositoryWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = [
"read",
"glob",
Expand Down Expand Up @@ -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) {
Expand Down
29 changes: 29 additions & 0 deletions runner/test/BridgeDiagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -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()
}
})