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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Abort hung Cloudflare terminal WebSocket upgrades after 15 seconds, matching the AWS handshake budget. Thanks @SebTardif.
- Pin the reviewed Crabbox appliance to OpenClaw, Slack, and `diagnostics-otel` `2026.7.1` with upstream managed-ClawRouter and SQLite plugin-metadata migration backports plus provider-compatible route probes.
- Harden AWS FakeCo admission with explicit verified-email handling for Cognito UserInfo, a locked Slack-off first-canary path without placeholder secrets or ingress, and ALB cookie-shard logout to an unauthenticated landing page.
- Add a protected-main FakeCo Crabhelm control-plane image publisher with a dedicated OIDC identity, landed-source fencing, native Linux/AMD64 BuildKit SBOM/provenance output, exact immutable ECR binding, canonical digest/platform proof, fail-closed vulnerability threshold, and non-secret handoff artifact.
Expand Down
39 changes: 39 additions & 0 deletions tests/worker-bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,3 +508,42 @@ test("live inference proof is re-keyed by managed policy", async () => {
assert.ok(changed.includes(`'v5:${testReleaseMarker}:p${second}:openai/gpt-5.5'`));
await run("/bin/bash", ["-n", "-c", changed]);
});

test("Cloudflare terminal upgrade aborts a hung handshake after 15 seconds", async (t) => {
t.mock.timers.enable({ apis: ["setTimeout"] });
const seen: Array<AbortSignal | null | undefined> = [];
const original = globalThis.fetch;
t.after(() => {
globalThis.fetch = original;
});
globalThis.fetch = ((_input: RequestInfo | URL, init?: RequestInit) => {
seen.push(init?.signal);
return new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true });
});
}) as typeof fetch;

const claw = createClawRecord({
name: "Terminal child",
owner: { subject: "github:terminal", label: "@terminal", source: "github" },
});
const bootstrap = new CrabboxWorkspaceBootstrap({
brokerToken: "broker-test-token",
publicUrl: "https://crabhelm.example.test",
releaseId: "a".repeat(64),
archiveId: "c".repeat(64),
nodeId: "e".repeat(64),
signingSecret: testSigningKey,
});

const diagnostics = bootstrap.runtimeDiagnostics(claw, {
status: "ready",
attachUrl: "wss://crabbox.example.test/attach",
});
t.mock.timers.tick(15_000);

await assert.rejects(diagnostics, /terminal handshake timed out/u);
assert.equal(seen.length, 1);
assert.ok(seen[0] instanceof AbortSignal);
assert.equal(seen[0].aborted, true);
});
26 changes: 19 additions & 7 deletions worker/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,20 +562,32 @@ async function captureTerminalSection(
});
}

const handshakeTimeoutMs = 15_000;

async function cloudflareTerminalDialer(
attachUrl: string,
brokerToken: string,
): Promise<TerminalSocket> {
const url = new URL(attachUrl);
if (url.protocol !== "wss:") throw new Error("Crabbox terminal URL must use WSS");
url.protocol = "https:";
const response = await fetch(url, {
headers: { authorization: `Bearer ${brokerToken}`, upgrade: "websocket" },
}) as Response & { webSocket?: WorkerWebSocket };
const socket = response.webSocket;
if (response.status !== 101 || !socket) throw new Error(`Crabbox terminal upgrade failed (HTTP ${response.status})`);
socket.accept();
return socket;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), handshakeTimeoutMs);
try {
const response = await fetch(url, {
headers: { authorization: `Bearer ${brokerToken}`, upgrade: "websocket" },
signal: controller.signal,
}) as Response & { webSocket?: WorkerWebSocket };
const socket = response.webSocket;
if (response.status !== 101 || !socket) throw new Error(`Crabbox terminal upgrade failed (HTTP ${response.status})`);
socket.accept();
return socket;
} catch (error) {
if (controller.signal.aborted) throw new Error("Crabbox terminal handshake timed out");
throw error;
} finally {
clearTimeout(timer);
}
}

function terminalInferenceFailure(
Expand Down