@humwick/runner 0.0.0-stage → 0.130.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.
Files changed (293) hide show
  1. package/LICENSE +167 -0
  2. package/agent-bin/agent-relay-browser +10 -0
  3. package/agent-bin/humwick +10 -0
  4. package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
  5. package/hooks/fff-search-redirect/hooks.json +13 -0
  6. package/hooks/rg-r-guard/hooks.json +18 -0
  7. package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
  8. package/hooks/web-tools-redirect/hooks.json +13 -0
  9. package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
  10. package/node_modules/@humwick/offload/LICENSE +167 -0
  11. package/node_modules/@humwick/offload/README.md +57 -0
  12. package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
  13. package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
  14. package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
  15. package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
  16. package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
  17. package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
  18. package/node_modules/@humwick/offload/dist/envelope.js +80 -0
  19. package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
  20. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
  21. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
  22. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
  23. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
  24. package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
  25. package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
  26. package/node_modules/@humwick/offload/dist/index.js +5 -0
  27. package/node_modules/@humwick/offload/dist/index.js.map +1 -0
  28. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
  29. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
  30. package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
  31. package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
  32. package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
  33. package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
  34. package/node_modules/@humwick/offload/dist/types.js +176 -0
  35. package/node_modules/@humwick/offload/dist/types.js.map +1 -0
  36. package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
  37. package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
  38. package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
  39. package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
  40. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
  41. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
  42. package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
  43. package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
  44. package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
  45. package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
  46. package/node_modules/@humwick/offload/dist/worker.js +1 -0
  47. package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
  48. package/node_modules/@humwick/offload/package.json +46 -0
  49. package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
  50. package/node_modules/@humwick/offload/src/envelope.ts +92 -0
  51. package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
  52. package/node_modules/@humwick/offload/src/index.ts +4 -0
  53. package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
  54. package/node_modules/@humwick/offload/src/types.ts +235 -0
  55. package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
  56. package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
  57. package/node_modules/@humwick/offload/src/worker.ts +5 -0
  58. package/package.json +47 -4
  59. package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
  60. package/plugins/claude/.claude-plugin/plugin.json +8 -0
  61. package/plugins/claude/claude-statusline-probe.cjs +144 -0
  62. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
  63. package/plugins/claude/hooks/gate-run-guard.sh +9 -0
  64. package/plugins/claude/hooks/gate-run-guard.ts +237 -0
  65. package/plugins/claude/hooks/hooks.json +158 -0
  66. package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
  67. package/plugins/claude/hooks/kill-guard.sh +341 -0
  68. package/plugins/claude/hooks/kill-guard.ts +337 -0
  69. package/plugins/claude/hooks/permission-request.sh +22 -0
  70. package/plugins/claude/hooks/post-compact.sh +10 -0
  71. package/plugins/claude/hooks/post-tool-use.sh +8 -0
  72. package/plugins/claude/hooks/pre-compact.sh +12 -0
  73. package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
  74. package/plugins/claude/hooks/relay-status.sh +373 -0
  75. package/plugins/claude/hooks/session-end.sh +26 -0
  76. package/plugins/claude/hooks/session-start.sh +31 -0
  77. package/plugins/claude/hooks/shell-trust.ts +28 -0
  78. package/plugins/claude/hooks/stop-failure.sh +47 -0
  79. package/plugins/claude/hooks/stop.sh +48 -0
  80. package/plugins/claude/hooks/subagent-guard.cjs +26 -0
  81. package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
  82. package/plugins/claude/hooks/subagent-start.sh +13 -0
  83. package/plugins/claude/hooks/subagent-stop.sh +19 -0
  84. package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
  85. package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
  86. package/plugins/claude/monitors/monitors.json +7 -0
  87. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
  88. package/plugins/claude/monitors/relay-monitor.ts +58 -0
  89. package/plugins/claude/skills/disconnect/SKILL.md +16 -0
  90. package/plugins/claude/skills/label/SKILL.md +23 -0
  91. package/plugins/claude/skills/message/SKILL.md +40 -0
  92. package/plugins/claude/skills/pair/SKILL.md +30 -0
  93. package/plugins/claude/skills/react/SKILL.md +18 -0
  94. package/plugins/claude/skills/read-message/SKILL.md +24 -0
  95. package/plugins/claude/skills/reply/SKILL.md +44 -0
  96. package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
  97. package/plugins/claude/skills/slice-first/SKILL.md +32 -0
  98. package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
  99. package/plugins/claude/skills/status/SKILL.md +16 -0
  100. package/plugins/claude/skills/tags/SKILL.md +25 -0
  101. package/plugins/claude/skills/task/SKILL.md +28 -0
  102. package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
  103. package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
  104. package/plugins/codex/skills/disconnect/SKILL.md +15 -0
  105. package/plugins/codex/skills/guide/SKILL.md +15 -0
  106. package/plugins/codex/skills/label/SKILL.md +22 -0
  107. package/plugins/codex/skills/message/SKILL.md +40 -0
  108. package/plugins/codex/skills/pair/SKILL.md +29 -0
  109. package/plugins/codex/skills/react/SKILL.md +17 -0
  110. package/plugins/codex/skills/read-message/SKILL.md +23 -0
  111. package/plugins/codex/skills/reply/SKILL.md +44 -0
  112. package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
  113. package/plugins/codex/skills/slice-first/SKILL.md +32 -0
  114. package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
  115. package/plugins/codex/skills/status/SKILL.md +15 -0
  116. package/plugins/codex/skills/tags/SKILL.md +24 -0
  117. package/plugins/codex/skills/task/SKILL.md +28 -0
  118. package/src/adapter.ts +1061 -0
  119. package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
  120. package/src/adapters/claude-api.ts +243 -0
  121. package/src/adapters/claude-context-command.ts +16 -0
  122. package/src/adapters/claude-credentials.ts +22 -0
  123. package/src/adapters/claude-delivery-lanes.ts +28 -0
  124. package/src/adapters/claude-delivery.ts +166 -0
  125. package/src/adapters/claude-effort.ts +94 -0
  126. package/src/adapters/claude-exit.ts +290 -0
  127. package/src/adapters/claude-headless-mirror.ts +88 -0
  128. package/src/adapters/claude-headless-refusal.ts +51 -0
  129. package/src/adapters/claude-headless.ts +617 -0
  130. package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
  131. package/src/adapters/claude-permission.ts +243 -0
  132. package/src/adapters/claude-prompt-gates.ts +269 -0
  133. package/src/adapters/claude-prompt-hook.ts +145 -0
  134. package/src/adapters/claude-session-capture.ts +293 -0
  135. package/src/adapters/claude-session-probe.ts +414 -0
  136. package/src/adapters/claude-shutdown.ts +190 -0
  137. package/src/adapters/claude-status-detectors.ts +426 -0
  138. package/src/adapters/claude-subagents.ts +93 -0
  139. package/src/adapters/claude-submit-confirmation.ts +287 -0
  140. package/src/adapters/claude-tmux.ts +548 -0
  141. package/src/adapters/claude-token-usage-capture.ts +253 -0
  142. package/src/adapters/claude-tool-policy.ts +165 -0
  143. package/src/adapters/claude-transcript-tail.ts +193 -0
  144. package/src/adapters/claude-transcript.ts +812 -0
  145. package/src/adapters/claude.ts +1097 -0
  146. package/src/adapters/codex-agent-message-capture.ts +189 -0
  147. package/src/adapters/codex-app-server.ts +70 -0
  148. package/src/adapters/codex-client.ts +362 -0
  149. package/src/adapters/codex-effective-config.ts +87 -0
  150. package/src/adapters/codex-launch-config.ts +153 -0
  151. package/src/adapters/codex-rollout-context.ts +272 -0
  152. package/src/adapters/codex-skill-discovery.ts +18 -0
  153. package/src/adapters/codex-token-usage-capture.ts +163 -0
  154. package/src/adapters/codex-tool-output.ts +53 -0
  155. package/src/adapters/codex-turn-outcome.ts +27 -0
  156. package/src/adapters/codex-version.ts +64 -0
  157. package/src/adapters/codex.ts +1442 -0
  158. package/src/adapters/incremental-file.ts +110 -0
  159. package/src/adapters/opencode-api.ts +71 -0
  160. package/src/adapters/opencode-context.ts +277 -0
  161. package/src/adapters/opencode-model-registry.ts +126 -0
  162. package/src/adapters/opencode-provider.ts +169 -0
  163. package/src/adapters/opencode-server-retry.ts +50 -0
  164. package/src/adapters/opencode-soft-steer.ts +302 -0
  165. package/src/adapters/opencode-spawn.ts +145 -0
  166. package/src/adapters/opencode-subscribe.ts +62 -0
  167. package/src/adapters/opencode-token-usage-capture.ts +187 -0
  168. package/src/adapters/opencode-tool-events.ts +302 -0
  169. package/src/adapters/opencode-tools-config.ts +17 -0
  170. package/src/adapters/opencode.ts +2264 -0
  171. package/src/adapters/project-shell.ts +80 -0
  172. package/src/adapters/provider-launch-config.ts +21 -0
  173. package/src/adapters/provider-quota-harvest.ts +127 -0
  174. package/src/adapters/service-access.ts +153 -0
  175. package/src/adapters/tmux-offload.ts +207 -0
  176. package/src/adapters/tool-summary.ts +214 -0
  177. package/src/agent-redact.ts +75 -0
  178. package/src/agent-runner/commands/dispatch.ts +185 -0
  179. package/src/agent-runner/commands/lifecycle.ts +84 -0
  180. package/src/agent-runner/commands/prompt.ts +284 -0
  181. package/src/agent-runner/credentials.ts +208 -0
  182. package/src/agent-runner/http-liveness.ts +85 -0
  183. package/src/agent-runner/lifecycle-milestones.ts +207 -0
  184. package/src/agent-runner/lifecycle-phase.ts +80 -0
  185. package/src/agent-runner/registration.ts +163 -0
  186. package/src/agent-runner/runner-context.ts +158 -0
  187. package/src/agent-runner/session-mirror.ts +278 -0
  188. package/src/agent-runner/task-claims.ts +116 -0
  189. package/src/agent-runner/token-usage-coverage.ts +54 -0
  190. package/src/agent-runner/turn-report.ts +269 -0
  191. package/src/api-budget-accounting.ts +91 -0
  192. package/src/area-memory.ts +255 -0
  193. package/src/artifact-path-materialize.ts +52 -0
  194. package/src/async-guard.ts +35 -0
  195. package/src/attachment-cache-sweep.ts +31 -0
  196. package/src/attachment-cache.ts +195 -0
  197. package/src/background-script-reconcile.ts +106 -0
  198. package/src/bounded-log.ts +59 -0
  199. package/src/bounded-spawn-sync.ts +18 -0
  200. package/src/busy-reconciler.ts +282 -0
  201. package/src/claim-tracker.ts +140 -0
  202. package/src/claude-settings.ts +167 -0
  203. package/src/claude-workspace-trust.ts +289 -0
  204. package/src/config.ts +547 -0
  205. package/src/context-boundary.ts +101 -0
  206. package/src/context-command.ts +151 -0
  207. package/src/context-probe-state.ts +6 -0
  208. package/src/continuation-archive.ts +179 -0
  209. package/src/control-server.ts +730 -0
  210. package/src/delivery-policy.ts +9 -0
  211. package/src/delivery-result.ts +24 -0
  212. package/src/effort-command.ts +125 -0
  213. package/src/empty-turn.ts +816 -0
  214. package/src/free-port.ts +25 -0
  215. package/src/fs-offload.ts +26 -0
  216. package/src/gate-run-config.ts +35 -0
  217. package/src/gate-run-lease-probe.ts +38 -0
  218. package/src/host-asset-plan.ts +450 -0
  219. package/src/host-claude-config-worker.ts +34 -0
  220. package/src/host-sources.ts +40 -0
  221. package/src/inbox-carrier.ts +7 -0
  222. package/src/index.ts +386 -0
  223. package/src/launch-assembly.ts +955 -0
  224. package/src/liveness.ts +105 -0
  225. package/src/logger.ts +108 -0
  226. package/src/mcp-outbox.ts +204 -0
  227. package/src/mcp-preflight.ts +86 -0
  228. package/src/mcp-source-selection.ts +54 -0
  229. package/src/mcp-tool-deferral.ts +35 -0
  230. package/src/mcp-vault-env.ts +25 -0
  231. package/src/message-body-config/config.ts +16 -0
  232. package/src/message-body-config.ts +1 -0
  233. package/src/mid-turn-delivery.ts +43 -0
  234. package/src/native-memory-lever.ts +329 -0
  235. package/src/native-self-resume.ts +244 -0
  236. package/src/outbox.ts +453 -0
  237. package/src/park-post.ts +17 -0
  238. package/src/pending-permission-store.ts +148 -0
  239. package/src/process-meta.ts +9 -0
  240. package/src/profile-home.ts +419 -0
  241. package/src/profile-projection.ts +411 -0
  242. package/src/project-hooks.ts +65 -0
  243. package/src/project-instructions.ts +119 -0
  244. package/src/provider-double.ts +59 -0
  245. package/src/provider-hold.ts +228 -0
  246. package/src/provider-home-marker.ts +3 -0
  247. package/src/provider-home-tombstone.ts +4 -0
  248. package/src/provider-input-queue.ts +134 -0
  249. package/src/provider-integration.ts +93 -0
  250. package/src/provider-processes.ts +17 -0
  251. package/src/provider-submit-failure.ts +15 -0
  252. package/src/providers.ts +35 -0
  253. package/src/provisioning.ts +703 -0
  254. package/src/question-decision.ts +18 -0
  255. package/src/quota.ts +22 -0
  256. package/src/rate-limit.ts +351 -0
  257. package/src/refused-prompt-store.ts +36 -0
  258. package/src/relay-injection-events.ts +161 -0
  259. package/src/relay-instructions.ts +107 -0
  260. package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
  261. package/src/relay-mcp-proxy.ts +704 -0
  262. package/src/relay-mcp.ts +583 -0
  263. package/src/reply-obligation-cache.ts +211 -0
  264. package/src/response-capture-report.ts +111 -0
  265. package/src/response-routing.ts +6 -0
  266. package/src/runner-core.ts +2955 -0
  267. package/src/runner-exit.ts +12 -0
  268. package/src/runner-helpers.ts +582 -0
  269. package/src/runner-insights.ts +105 -0
  270. package/src/runner-mcp-proxy.ts +46 -0
  271. package/src/runner-spawn-config.ts +68 -0
  272. package/src/runner-stale-scratch-sweep.ts +21 -0
  273. package/src/runner.ts +9 -0
  274. package/src/runtime-token-file.ts +45 -0
  275. package/src/runtime-token-remint.ts +56 -0
  276. package/src/service-access.ts +88 -0
  277. package/src/session-destroy.ts +27 -0
  278. package/src/session-insights.ts +118 -0
  279. package/src/session-scratch.ts +296 -0
  280. package/src/spawn-input.ts +147 -0
  281. package/src/startup-config-fetch.ts +48 -0
  282. package/src/subreaper.ts +113 -0
  283. package/src/template-resolver.ts +29 -0
  284. package/src/test-run-dedup.ts +435 -0
  285. package/src/transcript-context.ts +99 -0
  286. package/src/transport-errors.ts +48 -0
  287. package/src/turn-opener.ts +55 -0
  288. package/src/turn-reconcile-event.ts +88 -0
  289. package/src/vault-proxy-hop.ts +99 -0
  290. package/src/version.ts +44 -0
  291. package/src/web-tool-call-proxy.ts +128 -0
  292. package/src/web-tools.ts +59 -0
  293. package/README.md +0 -3
