agent-dealer 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/server/dist/adapters/agent-deck-bind.js +443 -0
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +454 -0
- package/bundle/server/dist/adapters/agent-deck.js +38 -50
- package/bundle/server/dist/adapters/agent-deck.test.js +63 -0
- package/bundle/server/dist/adapters/agent-health.js +230 -33
- package/bundle/server/dist/adapters/agent-health.test.js +237 -0
- package/bundle/server/dist/adapters/authority-lifecycle.js +289 -0
- package/bundle/server/dist/adapters/codex-scoped-config.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.test.js +166 -0
- package/bundle/server/dist/adapters/external.js +0 -1
- package/bundle/server/dist/adapters/git-worktree.js +514 -0
- package/bundle/server/dist/adapters/git-worktree.test.js +502 -0
- package/bundle/server/dist/adapters/github.js +170 -0
- package/bundle/server/dist/adapters/github.test.js +205 -0
- package/bundle/server/dist/adapters/linear-inbox.js +199 -30
- package/bundle/server/dist/adapters/linear-inbox.test.js +35 -0
- package/bundle/server/dist/adapters/linear-sync.js +3 -101
- package/bundle/server/dist/adapters/managed-repo.js +168 -0
- package/bundle/server/dist/adapters/managed-repo.test.js +63 -0
- package/bundle/server/dist/adapters/outbound-delivery.js +83 -0
- package/bundle/server/dist/adapters/outbound-delivery.test.js +53 -0
- package/bundle/server/dist/adapters/reflect-authority.js +43 -0
- package/bundle/server/dist/cli-env.js +26 -0
- package/bundle/server/dist/cli-env.test.js +14 -1
- package/bundle/server/dist/coordinator/abort.integration.test.js +74 -0
- package/bundle/server/dist/coordinator/admission.js +330 -0
- package/bundle/server/dist/coordinator/admission.test.js +338 -0
- package/bundle/server/dist/coordinator/args.js +128 -0
- package/bundle/server/dist/coordinator/args.test.js +132 -0
- package/bundle/server/dist/coordinator/authority-recovery.integration.test.js +661 -0
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/auto-merge.js +286 -0
- package/bundle/server/dist/coordinator/auto-merge.timeout.test.js +14 -0
- package/bundle/server/dist/coordinator/branch-progress.js +81 -0
- package/bundle/server/dist/coordinator/clock-jump.js +98 -0
- package/bundle/server/dist/coordinator/commands.js +1088 -0
- package/bundle/server/dist/coordinator/commands.test.js +554 -0
- package/bundle/server/dist/coordinator/deck-outage-config.js +32 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +200 -0
- package/bundle/server/dist/coordinator/dependencies.js +242 -0
- package/bundle/server/dist/coordinator/dependency-readiness.integration.test.js +535 -0
- package/bundle/server/dist/coordinator/developer-effect.js +959 -0
- package/bundle/server/dist/coordinator/developer-effect.test.js +1143 -0
- package/bundle/server/dist/coordinator/dispatcher.js +58 -0
- package/bundle/server/dist/coordinator/dispatcher.test.js +81 -0
- package/bundle/server/dist/coordinator/effect-registry.js +17 -0
- package/bundle/server/dist/coordinator/execution-env.integration.test.js +122 -0
- package/bundle/server/dist/coordinator/failure-reason.js +273 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +234 -0
- package/bundle/server/dist/coordinator/guidance.js +33 -0
- package/bundle/server/dist/coordinator/guidance.test.js +110 -0
- package/bundle/server/dist/coordinator/human-resolution.js +88 -0
- package/bundle/server/dist/coordinator/human-resolution.test.js +72 -0
- package/bundle/server/dist/coordinator/intent-forecast.js +13 -0
- package/bundle/server/dist/coordinator/intent-forecast.test.js +34 -0
- package/bundle/server/dist/coordinator/latest-failure.js +100 -0
- package/bundle/server/dist/coordinator/latest-failure.test.js +125 -0
- package/bundle/server/dist/coordinator/metrics.js +35 -0
- package/bundle/server/dist/coordinator/metrics.test.js +52 -0
- package/bundle/server/dist/coordinator/permissions.js +127 -0
- package/bundle/server/dist/coordinator/permissions.test.js +184 -0
- package/bundle/server/dist/coordinator/presumed-dead-budget.integration.test.js +146 -0
- package/bundle/server/dist/coordinator/process-liveness.js +191 -0
- package/bundle/server/dist/coordinator/profile-snapshot.js +20 -0
- package/bundle/server/dist/coordinator/profile-snapshot.test.js +118 -0
- package/bundle/server/dist/coordinator/projection.js +147 -0
- package/bundle/server/dist/coordinator/projection.test.js +121 -0
- package/bundle/server/dist/coordinator/prompts.js +164 -0
- package/bundle/server/dist/coordinator/prompts.test.js +177 -0
- package/bundle/server/dist/coordinator/queue-by-default.integration.test.js +271 -0
- package/bundle/server/dist/coordinator/reclaim-republish.integration.test.js +372 -0
- package/bundle/server/dist/coordinator/recovery.js +466 -0
- package/bundle/server/dist/coordinator/recovery.test.js +164 -0
- package/bundle/server/dist/coordinator/reflect-trigger.js +218 -0
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +324 -0
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +234 -0
- package/bundle/server/dist/coordinator/restart-recovery.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +507 -0
- package/bundle/server/dist/coordinator/reviewer-effect.test.js +612 -0
- package/bundle/server/dist/coordinator/reviewer-result.js +38 -0
- package/bundle/server/dist/coordinator/reviewer-result.test.js +45 -0
- package/bundle/server/dist/coordinator/routing.js +160 -0
- package/bundle/server/dist/coordinator/routing.test.js +336 -0
- package/bundle/server/dist/coordinator/session-lifecycle.integration.test.js +127 -0
- package/bundle/server/dist/coordinator/session-lifecycle.js +280 -0
- package/bundle/server/dist/coordinator/session-lifecycle.test.js +121 -0
- package/bundle/server/dist/coordinator/session-progress.js +351 -0
- package/bundle/server/dist/coordinator/session-progress.test.js +190 -0
- package/bundle/server/dist/coordinator/session-timeouts.js +33 -0
- package/bundle/server/dist/coordinator/sleep-recovery.integration.test.js +233 -0
- package/bundle/server/dist/coordinator/spawn.js +49 -0
- package/bundle/server/dist/coordinator/sync-issue-base-branch.js +18 -0
- package/bundle/server/dist/coordinator/usage-cap-config.js +10 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +189 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +193 -0
- package/bundle/server/dist/coordinator/usage.js +40 -0
- package/bundle/server/dist/coordinator/usage.test.js +69 -0
- package/bundle/server/dist/coordinator/verification-receipt.js +333 -0
- package/bundle/server/dist/coordinator/verification-receipt.test.js +382 -0
- package/bundle/server/dist/coordinator/worker-loop.js +363 -0
- package/bundle/server/dist/coordinator/worker-loop.test.js +327 -0
- package/bundle/server/dist/coordinator/workflow-instances.test.js +45 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.js +82 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +199 -0
- package/bundle/server/dist/db/artifacts-issue-columns.test.js +18 -0
- package/bundle/server/dist/db/authority-attempts-stale-at-migration.test.js +65 -0
- package/bundle/server/dist/db/idempotency-index-migration.test.js +57 -0
- package/bundle/server/dist/db/index.js +359 -7
- package/bundle/server/dist/db/linear-inbox-unlock.test.js +67 -0
- package/bundle/server/dist/db/migrate-to-issues.js +824 -0
- package/bundle/server/dist/db/migrate-to-issues.test.js +842 -0
- package/bundle/server/dist/db/schema.sql +311 -1
- package/bundle/server/dist/db/schema.test.js +66 -0
- package/bundle/server/dist/db/worker-session-pid-migration.test.js +112 -0
- package/bundle/server/dist/dev-review-cli-happy-path.integration.test.js +338 -0
- package/bundle/server/dist/direct-start-liveness.integration.test.js +350 -0
- package/bundle/server/dist/index.js +58 -5
- package/bundle/server/dist/paths.js +40 -4
- package/bundle/server/dist/paths.test.js +42 -0
- package/bundle/server/dist/pid-marker-claim-child.js +18 -0
- package/bundle/server/dist/pid-marker-hold-arbiter-child.js +24 -0
- package/bundle/server/dist/pid-marker.js +141 -0
- package/bundle/server/dist/pid-marker.test.js +208 -0
- package/bundle/server/dist/queue/approve-deliver.js +128 -28
- package/bundle/server/dist/queue/reflect-schedule.js +25 -0
- package/bundle/server/dist/queue/result-qa.js +1 -1
- package/bundle/server/dist/queue/send-gate.test.js +172 -36
- package/bundle/server/dist/repository/agents.js +55 -18
- package/bundle/server/dist/repository/artifacts-for-issue.js +42 -0
- package/bundle/server/dist/repository/artifacts-for-issue.test.js +48 -0
- package/bundle/server/dist/repository/artifacts.js +47 -0
- package/bundle/server/dist/repository/artifacts.test.js +48 -0
- package/bundle/server/dist/repository/authority-attempts.js +189 -0
- package/bundle/server/dist/repository/findings.js +70 -0
- package/bundle/server/dist/repository/findings.test.js +68 -0
- package/bundle/server/dist/repository/human-actions.js +142 -0
- package/bundle/server/dist/repository/human-actions.test.js +123 -0
- package/bundle/server/dist/repository/intake-settings.js +5 -59
- package/bundle/server/dist/repository/issues.js +305 -0
- package/bundle/server/dist/repository/issues.test.js +69 -0
- package/bundle/server/dist/repository/outbound-drafts.js +28 -0
- package/bundle/server/dist/repository/queue-entries.js +162 -0
- package/bundle/server/dist/repository/review-publications.js +153 -0
- package/bundle/server/dist/repository/review-publications.test.js +78 -0
- package/bundle/server/dist/repository/runs-external-id.test.js +5 -3
- package/bundle/server/dist/repository/runs.js +4 -211
- package/bundle/server/dist/repository/runtime-availability.js +54 -0
- package/bundle/server/dist/repository/usage-events.js +60 -0
- package/bundle/server/dist/repository/usage-events.test.js +56 -0
- package/bundle/server/dist/repository/work-items.js +305 -0
- package/bundle/server/dist/repository/work-items.test.js +142 -0
- package/bundle/server/dist/repository/worker-sessions.js +219 -0
- package/bundle/server/dist/repository/worker-sessions.test.js +60 -0
- package/bundle/server/dist/repository/workflow-events.js +193 -0
- package/bundle/server/dist/repository/workflow-events.test.js +89 -0
- package/bundle/server/dist/resolve-tsx-bin.js +19 -0
- package/bundle/server/dist/routes/human-actions.js +68 -0
- package/bundle/server/dist/routes/human-actions.test.js +284 -0
- package/bundle/server/dist/routes/index.js +29 -496
- package/bundle/server/dist/routes/issues.js +240 -0
- package/bundle/server/dist/routes/issues.test.js +451 -0
- package/bundle/server/dist/routes/queue.js +41 -0
- package/bundle/server/dist/routes/queue.test.js +78 -0
- package/bundle/server/dist/runners/claude-args.js +18 -30
- package/bundle/server/dist/runners/claude.js +19 -163
- package/bundle/server/dist/runners/claude.test.js +15 -70
- package/bundle/server/dist/runners/codex-args.js +59 -0
- package/bundle/server/dist/runners/codex-args.test.js +72 -0
- package/bundle/server/dist/runners/codex-jsonl.js +150 -0
- package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
- package/bundle/server/dist/runners/codex-resume.test.js +19 -0
- package/bundle/server/dist/runners/codex.js +46 -0
- package/bundle/server/dist/runners/models-codex.test.js +14 -0
- package/bundle/server/dist/runners/models.js +53 -1
- package/bundle/server/dist/runners/persist.js +4 -1
- package/bundle/server/dist/runners/process-registry.js +11 -0
- package/bundle/server/dist/runners/prompts.js +13 -280
- package/bundle/server/dist/runners/prompts.test.js +22 -153
- package/bundle/server/dist/runners/qa.js +17 -1
- package/bundle/server/dist/runners/reflect.js +4 -16
- package/bundle/server/dist/runners/spawn-cli.js +197 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +130 -0
- package/bundle/server/dist/runners/stream-json.js +0 -141
- package/bundle/server/dist/runners/usage-cap.js +242 -0
- package/bundle/server/dist/runners/usage-cap.test.js +97 -0
- package/bundle/server/dist/server-liveness.js +37 -0
- package/bundle/server/dist/server-liveness.test.js +82 -0
- package/bundle/server/dist/static-ui.test.js +44 -0
- package/bundle/server/package.json +4 -2
- package/bundle/server/static-ui/assets/index-BTtPGYpY.js +60 -0
- package/bundle/server/static-ui/assets/index-BxTx4b1d.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/agents.d.ts +262 -133
- package/bundle/shared/dist/agents.js +85 -19
- package/bundle/shared/dist/cursor-auth-health.d.ts +12 -0
- package/bundle/shared/dist/cursor-auth-health.js +36 -0
- package/bundle/shared/dist/cursor-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/cursor-auth-health.test.js +40 -0
- package/bundle/shared/dist/findings.d.ts +46 -0
- package/bundle/shared/dist/findings.js +17 -0
- package/bundle/shared/dist/findings.test.d.ts +1 -0
- package/bundle/shared/dist/findings.test.js +20 -0
- package/bundle/shared/dist/github-repo.d.ts +28 -0
- package/bundle/shared/dist/github-repo.js +98 -0
- package/bundle/shared/dist/github-repo.test.d.ts +1 -0
- package/bundle/shared/dist/github-repo.test.js +32 -0
- package/bundle/shared/dist/human-actions.d.ts +66 -0
- package/bundle/shared/dist/human-actions.js +51 -0
- package/bundle/shared/dist/human-actions.test.d.ts +1 -0
- package/bundle/shared/dist/human-actions.test.js +57 -0
- package/bundle/shared/dist/index.d.ts +567 -757
- package/bundle/shared/dist/index.js +22 -36
- package/bundle/shared/dist/issues.d.ts +385 -0
- package/bundle/shared/dist/issues.js +169 -0
- package/bundle/shared/dist/issues.test.d.ts +1 -0
- package/bundle/shared/dist/issues.test.js +72 -0
- package/bundle/shared/dist/outbound-draft.d.ts +6 -0
- package/bundle/shared/dist/outbound-draft.js +4 -0
- package/bundle/shared/dist/plan-triage.d.ts +6 -6
- package/bundle/shared/dist/profile-snapshot.d.ts +154 -0
- package/bundle/shared/dist/profile-snapshot.js +145 -0
- package/bundle/shared/dist/profile-snapshot.test.d.ts +1 -0
- package/bundle/shared/dist/profile-snapshot.test.js +67 -0
- package/bundle/shared/dist/queue-entries.d.ts +89 -0
- package/bundle/shared/dist/queue-entries.js +19 -0
- package/bundle/shared/dist/result-qa.d.ts +2 -2
- package/bundle/shared/dist/runtime-auth-health.d.ts +66 -0
- package/bundle/shared/dist/runtime-auth-health.js +186 -0
- package/bundle/shared/dist/runtime-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/runtime-auth-health.test.js +200 -0
- package/bundle/shared/dist/runtime-availability.d.ts +51 -0
- package/bundle/shared/dist/runtime-availability.js +13 -0
- package/bundle/shared/dist/runtime.d.ts +1 -1
- package/bundle/shared/dist/runtime.js +1 -1
- package/bundle/shared/dist/usage-events.d.ts +36 -0
- package/bundle/shared/dist/usage-events.js +13 -0
- package/bundle/shared/dist/worker-sessions.d.ts +140 -0
- package/bundle/shared/dist/worker-sessions.js +70 -0
- package/bundle/shared/dist/worker-sessions.test.d.ts +1 -0
- package/bundle/shared/dist/worker-sessions.test.js +39 -0
- package/bundle/shared/dist/workflow.d.ts +78 -0
- package/bundle/shared/dist/workflow.js +59 -0
- package/bundle/shared/dist/workflow.test.d.ts +1 -0
- package/bundle/shared/dist/workflow.test.js +61 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +50 -0
- package/dist/action.test.d.ts +1 -0
- package/dist/action.test.js +70 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +17 -0
- package/dist/agent.test.d.ts +1 -0
- package/dist/agent.test.js +19 -0
- package/dist/cli-check.d.ts +7 -0
- package/dist/cli-check.js +40 -0
- package/dist/doctor.js +59 -1
- package/dist/http.d.ts +5 -0
- package/dist/http.js +19 -0
- package/dist/index.js +28 -0
- package/dist/issue.d.ts +35 -0
- package/dist/issue.js +143 -0
- package/dist/issue.test.d.ts +1 -0
- package/dist/issue.test.js +240 -0
- package/dist/lifecycle.contract.test.d.ts +1 -0
- package/dist/lifecycle.contract.test.js +135 -0
- package/dist/queue.d.ts +11 -0
- package/dist/queue.js +60 -0
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +46 -0
- package/dist/test-fetch-stub.d.ts +6 -0
- package/dist/test-fetch-stub.js +31 -0
- package/package.json +2 -1
- package/templates/prod.env.example +1 -1
- package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
- package/bundle/server/static-ui/assets/index-QYv3zmE6.css +0 -1
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// packages/server/src/adapters/agent-deck-bind.ts
|
|
2
|
+
//
|
|
3
|
+
// Server-side Agent Deck preflight for a worker session (NOT-106). Before a
|
|
4
|
+
// developer/reviewer session spawns, the coordinator materializes a per-attempt,
|
|
5
|
+
// runtime-specific MCP config that sends `x-agent-deck-deck-id` (+ workspace) with
|
|
6
|
+
// **no** Authorization — Agent Deck's launch-fixed deck session (NOT-105). Then it
|
|
7
|
+
// verifies with a live `get_bound_deck` before spawn.
|
|
8
|
+
//
|
|
9
|
+
// Worker prompts still instruct `bind_workspace` first; under a launch-fixed deck that
|
|
10
|
+
// bind confirms the equipped deck (and rejects a different one with DECK_FIXED).
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
16
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
17
|
+
import { StreamableHTTPClientTransport, StreamableHTTPError, } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
18
|
+
import { getAgentDeckMcpUrl } from "./agent-deck.js";
|
|
19
|
+
import { getWorkerMcpConfigDir } from "../paths.js";
|
|
20
|
+
import { resolveAmbientCodexHome } from "../cli-env.js";
|
|
21
|
+
import { codexMcpServersTable } from "./codex-scoped-config.js";
|
|
22
|
+
const CODEX_HOME_ENV_VAR = "CODEX_HOME";
|
|
23
|
+
/**
|
|
24
|
+
* NOT-136: a preflight call got no answer at all. Raised **only** where a call is awaited
|
|
25
|
+
* (`withinPreflightDeadline`), never by the assertions that judge an answer we did receive —
|
|
26
|
+
* so a deck that is up and reports its own upstream as down (`isError: true` with the text
|
|
27
|
+
* `fetch failed`, the NOT-101 shape) still fails the attempt instead of looking like a dead
|
|
28
|
+
* port just because its prose reads like one.
|
|
29
|
+
*/
|
|
30
|
+
class DeckUnreachableError extends Error {
|
|
31
|
+
constructor(cause) {
|
|
32
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** The shared preflight budget ran out — no answer, as opposed to an answer we rejected. */
|
|
36
|
+
class PreflightTimeoutError extends DeckUnreachableError {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(new Error(message));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* NOT-136: the shape of a transport-level failure — *nothing was listening / nothing
|
|
43
|
+
* answered*. A shape test alone cannot separate "the deck is down" from "the deck told us
|
|
44
|
+
* something that reads like it", so this is only ever applied to an error thrown out of an
|
|
45
|
+
* awaited call, never to one raised about a received result.
|
|
46
|
+
*/
|
|
47
|
+
const UNREACHABLE_ERROR_CODES = new Set([
|
|
48
|
+
"ECONNREFUSED",
|
|
49
|
+
"ECONNRESET",
|
|
50
|
+
"EHOSTUNREACH",
|
|
51
|
+
"ENETUNREACH",
|
|
52
|
+
"ENOTFOUND",
|
|
53
|
+
"EAI_AGAIN",
|
|
54
|
+
"EPIPE",
|
|
55
|
+
"ETIMEDOUT",
|
|
56
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
57
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
58
|
+
"UND_ERR_SOCKET",
|
|
59
|
+
"ERR_SOCKET_CONNECTION_TIMEOUT",
|
|
60
|
+
]);
|
|
61
|
+
/** `fetch failed` is what undici surfaces to us when the port is dead — the literal string
|
|
62
|
+
* observed in the NOT-136 incident. The rest are the same conditions reported as prose by
|
|
63
|
+
* intermediate layers that dropped the `cause` chain. Prose is the *last* resort, and only
|
|
64
|
+
* after `deckAnswered` has ruled out a response — see below. */
|
|
65
|
+
const UNREACHABLE_MESSAGE_RE = /\bfetch failed\b|\bsocket hang up\b|\bgetaddrinfo\b|\bECONNREFUSED\b|\bECONNRESET\b|\bENOTFOUND\b|\bEAI_AGAIN\b|\bEHOSTUNREACH\b|\bENETUNREACH\b/i;
|
|
66
|
+
/** An HTTP status code — the range a real response can carry. */
|
|
67
|
+
function isHttpStatus(value) {
|
|
68
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Structural proof that the deck **answered**, which vetoes every heuristic below.
|
|
72
|
+
*
|
|
73
|
+
* The MCP SDK reports a non-2xx response by throwing `StreamableHTTPError(status, "Error
|
|
74
|
+
* POSTing to endpoint: <body>")` — the response body is pasted into the message. So a deck
|
|
75
|
+
* that is up and returns HTTP 500 with the body `fetch failed` (the NOT-101 shape, where the
|
|
76
|
+
* deck's own upstream is down) produces an error that reads exactly like a dead port. Message
|
|
77
|
+
* matching cannot tell those apart; an HTTP status can, and it is dispositive: bytes came
|
|
78
|
+
* back, so this is a real deck error and must still fail the attempt (NOT-136).
|
|
79
|
+
*/
|
|
80
|
+
function deckAnswered(err, depth = 0) {
|
|
81
|
+
if (!err || typeof err !== "object" || depth > 5)
|
|
82
|
+
return false;
|
|
83
|
+
if (err instanceof StreamableHTTPError)
|
|
84
|
+
return true;
|
|
85
|
+
const e = err;
|
|
86
|
+
// Belt and braces for a duplicated SDK copy, where `instanceof` silently fails:
|
|
87
|
+
// StreamableHTTPError carries the response status as a *numeric* `code` (node's transport
|
|
88
|
+
// errors all use string codes like ECONNREFUSED), and other clients use status/statusCode.
|
|
89
|
+
if (isHttpStatus(e.code) || isHttpStatus(e.status) || isHttpStatus(e.statusCode))
|
|
90
|
+
return true;
|
|
91
|
+
if (Array.isArray(e.errors) && e.errors.some((nested) => deckAnswered(nested, depth + 1)))
|
|
92
|
+
return true;
|
|
93
|
+
return e.cause !== undefined && deckAnswered(e.cause, depth + 1);
|
|
94
|
+
}
|
|
95
|
+
/** Walks `cause` / `AggregateError.errors` — node's fetch buries the real code one or two levels down. */
|
|
96
|
+
function hasUnreachableSignal(err, depth = 0) {
|
|
97
|
+
if (!err || typeof err !== "object" || depth > 5)
|
|
98
|
+
return false;
|
|
99
|
+
const e = err;
|
|
100
|
+
if (typeof e.code === "string" && UNREACHABLE_ERROR_CODES.has(e.code))
|
|
101
|
+
return true;
|
|
102
|
+
if (typeof e.message === "string" && UNREACHABLE_MESSAGE_RE.test(e.message))
|
|
103
|
+
return true;
|
|
104
|
+
if (Array.isArray(e.errors) && e.errors.some((nested) => hasUnreachableSignal(nested, depth + 1)))
|
|
105
|
+
return true;
|
|
106
|
+
return e.cause !== undefined && hasUnreachableSignal(e.cause, depth + 1);
|
|
107
|
+
}
|
|
108
|
+
/** True only when nothing answered: a transport signal *and* no sign of an HTTP response. */
|
|
109
|
+
export function isDeckUnreachableError(err) {
|
|
110
|
+
if (deckAnswered(err))
|
|
111
|
+
return false;
|
|
112
|
+
return hasUnreachableSignal(err);
|
|
113
|
+
}
|
|
114
|
+
function resultText(result) {
|
|
115
|
+
const content = result?.content;
|
|
116
|
+
return Array.isArray(content)
|
|
117
|
+
? content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n").trim()
|
|
118
|
+
: "";
|
|
119
|
+
}
|
|
120
|
+
/** MCP surfaces tool failures as `{ isError: true }` results, not thrown errors. */
|
|
121
|
+
export function assertToolResultOk(result, name) {
|
|
122
|
+
if (result?.isError) {
|
|
123
|
+
throw new Error(`${name} returned an error: ${resultText(result) || "(no detail)"}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Pull the text payload out of a (non-error) MCP tool result and JSON-parse it. */
|
|
127
|
+
export function parseDeckToolResult(result) {
|
|
128
|
+
const text = resultText(result);
|
|
129
|
+
if (!text)
|
|
130
|
+
throw new Error("empty tool result");
|
|
131
|
+
return JSON.parse(text);
|
|
132
|
+
}
|
|
133
|
+
const CODEX_AUTH_POLICY_KEYS = [
|
|
134
|
+
"cli_auth_credentials_store",
|
|
135
|
+
"chatgpt_base_url",
|
|
136
|
+
"forced_login_method",
|
|
137
|
+
"forced_chatgpt_workspace_id",
|
|
138
|
+
];
|
|
139
|
+
function readAmbientCodexAuthPolicy(ambientCodexHome) {
|
|
140
|
+
try {
|
|
141
|
+
const raw = fs.readFileSync(path.join(ambientCodexHome, "config.toml"), "utf8");
|
|
142
|
+
const root = parseToml(raw);
|
|
143
|
+
const policy = {};
|
|
144
|
+
for (const key of CODEX_AUTH_POLICY_KEYS) {
|
|
145
|
+
if (key in root && (typeof root[key] === "string" || typeof root[key] === "number" || typeof root[key] === "boolean")) {
|
|
146
|
+
policy[key] = root[key];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return policy;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function deckLaunchHeaders(deckId, worktreePath) {
|
|
156
|
+
return {
|
|
157
|
+
"x-agent-deck-deck-id": deckId,
|
|
158
|
+
"x-agent-deck-workspace": worktreePath,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** claude's `--mcp-config` file schema — deck headers only, no Authorization. */
|
|
162
|
+
function urlHeaderMcpConfig(mcpUrl, deckId, worktreePath) {
|
|
163
|
+
return {
|
|
164
|
+
mcpServers: {
|
|
165
|
+
"agent-deck": {
|
|
166
|
+
type: "http",
|
|
167
|
+
url: mcpUrl,
|
|
168
|
+
headers: deckLaunchHeaders(deckId, worktreePath),
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function gitCaptured(worktreePath, args) {
|
|
174
|
+
try {
|
|
175
|
+
const stdout = execFileSync("git", ["-C", worktreePath, ...args], {
|
|
176
|
+
encoding: "utf8",
|
|
177
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
178
|
+
});
|
|
179
|
+
return { ok: true, stdout: stdout ?? "", stderr: "" };
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
const e = err;
|
|
183
|
+
return { ok: false, stdout: e.stdout ?? "", stderr: e.stderr ?? String(err) };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function isCursorMcpTracked(worktreePath) {
|
|
187
|
+
const result = gitCaptured(worktreePath, ["ls-files", "--error-unmatch", ".cursor/mcp.json"]);
|
|
188
|
+
return result.ok;
|
|
189
|
+
}
|
|
190
|
+
/** Append `/.cursor/mcp.json` idempotently to git's info/exclude.
|
|
191
|
+
*
|
|
192
|
+
* Note: for a linked worktree, `git rev-parse --git-path info/exclude` resolves to the
|
|
193
|
+
* *main* repository's `.git/info/exclude` (shared across all worktrees of that repo), not
|
|
194
|
+
* a per-worktree file. The `/.cursor/mcp.json` pattern is still correct per worktree root;
|
|
195
|
+
* we rewrite with a single occurrence so concurrent preparations can't leave duplicate lines. */
|
|
196
|
+
function ensureCursorMcpExcluded(worktreePath) {
|
|
197
|
+
const excludePathResult = gitCaptured(worktreePath, ["rev-parse", "--git-path", "info/exclude"]);
|
|
198
|
+
if (!excludePathResult.ok) {
|
|
199
|
+
throw new Error(`could not resolve git info/exclude: ${excludePathResult.stderr || "unknown error"}`);
|
|
200
|
+
}
|
|
201
|
+
const excludePath = path.resolve(worktreePath, excludePathResult.stdout.trim());
|
|
202
|
+
fs.mkdirSync(path.dirname(excludePath), { recursive: true });
|
|
203
|
+
const line = "/.cursor/mcp.json";
|
|
204
|
+
let existing = "";
|
|
205
|
+
try {
|
|
206
|
+
existing = fs.readFileSync(excludePath, "utf8");
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
existing = "";
|
|
210
|
+
}
|
|
211
|
+
const kept = existing
|
|
212
|
+
.split(/\r?\n/)
|
|
213
|
+
.filter((l) => l.trim() !== "" && l.trim() !== line);
|
|
214
|
+
kept.push(line);
|
|
215
|
+
fs.writeFileSync(excludePath, `${kept.join("\n")}\n`, { mode: 0o644 });
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Writes this attempt's deck-launch MCP config as the spawned worker's route to Agent Deck
|
|
219
|
+
* (NOT-106) — no mint, no Authorization.
|
|
220
|
+
*
|
|
221
|
+
* - claude: JSON file outside the worktree (`--mcp-config --strict-mcp-config`).
|
|
222
|
+
* - codex: per-attempt `CODEX_HOME` outside the worktree with `http_headers` in config.toml.
|
|
223
|
+
* - cursor: `<worktree>/.cursor/mcp.json` + info/exclude so the worktree stays porcelain-clean.
|
|
224
|
+
*/
|
|
225
|
+
async function materializeWorkerMcpConfig(opts) {
|
|
226
|
+
const mcpBase = getAgentDeckMcpUrl().replace(/\/mcp\/?$/, "");
|
|
227
|
+
const mcpUrl = `${mcpBase}/mcp`;
|
|
228
|
+
const headers = deckLaunchHeaders(opts.deckId, opts.worktreePath);
|
|
229
|
+
if (opts.runtime === "codex_local") {
|
|
230
|
+
const codexHome = path.join(getWorkerMcpConfigDir(), `codex-home-${opts.deckId.slice(0, 8)}-${randomUUID()}`);
|
|
231
|
+
fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
|
|
232
|
+
try {
|
|
233
|
+
const ambientHome = resolveAmbientCodexHome();
|
|
234
|
+
try {
|
|
235
|
+
const ambientAuthPath = path.join(ambientHome, "auth.json");
|
|
236
|
+
if (fs.existsSync(ambientAuthPath)) {
|
|
237
|
+
fs.symlinkSync(ambientAuthPath, path.join(codexHome, "auth.json"));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// best-effort — OS-keychain-backed hosts have no auth.json
|
|
242
|
+
}
|
|
243
|
+
const toml = stringifyToml({
|
|
244
|
+
...readAmbientCodexAuthPolicy(ambientHome),
|
|
245
|
+
mcp_servers: codexMcpServersTable({
|
|
246
|
+
mcpUrl,
|
|
247
|
+
headers,
|
|
248
|
+
allowOutboundMutation: opts.policy.outboundMutation,
|
|
249
|
+
}),
|
|
250
|
+
});
|
|
251
|
+
fs.writeFileSync(path.join(codexHome, "config.toml"), toml, { mode: 0o600 });
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
fs.rmSync(codexHome, { recursive: true, force: true });
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
mcpConfigPath: codexHome,
|
|
259
|
+
mcpEnv: { [CODEX_HOME_ENV_VAR]: codexHome },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (opts.runtime === "cursor_local") {
|
|
263
|
+
if (isCursorMcpTracked(opts.worktreePath)) {
|
|
264
|
+
throw new Error(".cursor/mcp.json is tracked in this repository — refusing to overwrite a tracked file for the worker MCP config");
|
|
265
|
+
}
|
|
266
|
+
ensureCursorMcpExcluded(opts.worktreePath);
|
|
267
|
+
const cursorDir = path.join(opts.worktreePath, ".cursor");
|
|
268
|
+
fs.mkdirSync(cursorDir, { recursive: true });
|
|
269
|
+
const filePath = path.join(cursorDir, "mcp.json");
|
|
270
|
+
fs.writeFileSync(filePath, JSON.stringify({ mcpServers: { "agent-deck": { url: mcpUrl, headers } } }, null, 2) + "\n", { mode: 0o600 });
|
|
271
|
+
return { mcpConfigPath: filePath };
|
|
272
|
+
}
|
|
273
|
+
// claude_code
|
|
274
|
+
const dir = getWorkerMcpConfigDir();
|
|
275
|
+
const filePath = path.join(dir, `claude-mcp-${opts.deckId.slice(0, 8)}-${randomUUID()}.json`);
|
|
276
|
+
try {
|
|
277
|
+
fs.writeFileSync(filePath, JSON.stringify(urlHeaderMcpConfig(mcpUrl, opts.deckId, opts.worktreePath)), { mode: 0o600 });
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
fs.rmSync(filePath, { force: true });
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
return { mcpConfigPath: filePath };
|
|
284
|
+
}
|
|
285
|
+
/** `get_bound_deck`'s deck-identity field. */
|
|
286
|
+
function assertBoundDeckMatches(result, expectedDeckId) {
|
|
287
|
+
const payload = parseDeckToolResult(result);
|
|
288
|
+
const boundDeckId = typeof payload.id === "string" ? payload.id : undefined;
|
|
289
|
+
if (boundDeckId !== expectedDeckId) {
|
|
290
|
+
throw new Error(`get_bound_deck returned deck ${boundDeckId ?? "(missing id)"}, expected ${expectedDeckId}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function assertPlaybookMatches(result, expectedPlaybookId) {
|
|
294
|
+
const payload = parseDeckToolResult(result);
|
|
295
|
+
const playbookId = typeof payload.id === "string" ? payload.id : undefined;
|
|
296
|
+
if (playbookId !== expectedPlaybookId) {
|
|
297
|
+
throw new Error(`get_playbook returned ${playbookId ?? "(missing id)"}, expected ${expectedPlaybookId}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Single classification point for every preflight error. The split is decided by *where* the
|
|
302
|
+
* error came from, not by how it reads: only the call boundary raises `DeckUnreachableError`.
|
|
303
|
+
*/
|
|
304
|
+
function verifyFailed(err) {
|
|
305
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
306
|
+
return err instanceof DeckUnreachableError
|
|
307
|
+
? { ok: false, kind: "deck_unavailable", reason }
|
|
308
|
+
: { ok: false, kind: "infra_failure", reason };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Run one preflight operation inside the shared absolute deadline and clear its timer.
|
|
312
|
+
* This is the one place a preflight awaits the deck, so it is also the one place a
|
|
313
|
+
* transport failure is recognized as the deck being unreachable (NOT-136).
|
|
314
|
+
*/
|
|
315
|
+
async function withinPreflightDeadline(opts) {
|
|
316
|
+
const remainingMs = opts.deadlineMs - Date.now();
|
|
317
|
+
if (remainingMs <= 0) {
|
|
318
|
+
throw new PreflightTimeoutError(`${opts.label} timed out after ${opts.timeoutMs}ms total preflight budget`);
|
|
319
|
+
}
|
|
320
|
+
let timer;
|
|
321
|
+
const timeout = new Promise((_, reject) => {
|
|
322
|
+
timer = setTimeout(() => reject(new PreflightTimeoutError(`${opts.label} timed out after ${opts.timeoutMs}ms total preflight budget`)), remainingMs);
|
|
323
|
+
});
|
|
324
|
+
try {
|
|
325
|
+
return await Promise.race([Promise.resolve().then(opts.run), timeout]);
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
if (err instanceof DeckUnreachableError)
|
|
329
|
+
throw err;
|
|
330
|
+
throw isDeckUnreachableError(err) ? new DeckUnreachableError(err) : err;
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
if (timer !== undefined)
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async function verifyDeckConnection(opts) {
|
|
338
|
+
const deadlineMs = Date.now() + opts.timeoutMs;
|
|
339
|
+
const callBeforeDeadline = (label, run) => withinPreflightDeadline({ label, deadlineMs, timeoutMs: opts.timeoutMs, run });
|
|
340
|
+
if (opts.callTool) {
|
|
341
|
+
let result;
|
|
342
|
+
try {
|
|
343
|
+
result = await callBeforeDeadline("get_bound_deck", () => opts.callTool("get_bound_deck", {}));
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
return verifyFailed(err);
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
assertToolResultOk(result, "get_bound_deck");
|
|
350
|
+
assertBoundDeckMatches(result, opts.deckId);
|
|
351
|
+
for (const playbookId of new Set(opts.playbookIds)) {
|
|
352
|
+
const playbook = await callBeforeDeadline(`get_playbook(${playbookId})`, () => opts.callTool("get_playbook", { playbook_id: playbookId }));
|
|
353
|
+
assertToolResultOk(playbook, `get_playbook(${playbookId})`);
|
|
354
|
+
assertPlaybookMatches(playbook, playbookId);
|
|
355
|
+
}
|
|
356
|
+
return { ok: true };
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
return verifyFailed(err);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const mcpBase = getAgentDeckMcpUrl().replace(/\/mcp\/?$/, "");
|
|
363
|
+
const transport = new StreamableHTTPClientTransport(new URL(`${mcpBase}/mcp`), {
|
|
364
|
+
requestInit: { headers: deckLaunchHeaders(opts.deckId, opts.worktreePath) },
|
|
365
|
+
});
|
|
366
|
+
const client = new Client({ name: "agent-dealer-deck-preflight", version: "0.0.1" });
|
|
367
|
+
try {
|
|
368
|
+
await callBeforeDeadline("Agent Deck connection", () => client.connect(transport));
|
|
369
|
+
const result = await callBeforeDeadline("get_bound_deck", () => client.callTool({ name: "get_bound_deck", arguments: {} }));
|
|
370
|
+
assertToolResultOk(result, "get_bound_deck");
|
|
371
|
+
assertBoundDeckMatches(result, opts.deckId);
|
|
372
|
+
for (const playbookId of new Set(opts.playbookIds)) {
|
|
373
|
+
const playbook = await callBeforeDeadline(`get_playbook(${playbookId})`, () => client.callTool({ name: "get_playbook", arguments: { playbook_id: playbookId } }));
|
|
374
|
+
assertToolResultOk(playbook, `get_playbook(${playbookId})`);
|
|
375
|
+
assertPlaybookMatches(playbook, playbookId);
|
|
376
|
+
}
|
|
377
|
+
return { ok: true };
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
return verifyFailed(err);
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
try {
|
|
384
|
+
await client.close();
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// best-effort after a connection or deadline failure
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Materialize a per-attempt MCP config for the profile's deck and verify with
|
|
393
|
+
* `get_bound_deck` and every configured playbook before spawn. No mint, no ledger, no
|
|
394
|
+
* Authorization. A deck-bound worker is fail-closed: missing playbook authority is an
|
|
395
|
+
* infrastructure failure, never permission to improvise without the configured recipe.
|
|
396
|
+
*
|
|
397
|
+
* NOT-136: "the deck said no" and "the deck is not running" are different outcomes. Only the
|
|
398
|
+
* former is `infra_failure` (a spent attempt); an unreachable deck returns `deck_unavailable`
|
|
399
|
+
* so the caller can wait for it to come back.
|
|
400
|
+
*/
|
|
401
|
+
export async function prepareWorkerDeckConnection(opts) {
|
|
402
|
+
const timeoutMs = opts.timeoutMs ?? Number(process.env.DECK_BIND_TIMEOUT_MS ?? 30_000);
|
|
403
|
+
let materialized;
|
|
404
|
+
try {
|
|
405
|
+
materialized = await materializeWorkerMcpConfig({
|
|
406
|
+
runtime: opts.runtime,
|
|
407
|
+
deckId: opts.deckId,
|
|
408
|
+
worktreePath: opts.worktreePath,
|
|
409
|
+
policy: opts.policy,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
return { ok: false, kind: "infra_failure", reason: `MCP materialization failed: ${err.message}` };
|
|
414
|
+
}
|
|
415
|
+
const verified = await verifyDeckConnection({
|
|
416
|
+
deckId: opts.deckId,
|
|
417
|
+
worktreePath: opts.worktreePath,
|
|
418
|
+
playbookIds: opts.playbookIds ?? [],
|
|
419
|
+
callTool: opts.verifyCallTool,
|
|
420
|
+
timeoutMs,
|
|
421
|
+
});
|
|
422
|
+
if (!verified.ok) {
|
|
423
|
+
try {
|
|
424
|
+
fs.rmSync(materialized.mcpConfigPath, { recursive: true, force: true });
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// best-effort
|
|
428
|
+
}
|
|
429
|
+
return verified.kind === "deck_unavailable"
|
|
430
|
+
? { ok: false, kind: "deck_unavailable", reason: `Agent Deck is unreachable — ${verified.reason}` }
|
|
431
|
+
: { ok: false, kind: "infra_failure", reason: `preflight failed: ${verified.reason}` };
|
|
432
|
+
}
|
|
433
|
+
return { ok: true, mcpConfigPath: materialized.mcpConfigPath, mcpEnv: materialized.mcpEnv };
|
|
434
|
+
}
|
|
435
|
+
/** Best-effort cleanup of the on-disk MCP config after a spawn ends. */
|
|
436
|
+
export async function releaseWorkerDeckConnection(opts) {
|
|
437
|
+
try {
|
|
438
|
+
fs.rmSync(opts.mcpConfigPath, { recursive: true, force: true });
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// best-effort
|
|
442
|
+
}
|
|
443
|
+
}
|