@humwick/runner 0.130.4 → 0.130.6
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/LICENSE +661 -167
- package/agent-bin/humwick +2 -2
- package/agent-bin/humwick-browser +4 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
- package/package.json +8 -11
- package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
- package/plugins/claude/.claude-plugin/plugin.json +3 -3
- package/plugins/claude/claude-statusline-probe.cjs +26 -16
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
- package/plugins/claude/hooks/gate-run-guard.sh +3 -3
- package/plugins/claude/hooks/gate-run-guard.ts +5 -5
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +1 -1
- package/plugins/claude/hooks/kill-guard.sh +4 -4
- package/plugins/claude/hooks/kill-guard.ts +2 -2
- package/plugins/claude/hooks/permission-request.sh +1 -1
- package/plugins/claude/hooks/post-compact.sh +1 -1
- package/plugins/claude/hooks/post-tool-use.sh +3 -3
- package/plugins/claude/hooks/pre-compact.sh +2 -2
- package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
- package/plugins/claude/hooks/relay-status.sh +32 -32
- package/plugins/claude/hooks/session-end.sh +2 -2
- package/plugins/claude/hooks/session-start.sh +3 -3
- package/plugins/claude/hooks/shell-trust.ts +1 -1
- package/plugins/claude/hooks/stop-failure.sh +2 -2
- package/plugins/claude/hooks/stop.sh +6 -6
- package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
- package/plugins/claude/hooks/subagent-stop.sh +3 -3
- package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
- package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
- package/plugins/claude/monitors/monitors.json +2 -2
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
- package/plugins/claude/monitors/relay-monitor.ts +5 -5
- package/plugins/claude/skills/label/SKILL.md +3 -3
- package/plugins/claude/skills/message/SKILL.md +5 -5
- package/plugins/claude/skills/react/SKILL.md +1 -1
- package/plugins/claude/skills/read-message/SKILL.md +4 -4
- package/plugins/claude/skills/reply/SKILL.md +5 -5
- package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
- package/plugins/claude/skills/slice-first/SKILL.md +5 -3
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/claude/skills/status/SKILL.md +3 -3
- package/plugins/claude/skills/tags/SKILL.md +5 -5
- package/plugins/claude/skills/task/SKILL.md +10 -10
- package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
- package/plugins/codex/skills/guide/SKILL.md +3 -3
- package/plugins/codex/skills/label/SKILL.md +3 -3
- package/plugins/codex/skills/message/SKILL.md +5 -5
- package/plugins/codex/skills/react/SKILL.md +1 -1
- package/plugins/codex/skills/read-message/SKILL.md +4 -4
- package/plugins/codex/skills/reply/SKILL.md +5 -5
- package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
- package/plugins/codex/skills/slice-first/SKILL.md +5 -3
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/codex/skills/status/SKILL.md +3 -3
- package/plugins/codex/skills/tags/SKILL.md +5 -5
- package/plugins/codex/skills/task/SKILL.md +11 -10
- package/src/adapter.ts +95 -92
- package/src/adapters/claude-api.ts +8 -11
- package/src/adapters/claude-context-command.ts +1 -1
- package/src/adapters/claude-credentials.ts +7 -3
- package/src/adapters/claude-delivery-lanes.ts +2 -2
- package/src/adapters/claude-delivery.ts +16 -16
- package/src/adapters/claude-effort.ts +2 -2
- package/src/adapters/claude-exit.ts +2 -2
- package/src/adapters/claude-headless-mirror.ts +18 -2
- package/src/adapters/claude-headless-refusal.ts +3 -3
- package/src/adapters/claude-headless.ts +11 -10
- package/src/adapters/claude-launch-exit.ts +1 -1
- package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
- package/src/adapters/claude-permission.ts +5 -5
- package/src/adapters/claude-prompt-gates.ts +9 -9
- package/src/adapters/claude-prompt-hook.ts +2 -2
- package/src/adapters/claude-session-capture.ts +6 -6
- package/src/adapters/claude-session-probe.ts +25 -25
- package/src/adapters/claude-shutdown.ts +11 -11
- package/src/adapters/claude-status-detectors.ts +31 -31
- package/src/adapters/claude-subagents.ts +7 -7
- package/src/adapters/claude-submit-confirmation.ts +6 -6
- package/src/adapters/claude-tmux.ts +43 -43
- package/src/adapters/claude-token-usage-capture.ts +5 -5
- package/src/adapters/claude-tool-policy.ts +7 -7
- package/src/adapters/claude-transcript.ts +24 -24
- package/src/adapters/claude.ts +100 -100
- package/src/adapters/codex-agent-message-capture.ts +26 -63
- package/src/adapters/codex-app-server.ts +1 -1
- package/src/adapters/codex-client.ts +9 -9
- package/src/adapters/codex-effective-config.ts +1 -1
- package/src/adapters/codex-launch-config.ts +5 -5
- package/src/adapters/codex-rollout-context.ts +2 -2
- package/src/adapters/codex-skill-discovery.ts +1 -1
- package/src/adapters/codex-token-usage-capture.ts +16 -16
- package/src/adapters/codex-tool-output.ts +4 -4
- package/src/adapters/codex-version.ts +3 -3
- package/src/adapters/codex.ts +64 -64
- package/src/adapters/opencode-api.ts +1 -1
- package/src/adapters/opencode-context.ts +20 -22
- package/src/adapters/opencode-model-registry.ts +7 -7
- package/src/adapters/opencode-provider.ts +8 -8
- package/src/adapters/opencode-provisioning.ts +30 -0
- package/src/adapters/opencode-server-retry.ts +1 -1
- package/src/adapters/opencode-soft-steer.ts +5 -5
- package/src/adapters/opencode-spawn.ts +18 -14
- package/src/adapters/opencode-token-usage-capture.ts +12 -12
- package/src/adapters/opencode-tool-events.ts +15 -15
- package/src/adapters/opencode.ts +232 -232
- package/src/adapters/project-shell.ts +11 -11
- package/src/adapters/service-access.ts +6 -6
- package/src/adapters/tmux-offload.ts +9 -9
- package/src/adapters/tool-summary.ts +5 -5
- package/src/agent-redact.ts +1 -1
- package/src/agent-runner/commands/dispatch.ts +10 -10
- package/src/agent-runner/commands/lifecycle.ts +2 -2
- package/src/agent-runner/commands/prompt.ts +19 -19
- package/src/agent-runner/credentials.ts +5 -5
- package/src/agent-runner/inbox.ts +1 -1
- package/src/agent-runner/lifecycle-milestones.ts +3 -3
- package/src/agent-runner/lifecycle-phase.ts +2 -2
- package/src/agent-runner/liveness-exchange.ts +4 -10
- package/src/agent-runner/registration.ts +8 -8
- package/src/agent-runner/runner-context.ts +9 -14
- package/src/agent-runner/session-mirror.ts +12 -12
- package/src/agent-runner/task-claims.ts +2 -2
- package/src/agent-runner/token-usage-coverage.ts +1 -1
- package/src/agent-runner/turn-report.ts +12 -12
- package/src/api-budget-accounting.ts +3 -3
- package/src/area-memory.ts +6 -6
- package/src/artifact-path-materialize.ts +1 -1
- package/src/async-guard.ts +5 -5
- package/src/attachment-cache.ts +17 -22
- package/src/background-script-reconcile.ts +6 -6
- package/src/bounded-log.ts +1 -1
- package/src/busy-reconciler.ts +22 -22
- package/src/claim-tracker.ts +3 -3
- package/src/claude-settings.ts +8 -8
- package/src/claude-system-prompt.ts +1 -1
- package/src/claude-workspace-trust.ts +1 -1
- package/src/config.ts +62 -61
- package/src/context-boundary.ts +5 -5
- package/src/context-command.ts +3 -3
- package/src/control-server.ts +32 -32
- package/src/delivery-result.ts +2 -2
- package/src/effort-command.ts +2 -2
- package/src/empty-turn.ts +22 -14
- package/src/free-port.ts +1 -1
- package/src/gate-run-config.ts +3 -3
- package/src/host-asset-plan.ts +24 -17
- package/src/host-claude-config-worker.ts +2 -2
- package/src/host-sources.ts +2 -2
- package/src/index.ts +16 -16
- package/src/launch-assembly.ts +94 -96
- package/src/liveness.ts +15 -15
- package/src/logger.ts +3 -3
- package/src/mcp-outbox.ts +3 -3
- package/src/mcp-preflight.ts +2 -2
- package/src/mcp-source-selection.ts +2 -2
- package/src/mcp-tool-deferral.ts +1 -1
- package/src/mcp-vault-env.ts +2 -2
- package/src/message-body-config/config.ts +4 -4
- package/src/mid-turn-delivery.ts +2 -2
- package/src/native-memory-lever.ts +8 -8
- package/src/outbox.ts +15 -15
- package/src/pending-permission-store.ts +1 -1
- package/src/profile-home.ts +19 -19
- package/src/profile-projection.ts +26 -24
- package/src/project-instructions.ts +5 -5
- package/src/provider-auth-warning.ts +17 -0
- package/src/provider-double.ts +1 -1
- package/src/provider-hold.ts +3 -3
- package/src/provider-home-marker.ts +1 -1
- package/src/provider-home-tombstone.ts +1 -1
- package/src/provider-input-queue.ts +2 -2
- package/src/provider-integration.ts +15 -13
- package/src/provider-setup.ts +15 -3
- package/src/providers.ts +1 -1
- package/src/provisioning.ts +37 -40
- package/src/question-decision.ts +2 -2
- package/src/quota.ts +0 -6
- package/src/rate-limit.ts +24 -13
- package/src/refused-prompt-store.ts +2 -2
- package/src/relay-injection-events.ts +2 -2
- package/src/relay-instructions.ts +5 -5
- package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
- package/src/relay-mcp-proxy.ts +57 -58
- package/src/relay-mcp.ts +55 -69
- package/src/reply-obligation-cache.ts +9 -9
- package/src/response-capture-report.ts +9 -9
- package/src/response-routing.ts +1 -1
- package/src/runner-core.ts +209 -222
- package/src/runner-exit.ts +1 -1
- package/src/runner-helpers.ts +10 -10
- package/src/runner-insights.ts +4 -2
- package/src/runner-mcp-proxy.ts +3 -3
- package/src/runner-spawn-config.ts +3 -3
- package/src/runtime-token-file.ts +1 -1
- package/src/runtime-token-remint.ts +1 -1
- package/src/service-access.ts +2 -2
- package/src/session-destroy.ts +1 -1
- package/src/session-insights.ts +1 -1
- package/src/session-scratch.ts +24 -37
- package/src/spawn-input.ts +6 -6
- package/src/startup-config-fetch.ts +4 -4
- package/src/subreaper.ts +3 -5
- package/src/test-run-dedup.ts +13 -13
- package/src/transcript-context.ts +2 -2
- package/src/transport-errors.ts +1 -1
- package/src/turn-opener.ts +1 -1
- package/src/turn-reconcile-event.ts +2 -2
- package/src/vault-proxy-hop.ts +2 -2
- package/src/web-tools.ts +2 -2
- package/agent-bin/agent-relay-browser +0 -10
- package/plugins/claude/skills/disconnect/SKILL.md +0 -16
- package/plugins/claude/skills/pair/SKILL.md +0 -30
- package/plugins/codex/skills/disconnect/SKILL.md +0 -15
- package/plugins/codex/skills/pair/SKILL.md +0 -29
- package/src/adapters/provider-quota-harvest.ts +0 -76
package/src/relay-mcp-proxy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { relayHeader,
|
|
1
|
+
import { MCP_SERVER_INFO } from "@humwick/sdk";
|
|
2
|
+
import { relayHeader, errMessage, isRecord, resendOnRotatedToken } from "@humwick/sdk";
|
|
3
3
|
import { forwardSharedMcp, type WebToolLogOptions } from "./web-tool-call-proxy";
|
|
4
4
|
import { materializeArtifactPathUpload } from "./artifact-path-materialize";
|
|
5
5
|
import { logger } from "./logger";
|
|
@@ -10,12 +10,12 @@ import { RELAY_MCP_ONDEMAND_SURFACE_VALUE } from "./relay-mcp";
|
|
|
10
10
|
// member; the real global `fetch` satisfies it.
|
|
11
11
|
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
12
12
|
|
|
13
|
-
//
|
|
13
|
+
// Bun accepts a non-standard `timeout` member on the fetch init (ms, or `false` to disable
|
|
14
14
|
// its native HTTP idle watchdog), but bun-types@1.3.14 don't declare it, so a bare literal fails
|
|
15
|
-
// typecheck. Widen locally (mirrors the
|
|
15
|
+
// typecheck. Widen locally (mirrors the Phase-1 pattern) instead of casting to `any`.
|
|
16
16
|
export type RelayForwardRequestInit = RequestInit & { timeout?: number | false };
|
|
17
17
|
|
|
18
|
-
// Stage 2 of
|
|
18
|
+
// Stage 2 of — the Runner becomes the MCP endpoint the agent connects to,
|
|
19
19
|
// fronting the relay. The agent's MCP client points at this localhost server instead of
|
|
20
20
|
// directly at the relay, so the Runner owns the relay connection, reconnect/backoff, and a
|
|
21
21
|
// durable buffer. A relay restart/crash becomes invisible to the agent.
|
|
@@ -27,7 +27,7 @@ export type RelayForwardRequestInit = RequestInit & { timeout?: number | false }
|
|
|
27
27
|
// the live-tool-set capability the relay's static endpoint can't.
|
|
28
28
|
// 2. `tools/list` — forward (the relay already scope-filters per token), then context-
|
|
29
29
|
// NARROW: hide tools that don't apply to this agent's workspace mode/
|
|
30
|
-
// state (e.g. `
|
|
30
|
+
// state (e.g. `humwick_workspace_*` for a non-worktree agent). Strictly
|
|
31
31
|
// a subset — never widens. Served from a last-known cache during a blip.
|
|
32
32
|
// 3. `tools/call` — forward with the runner's LIVE token; for bufferable writes during a
|
|
33
33
|
// relay outage, enqueue durably + return a synthetic "queued" ok so the
|
|
@@ -41,7 +41,7 @@ export type RelayForwardRequestInit = RequestInit & { timeout?: number | false }
|
|
|
41
41
|
// already returned, so a proxy bug can never become an auth bypass — the relay owns the lock at
|
|
42
42
|
// `tools/call`, the proxy owns the menu.
|
|
43
43
|
//
|
|
44
|
-
//
|
|
44
|
+
// adds a SECOND narrowing dimension for Codex lean workers: the on-demand
|
|
45
45
|
// relay set. The agent's DIRECT relay client has its on-demand tools hidden here so only the eager
|
|
46
46
|
// hot-path set boots; the SAME tools are simultaneously fronted by the per-agent metaOnly callmux
|
|
47
47
|
// (a second relay downstream over THIS proxy), reachable via callmux_call. callmux's requests carry
|
|
@@ -55,7 +55,7 @@ const PROXY_PATH = "/mcp";
|
|
|
55
55
|
// Relay-side failures we treat as "relay down" (buffer/serve-cache), as opposed to a real 4xx
|
|
56
56
|
// rejection that must be surfaced to the agent verbatim.
|
|
57
57
|
const GATEWAY_STATUSES = new Set([502, 503, 504]);
|
|
58
|
-
//
|
|
58
|
+
// gateway sub-classes for the connect-retry safety boundary. A 502/503 means the reverse
|
|
59
59
|
// proxy could not reach the relay at all → the request never executed → safe to retry any tool
|
|
60
60
|
// (connect-phase). A 504 means the proxy DID forward and the relay's response was lost → the outcome
|
|
61
61
|
// is ambiguous → only safe to retry an idempotent call (same rule as a mid-flight socket reset).
|
|
@@ -63,15 +63,15 @@ const CONNECT_PHASE_GATEWAY_STATUSES = new Set([502, 503]);
|
|
|
63
63
|
const AMBIGUOUS_GATEWAY_STATUSES = new Set([504]);
|
|
64
64
|
const SSE_KEEPALIVE_MS = 25_000;
|
|
65
65
|
|
|
66
|
-
//
|
|
66
|
+
// humwick_spawn_agent is IDEMPOTENT by spawnRequestId: the server dedupes a repeat of the
|
|
67
67
|
// same id to the existing spawn instead of creating a second agent (see src/services/spawn-agent.ts).
|
|
68
68
|
// The proxy stabilizes that id up-front (below) so a retry across a restart can't double-create. This
|
|
69
|
-
// name is used only for that id-stabilization; the connect-retry itself
|
|
69
|
+
// name is used only for that id-stabilization; the connect-retry itself is GENERIC across every
|
|
70
70
|
// tool and keys "safe to resend an ambiguous failure" off the presence of an idempotency key, never a
|
|
71
71
|
// tool name.
|
|
72
|
-
const SPAWN_TOOL = "
|
|
72
|
+
const SPAWN_TOOL = "humwick_spawn_agent";
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// bounded connect-retry defaults. A CD deploy restarts the fleet relay on every green
|
|
75
75
|
// origin/main SHA (systemctl restart, no drain/quiesce); tool calls in that ~1-2 min window otherwise
|
|
76
76
|
// fail hard with "-32002 relay unreachable" and require a manual retry. The window (~75s) rides out
|
|
77
77
|
// the restart; backoff climbs 0.5→1→2→4s (capped) with jitter to avoid a thundering-herd reconnect.
|
|
@@ -84,12 +84,12 @@ const DEFAULT_CONNECT_RETRY_MAX_BACKOFF_MS = 4_000;
|
|
|
84
84
|
const DEFAULT_CONNECT_RETRY_WINDOW_MS = 75_000;
|
|
85
85
|
const RELAY_MCP_METADATA_FORWARD_TIMEOUT_MS = 10_000;
|
|
86
86
|
// A tool call can legitimately block in Relay: workspace status alone permits a
|
|
87
|
-
// 600-second wait, and spawn/artifact operations can also take time.
|
|
88
|
-
// separately caps its wait at 10s
|
|
87
|
+
// 600-second wait, and spawn/artifact operations can also take time. humwick_run
|
|
88
|
+
// separately caps its wait at 10s: this proxy budget cannot extend a
|
|
89
89
|
// provider client's shorter timeout. Keep the deadline for the other tools.
|
|
90
90
|
const RELAY_MCP_TOOL_CALL_FORWARD_TIMEOUT_MS = 610_000;
|
|
91
91
|
|
|
92
|
-
//
|
|
92
|
+
// the connect-retry safety boundary rests on distinguishing WHEN in a request's lifecycle the
|
|
93
93
|
// transport failed. A failure is one of three classes:
|
|
94
94
|
//
|
|
95
95
|
// "connect" — the request provably NEVER reached the relay app: connection refused, DNS failure,
|
|
@@ -117,8 +117,8 @@ function classifyGatewayStatus(status: number): TransportErrorClass {
|
|
|
117
117
|
return "fatal";
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
//
|
|
121
|
-
// server dedupes on: spawnRequestId (
|
|
120
|
+
// a tool call is idempotent (safe to resend after an ambiguous failure) iff it carries a key the
|
|
121
|
+
// server dedupes on: spawnRequestId (humwick_spawn_agent) or idempotencyKey (buffered writes, task deliver).
|
|
122
122
|
// Generic — no tool names — so any tool gaining such a key qualifies automatically.
|
|
123
123
|
function isIdempotentCall(args: Record<string, unknown>): boolean {
|
|
124
124
|
const spawnRequestId = typeof args.spawnRequestId === "string" ? args.spawnRequestId.trim() : "";
|
|
@@ -130,37 +130,37 @@ function isIdempotentCall(args: Record<string, unknown>): boolean {
|
|
|
130
130
|
// does not need synchronously — safe to queue durably and replay on reconnect. Reads, claims
|
|
131
131
|
// (409 contention), spawn/shutdown (need a real ack) are deliberately NOT bufferable.
|
|
132
132
|
const DEFAULT_BUFFERABLE_TOOLS = new Set<string>([
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
133
|
+
"humwick_send_message",
|
|
134
|
+
"humwick_reply",
|
|
135
|
+
"humwick_task_deliver",
|
|
136
136
|
]);
|
|
137
137
|
|
|
138
138
|
// Tools surfaced ONLY to an agent that owns a live isolated worktree. For any other agent the
|
|
139
139
|
// proxy narrows them out of `tools/list` even though the token scope permits them (the relay
|
|
140
|
-
// would return them). This is the context the coarse token scope can't express
|
|
140
|
+
// would return them). This is the context the coarse token scope can't express.
|
|
141
141
|
const WORKTREE_ONLY_TOOLS = new Set<string>([
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
142
|
+
"humwick_workspace_status",
|
|
143
|
+
"humwick_workspace_deps",
|
|
144
|
+
"humwick_workspace_list",
|
|
145
|
+
"humwick_workspace_claim",
|
|
146
|
+
"humwick_workspace_release",
|
|
147
147
|
]);
|
|
148
148
|
|
|
149
149
|
interface ProxyContext {
|
|
150
150
|
// The agent owns a live (non-terminal) isolated git worktree → workspace tools apply.
|
|
151
151
|
isolatedWorktree: boolean;
|
|
152
|
-
//
|
|
152
|
+
// this worktree agent's own workspace descriptor (from HUMWICK_WORKSPACE_JSON). The
|
|
153
153
|
// relay writes the workspace ROW lazily via the orchestrator's managed-agents report, so a worker
|
|
154
154
|
// that delivers before that write (or whose registration was lost under concurrent-spawn load)
|
|
155
|
-
// would 404. We inject this descriptor into `
|
|
155
|
+
// would 404. We inject this descriptor into `humwick_task_deliver` so the relay can self-heal the
|
|
156
156
|
// missing registration from the worker's own truth instead of stranding the delivery.
|
|
157
157
|
workspaceDescriptor?: Record<string, unknown>;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
//
|
|
161
|
-
const WORKSPACE_SELF_HEAL_TOOLS = new Set(["
|
|
160
|
+
// tool calls the proxy augments with the agent's workspace descriptor for self-heal.
|
|
161
|
+
const WORKSPACE_SELF_HEAL_TOOLS = new Set(["humwick_task_deliver"]);
|
|
162
162
|
|
|
163
|
-
//
|
|
163
|
+
// the minimal descriptor the relay needs to (re)register a dropped workspace row, distilled
|
|
164
164
|
// from the agent's WorkspaceMetadata. Undefined unless this agent owns a live isolated worktree with
|
|
165
165
|
// the required id/repo/worktree present, so a shared/non-worktree agent never sends one.
|
|
166
166
|
export function isolatedWorkspaceDescriptor(
|
|
@@ -200,7 +200,7 @@ export interface RelayMcpProxyOptions {
|
|
|
200
200
|
enqueueBuffered(call: BufferedToolCall): void;
|
|
201
201
|
initialContext?: ProxyContext;
|
|
202
202
|
bufferableTools?: Set<string>;
|
|
203
|
-
//
|
|
203
|
+
// the Codex-worker on-demand relay set to hide from the agent's DIRECT relay client so
|
|
204
204
|
// only the eager hot-path set boots. Undefined for every agent that isn't a lean Codex worker with
|
|
205
205
|
// callmux simultaneously fronting these tools (Claude, coordinators, reviewers, proxy-off) → no
|
|
206
206
|
// hiding, full surface. callmux's own relay downstream opts out via RELAY_MCP_ONDEMAND_SURFACE_HEADER.
|
|
@@ -209,14 +209,14 @@ export interface RelayMcpProxyOptions {
|
|
|
209
209
|
fetchImpl?: FetchLike;
|
|
210
210
|
// Test seam for inspecting the deadline selected for each relay forward.
|
|
211
211
|
abortSignal?: (timeoutMs: number) => AbortSignal;
|
|
212
|
-
//
|
|
212
|
+
// bounded connect-retry across a relay restart, GENERIC over every tool. Overridable for tests
|
|
213
213
|
// (fast/deterministic backoff + attempt cap); defaults ride out a CD-deploy's brief unreachable window.
|
|
214
214
|
// attempts — hard cap on forward attempts (safety bound; the window is the real bound in prod)
|
|
215
215
|
// backoffMs — base (first-retry) delay; doubles each retry up to maxBackoffMs
|
|
216
216
|
// maxBackoffMs — per-delay ceiling
|
|
217
217
|
// maxWindowMs — total retry budget (sum of backoffs) before the original error is surfaced
|
|
218
218
|
connectRetry?: { attempts?: number; backoffMs?: number; maxBackoffMs?: number; maxWindowMs?: number };
|
|
219
|
-
//
|
|
219
|
+
// deprecated alias for connectRetry (attempts/backoffMs only). `connectRetry` wins if both set.
|
|
220
220
|
spawnRetry?: { attempts?: number; backoffMs?: number };
|
|
221
221
|
// Test seam so the retry backoff doesn't sleep real wall-clock time in unit tests.
|
|
222
222
|
sleep?: (ms: number) => Promise<void>;
|
|
@@ -236,7 +236,7 @@ interface SseClient {
|
|
|
236
236
|
keepalive: ReturnType<typeof setInterval>;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
export class
|
|
239
|
+
export class HumwickMcpProxy {
|
|
240
240
|
private readonly redactToolResponse?: RelayMcpProxyOptions["redactToolResponse"];
|
|
241
241
|
private readonly relayMcpEndpoint: string;
|
|
242
242
|
private readonly getToken: () => string | undefined;
|
|
@@ -270,8 +270,8 @@ export class RelayMcpProxy {
|
|
|
270
270
|
this.getToken = options.getToken;
|
|
271
271
|
this.authSecret = options.authSecret;
|
|
272
272
|
this.enqueueBuffered = options.enqueueBuffered;
|
|
273
|
-
this.bufferableTools = options.bufferableTools
|
|
274
|
-
this.onDemandTools = options.onDemandTools;
|
|
273
|
+
this.bufferableTools = options.bufferableTools ? new Set(options.bufferableTools) : DEFAULT_BUFFERABLE_TOOLS;
|
|
274
|
+
this.onDemandTools = options.onDemandTools ? new Set(options.onDemandTools) : undefined;
|
|
275
275
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
276
276
|
this.abortSignal = options.abortSignal ?? AbortSignal.timeout;
|
|
277
277
|
const retry = options.connectRetry ?? options.spawnRetry;
|
|
@@ -323,7 +323,7 @@ export class RelayMcpProxy {
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
// The runner calls this after a token re-mint (scope may change, e.g. command:spawn) and on every
|
|
326
|
-
// bus re-register, since a deploy restarts the relay with new tools or schemas
|
|
326
|
+
// bus re-register, since a deploy restarts the relay with new tools or schemas. Re-fetch
|
|
327
327
|
// the tool list with the live token and emit list_changed if the visible set changed; setContext
|
|
328
328
|
// covers the workspace mode/state path. Best-effort: a failed refresh keeps the last-known list.
|
|
329
329
|
async refreshTools(): Promise<void> {
|
|
@@ -337,8 +337,6 @@ export class RelayMcpProxy {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
private async handle(req: Request): Promise<Response> {
|
|
340
|
-
const conflict = relayHeaderConflict(req.headers);
|
|
341
|
-
if (conflict) return Response.json({ error: conflict }, { status: 400 });
|
|
342
340
|
const url = new URL(req.url);
|
|
343
341
|
if (this.options.webToolLog && url.pathname === `/web/${this.options.authSecret}`) return forwardSharedMcp(req, this.options.webToolLog);
|
|
344
342
|
if (url.pathname !== PROXY_PATH) return new Response("not found", { status: 404 });
|
|
@@ -359,7 +357,7 @@ export class RelayMcpProxy {
|
|
|
359
357
|
}
|
|
360
358
|
|
|
361
359
|
private async handleRpc(req: Request): Promise<Response> {
|
|
362
|
-
//
|
|
360
|
+
// read the on-demand-front header BEFORE consuming the body. The per-agent metaOnly
|
|
363
361
|
// callmux's relay downstream sets it to opt OUT of the on-demand hiding (it fronts those tools);
|
|
364
362
|
// the agent's direct relay client never sets it, so it gets the eager hot-path set only.
|
|
365
363
|
const frontsOnDemand = relayHeader(req.headers, "ondemand-front") === RELAY_MCP_ONDEMAND_SURFACE_VALUE;
|
|
@@ -395,7 +393,7 @@ export class RelayMcpProxy {
|
|
|
395
393
|
// usable initialize result advertising the proxy's capabilities.
|
|
396
394
|
const result: Record<string, unknown> = isRecord(relay?.body?.result) ? { ...(relay!.body!.result as Record<string, unknown>) } : {
|
|
397
395
|
protocolVersion: "2024-11-05",
|
|
398
|
-
serverInfo: { ...
|
|
396
|
+
serverInfo: { ...MCP_SERVER_INFO, version: "proxy" },
|
|
399
397
|
};
|
|
400
398
|
const caps = isRecord(result.capabilities) ? { ...result.capabilities } : {};
|
|
401
399
|
// The capability the relay's static endpoint doesn't advertise: live tool sets.
|
|
@@ -413,7 +411,7 @@ export class RelayMcpProxy {
|
|
|
413
411
|
// If the relay is down and we have no cache yet, surface an empty list rather than erroring —
|
|
414
412
|
// the agent can still operate (writes buffer; tools/list refreshes on the next call).
|
|
415
413
|
const base = tools ?? this.lastRelayTools;
|
|
416
|
-
//
|
|
414
|
+
// callmux's relay downstream (frontsOnDemand) sees the full set so it can route
|
|
417
415
|
// callmux_call to any of it; the direct client has the on-demand set hidden.
|
|
418
416
|
const narrowed = this.narrow(base, frontsOnDemand);
|
|
419
417
|
this.lastFingerprints[frontsOnDemand ? "front" : "direct"] = toolsFingerprint(narrowed);
|
|
@@ -421,14 +419,14 @@ export class RelayMcpProxy {
|
|
|
421
419
|
}
|
|
422
420
|
|
|
423
421
|
// Strict subset of the relay's already-scope-filtered list. Only removes — never adds. Narrowing
|
|
424
|
-
// rules: (1) workspace tools apply only to an agent that owns a live worktree; (2)
|
|
422
|
+
// rules: (1) workspace tools apply only to an agent that owns a live worktree; (2) — the
|
|
425
423
|
// on-demand relay set is hidden from the agent's DIRECT client (only when `hideOnDemand`), since
|
|
426
424
|
// the per-agent metaOnly callmux fronts those tools on demand. `frontsOnDemand` (callmux's own
|
|
427
425
|
// request) skips rule 2 so callmux sees the full relay set.
|
|
428
426
|
private narrow(tools: Array<Record<string, unknown>>, frontsOnDemand = false): Array<Record<string, unknown>> {
|
|
429
427
|
const hideOnDemand = !frontsOnDemand && this.onDemandTools !== undefined && this.onDemandTools.size > 0;
|
|
430
428
|
return tools.filter((tool) => {
|
|
431
|
-
const name =
|
|
429
|
+
const name = typeof tool.name === "string" ? tool.name : "";
|
|
432
430
|
if (WORKTREE_ONLY_TOOLS.has(name) && !this.context.isolatedWorktree) return false;
|
|
433
431
|
if (hideOnDemand && this.onDemandTools!.has(name)) return false;
|
|
434
432
|
return true;
|
|
@@ -437,10 +435,11 @@ export class RelayMcpProxy {
|
|
|
437
435
|
|
|
438
436
|
private async handleToolsCall(id: string | number | null, message: JsonRpcMessage): Promise<Response> {
|
|
439
437
|
const params = isRecord(message.params) ? message.params : {};
|
|
440
|
-
const
|
|
438
|
+
const wireName = typeof params.name === "string" ? params.name : "";
|
|
439
|
+
const toolName = wireName;
|
|
441
440
|
let args = isRecord(params.arguments) ? params.arguments : {};
|
|
442
441
|
|
|
443
|
-
//
|
|
442
|
+
// inject this agent's workspace descriptor so the relay can self-heal a missing/dropped
|
|
444
443
|
// workspace registration during delivery. Only for worktree agents, only when the agent didn't
|
|
445
444
|
// already supply one, and the augmented args are forwarded (not just used locally).
|
|
446
445
|
if (
|
|
@@ -453,7 +452,7 @@ export class RelayMcpProxy {
|
|
|
453
452
|
message = { ...message, params: { ...params, arguments: args } };
|
|
454
453
|
}
|
|
455
454
|
|
|
456
|
-
//
|
|
455
|
+
// stabilize the spawn correlation id BEFORE the first attempt so a retry across a relay
|
|
457
456
|
// restart reuses it and the server dedupes to the SAME spawn (never a second agent). The caller's
|
|
458
457
|
// own id wins if supplied; otherwise mint an fs-safe `sp_<uuid>` here, because the relay would
|
|
459
458
|
// otherwise mint a FRESH id on each attempt and a retry would double-create. This is id-provisioning,
|
|
@@ -466,10 +465,10 @@ export class RelayMcpProxy {
|
|
|
466
465
|
}
|
|
467
466
|
}
|
|
468
467
|
|
|
469
|
-
//
|
|
468
|
+
// a `path` upload names a file on THIS host, but the relay reads it from
|
|
470
469
|
// its own filesystem, so remote agents always got "path not found". Materialize the
|
|
471
470
|
// file here and forward bytes; rejections mirror the relay's own messages verbatim.
|
|
472
|
-
if (toolName === "
|
|
471
|
+
if (toolName === "humwick_upload_artifact") {
|
|
473
472
|
const materialized = await materializeArtifactPathUpload(args, this.getToken);
|
|
474
473
|
if (materialized.error !== undefined) return Response.json(jsonRpcError(id, -32602, materialized.error));
|
|
475
474
|
if (materialized.args !== undefined) {
|
|
@@ -478,14 +477,14 @@ export class RelayMcpProxy {
|
|
|
478
477
|
}
|
|
479
478
|
}
|
|
480
479
|
|
|
481
|
-
//
|
|
480
|
+
// the call is safe to resend after an AMBIGUOUS (sent-then-response-lost) failure only if it
|
|
482
481
|
// carries an idempotency key the server dedupes on. Read generically off the args (spawnRequestId or
|
|
483
482
|
// idempotencyKey) — never off the tool name — so every idempotent tool qualifies and no other does.
|
|
484
483
|
const idempotent = isIdempotentCall(args);
|
|
485
484
|
|
|
486
485
|
const relay = await this.forwardWithConnectionRetry(message, toolName, idempotent).catch((error) => {
|
|
487
486
|
// A thrown fetch = transport failure (relay unreachable / DNS / connection refused / socket closed).
|
|
488
|
-
// The generic connect-retry
|
|
487
|
+
// The generic connect-retry has already exhausted its bounded window by here.
|
|
489
488
|
return { ok: false, status: 0, body: null, transportError: errMessage(error) } as ForwardResult;
|
|
490
489
|
});
|
|
491
490
|
|
|
@@ -495,17 +494,17 @@ export class RelayMcpProxy {
|
|
|
495
494
|
const idempotencyKey = typeof args.idempotencyKey === "string" && args.idempotencyKey
|
|
496
495
|
? args.idempotencyKey
|
|
497
496
|
: `mcp-${toolName}-${crypto.randomUUID()}`;
|
|
498
|
-
this.enqueueBuffered({ tool:
|
|
497
|
+
this.enqueueBuffered({ tool: wireName, arguments: { ...args, idempotencyKey }, idempotencyKey });
|
|
499
498
|
logger.info("mcp-proxy", `relay unreachable — buffered ${toolName} (idempotencyKey=${idempotencyKey})`);
|
|
500
499
|
return Response.json(jsonRpcResult(id, toolResult({
|
|
501
500
|
queued: true,
|
|
502
|
-
tool:
|
|
501
|
+
tool: wireName,
|
|
503
502
|
idempotencyKey,
|
|
504
503
|
note: "Relay was unreachable; your call was queued durably and will be delivered automatically when the relay comes back. It is not lost.",
|
|
505
504
|
})));
|
|
506
505
|
}
|
|
507
506
|
|
|
508
|
-
//
|
|
507
|
+
// propagate the relay's actual status (e.g. 413 payload-too-large, 400 validation,
|
|
509
508
|
// 403 auth) instead of the default 200 `Response.json` gives an untouched status option —
|
|
510
509
|
// a hard rejection silently reads as a normal 200 response to anything downstream that
|
|
511
510
|
// branches on HTTP status rather than parsing the JSON-RPC `error` field, masking a large
|
|
@@ -542,7 +541,7 @@ export class RelayMcpProxy {
|
|
|
542
541
|
const payload: Record<string, unknown> = { jsonrpc: "2.0", method: message.method };
|
|
543
542
|
if (message.id !== undefined) payload.id = message.id;
|
|
544
543
|
if (message.params !== undefined) payload.params = message.params;
|
|
545
|
-
//
|
|
544
|
+
// Bun 1.3.x's native HTTP idle watchdog aborts any fetch that sees no response bytes
|
|
546
545
|
// for ~300s, and an AbortSignal does NOT govern it: the server awaits the WHOLE tool call
|
|
547
546
|
// before its first response byte (src/mcp/index.ts `tools/call` → Response.json, no streaming),
|
|
548
547
|
// so a legitimate >~300s tool call was killed client-side at ~300s despite the 610s budget.
|
|
@@ -561,7 +560,7 @@ export class RelayMcpProxy {
|
|
|
561
560
|
return { ok: response.ok, status: response.status, body: parsed };
|
|
562
561
|
}
|
|
563
562
|
|
|
564
|
-
//
|
|
563
|
+
// GENERIC bounded connect-retry so a relay restart (CD deploy, no drain) is invisible to the
|
|
565
564
|
// agent for EVERY tool, not just spawn. One chokepoint, uniform across the tool suite. The retry
|
|
566
565
|
// decision is driven purely by the failure CLASS (connect / ambiguous / fatal) and the call's
|
|
567
566
|
// idempotency — never by tool name:
|
|
@@ -650,7 +649,7 @@ export class RelayMcpProxy {
|
|
|
650
649
|
}
|
|
651
650
|
|
|
652
651
|
private maybeEmitListChanged(): void {
|
|
653
|
-
// Definitions, not names: a new parameter on an existing tool is a change too
|
|
652
|
+
// Definitions, not names: a new parameter on an existing tool is a change too. A view
|
|
654
653
|
// nobody has listed yet needs no notification; its first list is current.
|
|
655
654
|
let changed = false;
|
|
656
655
|
for (const view of ["direct", "front"] as const) {
|