@@ -0,0 +1,107 @@
1
+ import { relayEnv } from "@humwick/sdk/env";
2
+ import { existsSync } from "node:fs";
3
+ import type { AgentProfile, ProvisioningMcpServer } from "@humwick/sdk";
4
+ import { webToolSteerRefs, type WebToolName } from "@humwick/sdk/web-tools";
5
+ import { TEMP_DIR_TEMPLATE_SLUG } from "@humwick/sdk/prompt-template-defaults";
6
+ import type { RunnerSpawnConfig } from "./adapter";
7
+ import { renderRunnerTemplate } from "./template-resolver";
8
+
9
+ export interface RelayContextResolution {
10
+ /** True only when the profile permits a non-empty context block for this provider. */
11
+ available: boolean;
12
+ /** The exact text the provider transport must emit, or an empty string when suppressed. */
13
+ text: string;
14
+ }
15
+
16
+ /** Shared profile-policy gate for every Relay-owned launch feature. */
17
+ export function profileAllowsRelayFeature(config: Pick<RunnerSpawnConfig, "agentProfile">, feature: keyof AgentProfile["relay"]): boolean {
18
+ return config.agentProfile?.relay?.[feature] !== false;
19
+ }
20
+
21
+ /**
22
+ * First-turn briefing (availability + text together). #2836: no core copy — the core is
23
+ * server-composed, and hand-started runners get no Relay instructions at all.
24
+ */
25
+ export function resolveRelayContext(input: {
26
+ slug: string;
27
+ config?: Pick<RunnerSpawnConfig, "agentProfile">;
28
+ mcpServers?: Record<string, ProvisioningMcpServer>;
29
+ /** #3024 — the launch's Brave/Jina tools, named per provider in the steer. */
30
+ webTools?: { names: readonly WebToolName[]; claude: boolean };
31
+ }): RelayContextResolution {
32
+ if (input.config && !profileAllowsRelayFeature(input.config, "context")) {
33
+ return { available: false, text: "" };
34
+ }
35
+ const fff = input.mcpServers?.fff;
36
+ const search = fff && (fff.url || (fff.command && existsSync(fff.command)))
37
+ ? renderRunnerTemplate("relay.search.fff") : "";
38
+ const web = input.webTools ? webToolsSteer(input.webTools.names, input.webTools.claude) : "";
39
+ const text = [renderRunnerTemplate(input.slug).trim(), search, web, renderRunnerTemplate(TEMP_DIR_TEMPLATE_SLUG)].filter(Boolean).join("\n\n");
40
+ return { available: text.length > 0, text };
41
+ }
42
+
43
+ export function claudeRelayManual(): string {
44
+ return renderRunnerTemplate("relay.manual.claude");
45
+ }
46
+
47
+ /** One line per capability: Brave searches; the local reader fetches, with keyless Jina fallback. */
48
+ export function webToolsSteer(names: readonly WebToolName[], claude: boolean): string {
49
+ return names.map(name => name === "brave"
50
+ ? `Web search: use ${claude ? "web_search on the agent-relay MCP server (load with ToolSearch)" : "web_search on the agent-relay MCP server"}. Relay tries Tavily, then budget-capped Brave. If the monthly cap is reached, report it; do not retry in a loop.`
51
+ : renderRunnerTemplate("relay.web.fetch", {
52
+ tools: webToolSteerRefs(name, claude), fallback: webToolSteerRefs(name, claude, true),
53
+ })).join("\n");
54
+ }
55
+
56
+ export function claudeRelayContext(): string {
57
+ return resolveRelayContext({ slug: "relay.context.claude" }).text;
58
+ }
59
+
60
+ export function claudeReadOnlyRelayContext(): string {
61
+ return resolveRelayContext({ slug: "relay.context.claude.readonly" }).text;
62
+ }
63
+
64
+ export function relayReplyActionText(messageId: number): string {
65
+ return `relay_reply (messageId: ${messageId}, body: "<your reply>"). CLI fallback: agent-relay /reply ${messageId} "<your reply>"`;
66
+ }
67
+
68
+ export function workspaceDepsNote(input: { mode?: string | null; depsMode?: string | null; installCommand?: string | null }): string {
69
+ if (input.mode !== "isolated") return "";
70
+ switch (input.depsMode) {
71
+ case "symlink":
72
+ return renderRunnerTemplate("workspace.deps.symlink");
73
+ case "command":
74
+ return renderRunnerTemplate("workspace.deps.command", { installCommand: input.installCommand ?? "the repository install command" });
75
+ case "none":
76
+ return renderRunnerTemplate("workspace.deps.none");
77
+ default:
78
+ return "";
79
+ }
80
+ }
81
+
82
+ export function workspaceLifecycleNote(input: { mode?: string | null; branch?: string | null; baseRef?: string | null }): string {
83
+ if (input.mode !== "isolated") return "";
84
+ const branch = input.branch ? `\`${input.branch}\`` : "an isolated agent branch";
85
+ const base = input.baseRef ? `\`${input.baseRef}\`` : "the base branch";
86
+ return renderRunnerTemplate("workspace.lifecycle", { branch, baseRef: base });
87
+ }
88
+
89
+ export function workspaceSymlinksNote(linked: string[]): string {
90
+ if (!linked.length) return "";
91
+ return renderRunnerTemplate("workspace.symlinks", { linked: linked.join(", ") });
92
+ }
93
+
94
+ export function workspaceDepsNoteFromEnv(env: Record<string, string | undefined> = process.env): string {
95
+ const json = relayEnv(env, "AGENT_RELAY_WORKSPACE_JSON");
96
+ if (!json) return "";
97
+ try {
98
+ const parsed = JSON.parse(json) as { mode?: string; branch?: string; baseRef?: string; deps?: { mode?: string; command?: string }; symlinks?: { linked?: string[] } };
99
+ return [
100
+ workspaceLifecycleNote({ mode: parsed.mode ?? null, branch: parsed.branch ?? null, baseRef: parsed.baseRef ?? null }),
101
+ workspaceDepsNote({ mode: parsed.mode ?? null, depsMode: parsed.deps?.mode ?? null, installCommand: parsed.deps?.command ?? null }),
102
+ parsed.mode === "isolated" ? workspaceSymlinksNote(parsed.symlinks?.linked ?? []) : "",
103
+ ].filter(Boolean).join("\n\n");
104
+ } catch {
105
+ return "";
106
+ }
107
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * #2429 / #2537 — opt-in compressed long-turn E2E for the shared MCP tool-call forward.
3
+ *
4
+ * Spawned as a CHILD bun process by the AGENT_RELAY_LONG_TURN_E2E=1-gated test in
5
+ * relay-mcp-proxy.test.ts, with BUN_CONFIG_HTTP_IDLE_TIMEOUT=2 compressing Bun's native
6
+ * ~300s HTTP idle watchdog down to ~2s for this process. A stand-in relay withholds ALL
7
+ * response bytes for 6s (mimicking src/mcp/index.ts `tools/call`, which awaits the WHOLE
8
+ * tool before its first byte):
9
+ *
10
+ * CONTROL — a plain fetch (no `timeout` option) against the slow relay must die with
11
+ * TimeoutError: proves this process's idle watchdog is live and would abort
12
+ * any >idle-timeout forward, i.e. the pre-fix behavior of forward().
13
+ * REAL PATH — a long `relay_run` `tools/call` through the REAL RelayMcpProxy (whose
14
+ * forward() sets `timeout:false`) must complete with the usable
15
+ * `still-running` + `runId` envelope despite sitting silent well past the
16
+ * compressed idle timeout.
17
+ *
18
+ * Exits 0 and prints LONG-TURN E2E PASS on success; non-zero with diagnostics otherwise.
19
+ */
20
+ import { RelayMcpProxy } from "./relay-mcp-proxy";
21
+
22
+ const WITHHOLD_MS = 6_000;
23
+ const SECRET = "long-turn-e2e-secret";
24
+ const CHILD_DEADLINE_MS = 45_000;
25
+
26
+ interface JsonRpcBody {
27
+ id?: string | number | null;
28
+ method?: string;
29
+ params?: {
30
+ name?: string;
31
+ arguments?: {
32
+ waitMs?: number;
33
+ };
34
+ };
35
+ }
36
+
37
+ function fail(message: string): never {
38
+ console.error(`LONG-TURN E2E FAIL: ${message}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ async function withDeadline<T>(label: string, promise: Promise<T>, ms: number): Promise<T> {
43
+ let timer: ReturnType<typeof setTimeout> | undefined;
44
+ const guard = new Promise<never>((_, reject) => {
45
+ timer = setTimeout(() => reject(new Error(`${label} exceeded ${ms}ms child deadline`)), ms);
46
+ });
47
+ try {
48
+ return await Promise.race([promise, guard]);
49
+ } finally {
50
+ clearTimeout(timer);
51
+ }
52
+ }
53
+
54
+ // Stand-in relay: accepts the JSON-RPC POST, drains the body (request fully received — the
55
+ // connect phase is over), then withholds every response byte until WITHHOLD_MS has elapsed.
56
+ // Server-side idleTimeout pinned high so ONLY the client-side watchdog is under test.
57
+ const slowRelay = Bun.serve({
58
+ port: 0,
59
+ hostname: "127.0.0.1",
60
+ idleTimeout: 255,
61
+ fetch: async (req) => {
62
+ const raw = await req.text();
63
+ let id: string | number | null = null;
64
+ let request: JsonRpcBody | undefined;
65
+ try {
66
+ request = JSON.parse(raw) as JsonRpcBody;
67
+ id = request.id ?? null;
68
+ } catch {
69
+ // Keep the intentionally silent response shape even for malformed control input.
70
+ }
71
+ await Bun.sleep(WITHHOLD_MS);
72
+ if (!request || request.method !== "tools/call" || request.params?.name !== "relay_run") {
73
+ return Response.json(
74
+ { jsonrpc: "2.0", id, error: { code: -32601, message: "expected relay_run tools/call" } },
75
+ { status: 400 },
76
+ );
77
+ }
78
+ if (request.params.arguments?.waitMs !== 600_000) {
79
+ return Response.json(
80
+ { jsonrpc: "2.0", id, error: { code: -32602, message: "expected a 600000ms inline wait" } },
81
+ { status: 400 },
82
+ );
83
+ }
84
+ return Response.json({
85
+ jsonrpc: "2.0",
86
+ id,
87
+ result: {
88
+ content: [{ type: "text", text: "relay_run still running" }],
89
+ structuredContent: {
90
+ run: {
91
+ runId: "long-turn-run-id",
92
+ status: "still-running",
93
+ exitCode: null,
94
+ },
95
+ },
96
+ },
97
+ });
98
+ },
99
+ });
100
+
101
+ async function controlPlainFetchMustTimeOut(relayUrl: string): Promise<void> {
102
+ const startedAt = Date.now();
103
+ try {
104
+ const res = await fetch(relayUrl, {
105
+ method: "POST",
106
+ headers: { "content-type": "application/json" },
107
+ body: JSON.stringify({ jsonrpc: "2.0", id: "control", method: "tools/call", params: {} }),
108
+ });
109
+ await res.text();
110
+ fail(
111
+ `control plain fetch COMPLETED after ${Date.now() - startedAt}ms — the compressed idle `
112
+ + "watchdog is NOT active in this process; BUN_CONFIG_HTTP_IDLE_TIMEOUT did not apply and "
113
+ + "this E2E proves nothing",
114
+ );
115
+ } catch (error) {
116
+ const err = error as Error;
117
+ const timedOut = err.name === "TimeoutError" || /timed out/i.test(err.message);
118
+ if (!timedOut) fail(`control plain fetch failed with an unexpected error: ${err.name}: ${err.message}`);
119
+ console.log(`control: plain fetch died as expected (${err.name}) after ${Date.now() - startedAt}ms`);
120
+ }
121
+ }
122
+
123
+ async function proxyForwardMustComplete(): Promise<void> {
124
+ const proxy = new RelayMcpProxy({
125
+ relayMcpEndpoint: `http://127.0.0.1:${slowRelay.port}/api/mcp`,
126
+ getToken: () => "long-turn-e2e-live-token",
127
+ authSecret: SECRET,
128
+ enqueueBuffered: () => {},
129
+ });
130
+ const { url } = proxy.start();
131
+ try {
132
+ const startedAt = Date.now();
133
+ // The agent-facing hop is NOT under test here (in production it does not run under the
134
+ // compressed env), so it also opts out of the watchdog to avoid a false failure; the hop
135
+ // being verified is forward() inside the proxy, whose fetch carries the #2429 fix.
136
+ const agentSideInit: RequestInit & { timeout?: number | false } = {
137
+ method: "POST",
138
+ headers: { "content-type": "application/json", authorization: `Bearer ${SECRET}` },
139
+ body: JSON.stringify({
140
+ jsonrpc: "2.0",
141
+ id: "long-turn-1",
142
+ method: "tools/call",
143
+ params: {
144
+ name: "relay_run",
145
+ // A real maximum inline window is intentionally longer than this child's compressed
146
+ // watchdog. The stand-in withholds the envelope for 6s, while its unconfigured
147
+ // watchdog dies at ~2-4s, reproducing the pre-fix no-runId failure shape.
148
+ arguments: { command: "sleep 600", waitMs: 600_000, maxMs: 900_000 },
149
+ },
150
+ }),
151
+ timeout: false,
152
+ };
153
+ const res = await fetch(url, agentSideInit);
154
+ const body = await res.json() as {
155
+ result?: {
156
+ content?: Array<{ text?: string }>;
157
+ structuredContent?: { run?: { runId?: string; status?: string; exitCode?: number | null } };
158
+ };
159
+ error?: unknown;
160
+ };
161
+ if (!res.ok || !body.result) {
162
+ fail(`proxy forward returned status ${res.status} body ${JSON.stringify(body)} — forward() `
163
+ + "did not survive the idle watchdog; is the #2429 `timeout:false` flag present?");
164
+ }
165
+ const run = body.result?.structuredContent?.run;
166
+ if (run?.runId !== "long-turn-run-id" || run.status !== "still-running" || run.exitCode !== null) {
167
+ fail(`relay_run did not return a usable still-running + runId envelope: ${JSON.stringify(body.result)}`);
168
+ }
169
+ console.log(`real path: relay_run returned still-running + runId after ${Date.now() - startedAt}ms`);
170
+ } finally {
171
+ proxy.stop();
172
+ }
173
+ }
174
+
175
+ async function main(): Promise<void> {
176
+ const relayUrl = `http://127.0.0.1:${slowRelay.port}/api/mcp`;
177
+ await withDeadline("control plain fetch", controlPlainFetchMustTimeOut(relayUrl), 20_000);
178
+ await withDeadline("proxy forward", proxyForwardMustComplete(), 20_000);
179
+ slowRelay.stop(true);
180
+ console.log("LONG-TURN E2E PASS");
181
+ }
182
+
183
+ await withDeadline("long-turn E2E child", main(), CHILD_DEADLINE_MS).catch((error: Error) => fail(error.message));