@desplega.ai/agent-swarm 1.124.0 → 1.125.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 (101) hide show
  1. package/dist/{actions-z3d0fk7z.js → actions-3aky6er5.js} +6 -5
  2. package/dist/{app-q2rc9tdr.js → app-xbt4n4mt.js} +4 -3
  3. package/dist/{assistant-3stjq7fz.js → assistant-ncftbs9t.js} +12 -10
  4. package/dist/{boot-reembed-v6r1df32.js → boot-reembed-00cxm1mj.js} +5 -4
  5. package/dist/{boot-reembed-5867whnb.js → boot-reembed-5xxzva12.js} +4 -3
  6. package/dist/{boot-scrub-logs-8b8jegk3.js → boot-scrub-logs-9cmcq6ez.js} +3 -2
  7. package/dist/{cli-0925phzv.js → cli-0w3mk6r2.js} +1 -1
  8. package/dist/{cli-zxb72ctk.js → cli-3x5aphbt.js} +2 -2
  9. package/dist/{cli-exfs8aym.js → cli-3y8xyp0b.js} +7 -1
  10. package/dist/{cli-2yr6edr7.js → cli-4xft7caa.js} +4 -4
  11. package/dist/{cli-kk58tg33.js → cli-5a4nmfrn.js} +12 -10
  12. package/dist/{cli-5jn6wnd9.js → cli-88r31vx9.js} +2 -2
  13. package/dist/{cli-sqv6c9b8.js → cli-8bkwn1wv.js} +11 -9
  14. package/dist/{cli-p0zgv8pj.js → cli-8vzddbs7.js} +2 -1
  15. package/dist/{cli-qf7acvyr.js → cli-94796kxr.js} +2 -2
  16. package/dist/{cli-wn3m6q54.js → cli-dbmh9yqn.js} +7 -5
  17. package/dist/{cli-p0ec4vv6.js → cli-ff8b5cbe.js} +1 -1
  18. package/dist/{cli-y43n987v.js → cli-gbx122ge.js} +1 -1
  19. package/dist/{cli-ykthjw29.js → cli-jcavqvh7.js} +20 -24
  20. package/dist/cli-k1py8264.js +1129 -0
  21. package/dist/{cli-qwsagsmg.js → cli-k5c6jfzn.js} +1 -1
  22. package/dist/{cli-gkvf8d83.js → cli-n02497b2.js} +5 -3
  23. package/dist/{cli-vm20sh8d.js → cli-nqq9tthd.js} +9 -7
  24. package/dist/{cli-jp4mdj9d.js → cli-pqtrhrss.js} +2 -2
  25. package/dist/{cli-gzjsamwc.js → cli-q92pr3rt.js} +1 -1
  26. package/dist/{cli-mmk5vagg.js → cli-qzy21svh.js} +2 -2
  27. package/dist/{cli-dq2ssyf0.js → cli-refygp3c.js} +5 -5
  28. package/dist/{cli-perb20ma.js → cli-tar44ekp.js} +1 -1
  29. package/dist/{cli-h89g0qrd.js → cli-v292k2fv.js} +1 -1
  30. package/dist/{cli-mcez7z4p.js → cli-vvbwy286.js} +1 -1
  31. package/dist/{cli-x268say8.js → cli-wbcsjpyv.js} +366 -68
  32. package/dist/cli-wdrrgv4m.js +22 -0
  33. package/dist/{cli-3z4q09tn.js → cli-yr11vvst.js} +32 -1127
  34. package/dist/{cli-92bqgbgy.js → cli-zhp3t49e.js} +4 -2
  35. package/dist/cli.js +23 -10
  36. package/dist/{codex-adapter-vcseqpwf.js → codex-adapter-mqczt8xv.js} +1 -1
  37. package/dist/codex-hook-emq42pfq.js +106 -0
  38. package/dist/{codex-session-runner-jec9e9t1.js → codex-session-runner-mrwz1h50.js} +1 -1
  39. package/dist/{commands-sda76yn8.js → commands-7dbx98fp.js} +5 -3
  40. package/dist/{db-vn9e4rqj.js → db-r5705rk8.js} +7 -2
  41. package/dist/{handlers-xfja18ac.js → handlers-8kj038es.js} +12 -10
  42. package/dist/{hook-2medrp47.js → hook-2590fnyk.js} +1 -1
  43. package/dist/{http-937pyjfc.js → http-8yej35d0.js} +77 -55
  44. package/dist/{index-vah60vxp.js → index-hy73j39b.js} +8 -7
  45. package/dist/{index-knqczmnm.js → index-kgay2v5w.js} +11 -10
  46. package/dist/{index-gw4en7jg.js → index-v3n4hb3w.js} +10 -9
  47. package/dist/{index-2bqd2q9v.js → index-x1n3tybc.js} +12 -11
  48. package/dist/{keepalive-20rd6hxh.js → keepalive-q8yg6d6r.js} +6 -5
  49. package/dist/{lead-7w2k3a49.js → lead-yxtt169p.js} +23 -21
  50. package/dist/{maintenance-p3x918yw.js → maintenance-70cskw5t.js} +5 -4
  51. package/dist/{oauth-refresh-sweep-shm0nzm4.js → oauth-refresh-sweep-90h9rzx8.js} +5 -4
  52. package/dist/{onboard-py9dnf54.js → onboard-20reqz6j.js} +2 -2
  53. package/dist/{otel-impl-7yjkfg68.js → otel-impl-vja8f9mm.js} +1 -1
  54. package/dist/{pricing-refresh-3snb6370.js → pricing-refresh-34mbexgm.js} +5 -4
  55. package/dist/{rbac-roles-gqxdp8st.js → rbac-roles-s1qnewkt.js} +5 -4
  56. package/dist/{rbac-roles-5e72bbpk.js → rbac-roles-sa59eyp4.js} +4 -3
  57. package/dist/{seed-pricing-bg42d7h2.js → seed-pricing-p5fnj0qd.js} +4 -3
  58. package/dist/{setup-nzk3kbbq.js → setup-p9yq3mb2.js} +2 -2
  59. package/dist/{worker-0p1jdzkm.js → worker-21zrq8cd.js} +23 -21
  60. package/openapi.json +1 -1
  61. package/package.json +2 -1
  62. package/src/be/db.ts +30 -0
  63. package/src/cli.tsx +12 -0
  64. package/src/commands/runner.ts +11 -23
  65. package/src/hooks/codex-hook.ts +157 -0
  66. package/src/http/kv.ts +37 -8
  67. package/src/http/mcp-bridge.ts +3 -2
  68. package/src/kv-overflow.ts +20 -0
  69. package/src/prompts/steering-delivery.ts +32 -0
  70. package/src/providers/codex-adapter.ts +20 -4
  71. package/src/providers/types.ts +8 -0
  72. package/src/script-workflows/harness.ts +5 -1
  73. package/src/script-workflows/workflow-ctx.ts +6 -7
  74. package/src/scripts-runtime/eval-harness.ts +11 -2
  75. package/src/scripts-runtime/extract-args-schema.ts +5 -1
  76. package/src/scripts-runtime/response-limit.ts +50 -0
  77. package/src/scripts-runtime/swarm-sdk.ts +3 -4
  78. package/src/tests/claude-adapter-binary.test.ts +64 -32
  79. package/src/tests/codex-steering-hook.test.ts +224 -0
  80. package/src/tests/kv-http.test.ts +35 -1
  81. package/src/tests/kv-storage.test.ts +45 -0
  82. package/src/tests/kv-tool.test.ts +51 -0
  83. package/src/tests/run-bun-tests.test.ts +171 -0
  84. package/src/tests/scripts-mcp-e2e.test.ts +105 -1
  85. package/src/tests/scripts-runtime.test.ts +33 -0
  86. package/src/tests/sdk-allowlist.test.ts +27 -1
  87. package/src/tests/slack-read-boundaries.test.ts +229 -0
  88. package/src/tests/steering-core.test.ts +19 -19
  89. package/src/tests/steering-transport.test.ts +21 -3
  90. package/src/tests/store-progress-attachments-handler.test.ts +45 -10
  91. package/src/tests/swarm-tool-result-gate.test.ts +300 -21
  92. package/src/tests/tool-output-agent-id.test.ts +9 -3
  93. package/src/tests/tool-registrar-no-input.test.ts +6 -0
  94. package/src/tools/kv/kv-get.ts +10 -0
  95. package/src/tools/kv/kv-list.ts +7 -0
  96. package/src/tools/kv/kv-read-auth.ts +13 -0
  97. package/src/tools/kv/kv-write-auth.ts +4 -0
  98. package/src/tools/steer-task.ts +1 -1
  99. package/src/tools/store-progress.ts +24 -7
  100. package/src/tools/utils.ts +403 -43
  101. package/src/types.ts +7 -1
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  forceRefreshAuthorizationOrThrow
3
- } from "./cli-h89g0qrd.js";
3
+ } from "./cli-v292k2fv.js";
4
4
  import {
5
5
  getAuthorizationById,
6
6
  listAuthorizationSweepRows
7
- } from "./cli-mcez7z4p.js";
8
- import"./cli-3z4q09tn.js";
7
+ } from "./cli-vvbwy286.js";
8
+ import"./cli-yr11vvst.js";
9
+ import"./cli-k1py8264.js";
9
10
  import"./cli-z2zcxes1.js";
10
- import"./cli-p0zgv8pj.js";
11
+ import"./cli-8vzddbs7.js";
11
12
  import {
12
13
  init_secret_scrubber,
13
14
  scrubSecrets
@@ -3,7 +3,7 @@ import {
3
3
  createDefaultMcpJson,
4
4
  createDefaultSettingsLocal,
5
5
  createHooksConfig
6
- } from "./cli-perb20ma.js";
6
+ } from "./cli-tar44ekp.js";
7
7
  import {
8
8
  require_jsx_dev_runtime
9
9
  } from "./cli-wx82tw1k.js";
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  init_package,
15
15
  package_default
16
- } from "./cli-p0zgv8pj.js";
16
+ } from "./cli-8vzddbs7.js";
17
17
  import {
18
18
  Box_default,
19
19
  Select,
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  init_package,
11
11
  package_default
12
- } from "./cli-p0zgv8pj.js";
12
+ } from "./cli-8vzddbs7.js";
13
13
  import {
14
14
  init_secret_scrubber,
15
15
  scrubSecrets
@@ -2,11 +2,12 @@ import {
2
2
  PRICING_REFRESH_INTERVAL_MS,
3
3
  refreshPricingFromModelsDev,
4
4
  startPricingRefreshLoop
5
- } from "./cli-qf7acvyr.js";
6
- import"./cli-gzjsamwc.js";
7
- import"./cli-3z4q09tn.js";
5
+ } from "./cli-94796kxr.js";
6
+ import"./cli-q92pr3rt.js";
7
+ import"./cli-yr11vvst.js";
8
+ import"./cli-k1py8264.js";
8
9
  import"./cli-z2zcxes1.js";
9
- import"./cli-p0zgv8pj.js";
10
+ import"./cli-8vzddbs7.js";
10
11
  import"./cli-4j2mrzag.js";
11
12
  import"./cli-bw80ck94.js";
12
13
  import"./cli-xz9aq0rf.js";
@@ -7,11 +7,12 @@ import {
7
7
  getUserGrant,
8
8
  listUserRoles,
9
9
  runRbacCliCommand
10
- } from "./cli-zxb72ctk.js";
11
- import"./cli-y43n987v.js";
12
- import"./cli-3z4q09tn.js";
10
+ } from "./cli-3x5aphbt.js";
11
+ import"./cli-gbx122ge.js";
12
+ import"./cli-yr11vvst.js";
13
+ import"./cli-k1py8264.js";
13
14
  import"./cli-z2zcxes1.js";
14
- import"./cli-p0zgv8pj.js";
15
+ import"./cli-8vzddbs7.js";
15
16
  import"./cli-4j2mrzag.js";
16
17
  import"./cli-bw80ck94.js";
17
18
  import"./cli-xz9aq0rf.js";
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  PermissionVerbSchema
3
- } from "./cli-y43n987v.js";
3
+ } from "./cli-gbx122ge.js";
4
4
  import {
5
5
  getDb,
6
6
  init_db
7
- } from "./cli-3z4q09tn.js";
7
+ } from "./cli-yr11vvst.js";
8
+ import"./cli-k1py8264.js";
8
9
  import"./cli-z2zcxes1.js";
9
- import"./cli-p0zgv8pj.js";
10
+ import"./cli-8vzddbs7.js";
10
11
  import"./cli-4j2mrzag.js";
11
12
  import"./cli-bw80ck94.js";
12
13
  import"./cli-xz9aq0rf.js";
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  buildModelsDevSeedRows,
3
3
  seedPricingFromModelsDev
4
- } from "./cli-gzjsamwc.js";
5
- import"./cli-3z4q09tn.js";
4
+ } from "./cli-q92pr3rt.js";
5
+ import"./cli-yr11vvst.js";
6
+ import"./cli-k1py8264.js";
6
7
  import"./cli-z2zcxes1.js";
7
- import"./cli-p0zgv8pj.js";
8
+ import"./cli-8vzddbs7.js";
8
9
  import"./cli-4j2mrzag.js";
9
10
  import"./cli-bw80ck94.js";
10
11
  import"./cli-xz9aq0rf.js";
@@ -4,14 +4,14 @@ import {
4
4
  createDefaultMcpJson,
5
5
  createDefaultSettingsLocal,
6
6
  createHooksConfig
7
- } from "./cli-perb20ma.js";
7
+ } from "./cli-tar44ekp.js";
8
8
  import {
9
9
  require_jsx_dev_runtime
10
10
  } from "./cli-wx82tw1k.js";
11
11
  import {
12
12
  getApiKey
13
13
  } from "./cli-f14fvzag.js";
14
- import"./cli-p0zgv8pj.js";
14
+ import"./cli-8vzddbs7.js";
15
15
  import {
16
16
  Box_default,
17
17
  Spinner,
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  runAgent
3
- } from "./cli-ykthjw29.js";
4
- import"./cli-exfs8aym.js";
3
+ } from "./cli-jcavqvh7.js";
4
+ import"./cli-wdrrgv4m.js";
5
+ import"./cli-3y8xyp0b.js";
5
6
  import"./cli-jj1arm18.js";
6
7
  import"./cli-07er7wrs.js";
7
8
  import"./cli-w5r4wfm5.js";
@@ -14,7 +15,7 @@ import"./cli-d6n0thxw.js";
14
15
  import"./cli-v1mwc5v2.js";
15
16
  import {
16
17
  getEnabledCapabilities
17
- } from "./cli-x268say8.js";
18
+ } from "./cli-wbcsjpyv.js";
18
19
  import"./cli-1z7048dy.js";
19
20
  import"./cli-gz9sbyk3.js";
20
21
  import"./cli-ftyf58xj.js";
@@ -25,36 +26,37 @@ import"./cli-tng3131n.js";
25
26
  import"./cli-zm7njwzd.js";
26
27
  import"./cli-cw70hymf.js";
27
28
  import"./cli-e61fs2hp.js";
28
- import"./cli-2yr6edr7.js";
29
- import"./cli-qf7acvyr.js";
30
- import"./cli-gzjsamwc.js";
31
- import"./cli-zxb72ctk.js";
32
- import"./cli-y43n987v.js";
33
- import"./cli-gkvf8d83.js";
34
- import"./cli-kk58tg33.js";
29
+ import"./cli-4xft7caa.js";
30
+ import"./cli-94796kxr.js";
31
+ import"./cli-q92pr3rt.js";
32
+ import"./cli-3x5aphbt.js";
33
+ import"./cli-gbx122ge.js";
34
+ import"./cli-n02497b2.js";
35
+ import"./cli-5a4nmfrn.js";
35
36
  import"./cli-f14fvzag.js";
36
37
  import"./cli-3dpj7atn.js";
37
- import"./cli-h89g0qrd.js";
38
- import"./cli-wn3m6q54.js";
38
+ import"./cli-v292k2fv.js";
39
+ import"./cli-dbmh9yqn.js";
39
40
  import"./cli-jkhwmmgg.js";
40
- import"./cli-mmk5vagg.js";
41
- import"./cli-p0ec4vv6.js";
41
+ import"./cli-qzy21svh.js";
42
+ import"./cli-ff8b5cbe.js";
42
43
  import"./cli-q25ejbyn.js";
43
- import"./cli-mcez7z4p.js";
44
+ import"./cli-vvbwy286.js";
44
45
  import"./cli-fad8m16k.js";
45
46
  import"./cli-7zd05xb4.js";
46
- import"./cli-92bqgbgy.js";
47
- import"./cli-0925phzv.js";
48
- import"./cli-qwsagsmg.js";
49
- import"./cli-dq2ssyf0.js";
47
+ import"./cli-zhp3t49e.js";
48
+ import"./cli-0w3mk6r2.js";
49
+ import"./cli-k5c6jfzn.js";
50
+ import"./cli-refygp3c.js";
50
51
  import"./cli-rkndnn90.js";
51
52
  import"./cli-b0p7rfnd.js";
52
53
  import"./cli-5ncdb7ff.js";
53
54
  import"./cli-wspgs9bt.js";
54
55
  import"./cli-dh55d5fg.js";
55
- import"./cli-3z4q09tn.js";
56
+ import"./cli-yr11vvst.js";
57
+ import"./cli-k1py8264.js";
56
58
  import"./cli-z2zcxes1.js";
57
- import"./cli-p0zgv8pj.js";
59
+ import"./cli-8vzddbs7.js";
58
60
  import"./cli-4j2mrzag.js";
59
61
  import"./cli-bw80ck94.js";
60
62
  import"./cli-xz9aq0rf.js";
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.124.0",
5
+ "version": "1.125.0",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.124.0",
3
+ "version": "1.125.0",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -65,6 +65,7 @@
65
65
  "mono:test": "turbo run test",
66
66
  "mono:lint": "turbo run lint",
67
67
  "mono:dev": "turbo run dev",
68
+ "test:root": "bash scripts/run-bun-tests.sh",
68
69
  "prepack": "bun run build:cli",
69
70
  "tsc:check": "bun tsc --noEmit",
70
71
  "check:db-boundary": "bash scripts/check-db-boundary.sh",
package/src/be/db.ts CHANGED
@@ -13657,6 +13657,36 @@ export function getKv(namespace: string, key: string): KvEntry | null {
13657
13657
  return decodeKvRow(row);
13658
13658
  }
13659
13659
 
13660
+ /** Delete expired entries in one namespace. Used by internal TTL-backed stores
13661
+ * that need proactive cleanup rather than waiting for a point read. */
13662
+ export function sweepExpiredKv(namespace: string, now = Date.now()): number {
13663
+ const result = getDb()
13664
+ .prepare<unknown, [string, number]>(
13665
+ `DELETE FROM kv_entries
13666
+ WHERE namespace = ?
13667
+ AND expires_at IS NOT NULL
13668
+ AND expires_at <= ?`,
13669
+ )
13670
+ .run(namespace, now);
13671
+ return result.changes;
13672
+ }
13673
+
13674
+ /** Delete expired entries across a namespace family (`prefix` and
13675
+ * `prefix:*`). Used by per-agent internal stores whose inactive owners may
13676
+ * never return to trigger a namespace-local sweep. */
13677
+ export function sweepExpiredKvPrefix(prefix: string, now = Date.now()): number {
13678
+ const escaped = prefix.replace(/[\\%_]/g, "\\$&");
13679
+ const result = getDb()
13680
+ .prepare<unknown, [string, string, number]>(
13681
+ `DELETE FROM kv_entries
13682
+ WHERE (namespace = ? OR namespace LIKE ? ESCAPE '\\')
13683
+ AND expires_at IS NOT NULL
13684
+ AND expires_at <= ?`,
13685
+ )
13686
+ .run(prefix, `${escaped}:%`, now);
13687
+ return result.changes;
13688
+ }
13689
+
13660
13690
  /**
13661
13691
  * Upsert a KV entry. Caller passes the decoded value + valueType; we encode
13662
13692
  * before storing. `expiresAt` is unix-ms (NULL means no expiry).
package/src/cli.tsx CHANGED
@@ -244,6 +244,13 @@ const COMMAND_HELP: Record<
244
244
  options: " -h, --help Show this help",
245
245
  examples: ` ${binName} hook`,
246
246
  },
247
+ "codex-hook": {
248
+ usage: `${binName} codex-hook`,
249
+ description:
250
+ "Handle Codex CLI hook events from stdin (steering delivery).\nUsed internally by the codex hooks registered in the worker image.",
251
+ options: " -h, --help Show this help",
252
+ examples: ` ${binName} codex-hook`,
253
+ },
247
254
  artifact: {
248
255
  usage: `${binName} artifact <subcommand> [options]`,
249
256
  description: "Manage agent artifacts (serve, list, etc.).",
@@ -383,6 +390,7 @@ function printHelp(command?: string) {
383
390
  ["api", "Start the API + MCP HTTP server"],
384
391
  ["claude", "Run Claude CLI"],
385
392
  ["hook", "Handle Claude Code hook events (stdin)"],
393
+ ["codex-hook", "Handle Codex CLI hook events (stdin, steering delivery)"],
386
394
  ["artifact", "Manage agent artifacts"],
387
395
  ["x", "Execute external command routes"],
388
396
  ["scripts", "Reusable scripts maintenance"],
@@ -672,6 +680,10 @@ if (args.showHelp || args.command === "help" || args.command === undefined) {
672
680
  } else if (args.command === "hook") {
673
681
  const { runHook } = await import("./commands/hook.ts");
674
682
  await runHook();
683
+ } else if (args.command === "codex-hook") {
684
+ const { handleCodexHook } = await import("./hooks/codex-hook.ts");
685
+ await handleCodexHook();
686
+ process.exit(0);
675
687
  } else if (args.command === "artifact") {
676
688
  // Pass all args after "artifact" directly
677
689
  const artifactArgs = process.argv.slice(process.argv.indexOf("artifact") + 1);
@@ -21,6 +21,7 @@ import {
21
21
  } from "../prompts/defaults.ts";
22
22
  import { renderMemoriesPrompt } from "../prompts/memories.ts";
23
23
  import { configureHttpResolver, resolveTemplateAsync } from "../prompts/resolver.ts";
24
+ import { renderSteeringDelivery } from "../prompts/steering-delivery.ts";
24
25
  import { authJsonToCredentialSelection } from "../providers/codex-oauth/auth-json.js";
25
26
  import { materializeCodexAuthJson } from "../providers/codex-oauth/auth-json-fs.js";
26
27
  import { loadAllCodexOAuthSlots } from "../providers/codex-oauth/storage.js";
@@ -486,29 +487,10 @@ export function createSteeringDispatchState(): SteeringDispatchState {
486
487
  };
487
488
  }
488
489
 
489
- /**
490
- * Wrap a steering body in the registered delivery envelope so the injected
491
- * message carries its own ID. `accept-steer` takes that ID, and it is the only
492
- * route to `handled` — without the envelope the agent obeys the message but
493
- * can never acknowledge it, and the row sits at `delivered` forever.
494
- *
495
- * Falls back to the bare body if template resolution fails: delivering an
496
- * un-acknowledgeable message beats not delivering one at all.
497
- */
498
- export async function renderSteeringDelivery(
499
- steeringMessageId: string,
500
- body: string,
501
- ): Promise<string> {
502
- try {
503
- const result = await resolveTemplateAsync("system.agent.steering.delivery", {
504
- steeringMessageId,
505
- body,
506
- });
507
- return result.skipped || !result.text.trim() ? body : result.text;
508
- } catch {
509
- return body;
510
- }
511
- }
490
+ // Moved to src/prompts/steering-delivery.ts so the codex-hook can render the
491
+ // same envelope without importing the whole runner. Re-exported for existing
492
+ // consumers (steering-transport tests).
493
+ export { renderSteeringDelivery };
512
494
 
513
495
  export async function pollAndDispatchSteering(
514
496
  config: ApiConfig,
@@ -518,6 +500,12 @@ export async function pollAndDispatchSteering(
518
500
  fetchImpl: typeof fetch = fetch,
519
501
  ): Promise<void> {
520
502
  if (!isSteeringEnabled()) return;
503
+ // Harness-side delivery (codex hooks): the hook polls pending rows and
504
+ // injects them itself. Dispatching here would race it into a false
505
+ // "Provider session does not support live steering" undeliverable →
506
+ // premature promotion. Leave the rows pending; the terminal sweep still
507
+ // promotes anything the hook never delivered.
508
+ if (session.steeringDeliveredExternally) return;
521
509
 
522
510
  const headers = {
523
511
  Authorization: `Bearer ${config.apiKey}`,
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Codex lifecycle hook — harness-side steering delivery.
5
+ *
6
+ * Codex has no in-process steering channel: `@openai/codex-sdk` drives
7
+ * `codex exec` with stdin written once and closed, and the app-server's
8
+ * native `turn/steer` is not reachable through the SDK (issue #1034). So
9
+ * delivery happens here instead: this hook runs inside the codex lifecycle
10
+ * (registered for SessionStart / PostToolUse / Stop via the managed
11
+ * `/etc/codex/requirements.toml` in the worker image), polls the API for
12
+ * pending steering rows, marks them `delivered`, and injects the rendered
13
+ * envelope into the model's context:
14
+ *
15
+ * - SessionStart / PostToolUse → `hookSpecificOutput.additionalContext`
16
+ * (verified to reach the model at codex-cli 0.146.0; PreToolUse drops
17
+ * additionalContext upstream, so it is deliberately not registered).
18
+ * - Stop → `{"decision":"block","reason":...}` so a session about to end
19
+ * still receives the message and continues to act on it.
20
+ *
21
+ * One-shot guarantee: a message is included in hook output only after its
22
+ * `/delivered` POST succeeded, so a message is injected at most once. A
23
+ * failed POST leaves the row `pending` for the next lifecycle event; rows a
24
+ * dying session never picks up are promoted by the server's terminal sweep.
25
+ * The runner's dispatch poll skips codex sessions entirely
26
+ * (`ProviderSession.steeringDeliveredExternally`).
27
+ */
28
+
29
+ import { renderSteeringDelivery } from "../prompts/steering-delivery.ts";
30
+ import type { SteeringMessage } from "../types";
31
+ import { getApiKey } from "../utils/api-key";
32
+ import { getMcpBaseUrl } from "../utils/constants";
33
+ import { isSteeringEnabled } from "../utils/steering-enabled";
34
+
35
+ const FETCH_TIMEOUT_MS = 5_000;
36
+
37
+ export interface CodexHookMessage {
38
+ hook_event_name?: string;
39
+ stop_hook_active?: boolean;
40
+ }
41
+
42
+ export interface CodexHookConfig {
43
+ apiUrl: string;
44
+ apiKey: string;
45
+ agentId: string;
46
+ }
47
+
48
+ export function resolveCodexHookConfig(
49
+ env: Record<string, string | undefined> = process.env,
50
+ ): CodexHookConfig | null {
51
+ const agentId = env.AGENT_ID;
52
+ const apiKey = getApiKey(env);
53
+ const apiUrl = getMcpBaseUrl();
54
+ if (!agentId || !apiKey || !apiUrl) return null;
55
+ return { apiUrl, apiKey, agentId };
56
+ }
57
+
58
+ /**
59
+ * Fetch the agent's pending steering rows, mark each `delivered`, and return
60
+ * the rendered envelopes of the ones whose delivered-POST succeeded.
61
+ *
62
+ * Any error yields an empty result — a hook must never take the harness down
63
+ * over steering, and `pending` rows are retried on the next lifecycle event.
64
+ */
65
+ export async function collectDeliverableSteering(
66
+ config: CodexHookConfig,
67
+ fetchImpl: typeof fetch = fetch,
68
+ ): Promise<string[]> {
69
+ const headers = {
70
+ Authorization: `Bearer ${config.apiKey}`,
71
+ "X-Agent-ID": config.agentId,
72
+ };
73
+
74
+ let messages: SteeringMessage[];
75
+ try {
76
+ const response = await fetchImpl(`${config.apiUrl}/api/steering-messages`, {
77
+ headers,
78
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
79
+ });
80
+ if (!response.ok) return [];
81
+ const data = (await response.json()) as { messages?: SteeringMessage[] };
82
+ messages = (data.messages ?? []).filter((message) => message.status === "pending");
83
+ } catch {
84
+ return [];
85
+ }
86
+
87
+ const delivered: string[] = [];
88
+ for (const message of messages) {
89
+ // Mark delivered BEFORE injecting: if the POST fails the row stays
90
+ // pending and is retried later; the inverse order could inject the same
91
+ // message on every tool call ("no polluting").
92
+ try {
93
+ const report = await fetchImpl(
94
+ `${config.apiUrl}/api/steering-messages/${encodeURIComponent(message.id)}/delivered`,
95
+ {
96
+ method: "POST",
97
+ headers: { ...headers, "Content-Type": "application/json" },
98
+ body: JSON.stringify({ mode: "queue" }),
99
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
100
+ },
101
+ );
102
+ if (!report.ok) continue;
103
+ } catch {
104
+ continue;
105
+ }
106
+ delivered.push(await renderSteeringDelivery(message.id, message.body));
107
+ }
108
+ return delivered;
109
+ }
110
+
111
+ /**
112
+ * Handle one codex hook event. Returns the JSON object to print to stdout
113
+ * (codex reads hook responses from stdout), or null for no output.
114
+ */
115
+ export async function handleCodexHookEvent(
116
+ msg: CodexHookMessage,
117
+ config: CodexHookConfig | null,
118
+ env: Record<string, string | undefined> = process.env,
119
+ fetchImpl: typeof fetch = fetch,
120
+ ): Promise<Record<string, unknown> | null> {
121
+ const event = msg.hook_event_name;
122
+ if (!event || !config || !isSteeringEnabled(env)) return null;
123
+ if (event !== "SessionStart" && event !== "PostToolUse" && event !== "Stop") return null;
124
+
125
+ const envelopes = await collectDeliverableSteering(config, fetchImpl);
126
+ if (envelopes.length === 0) return null;
127
+ const additionalContext = envelopes.join("\n\n");
128
+
129
+ if (event === "Stop") {
130
+ // Delivered rows are marked before this block fires, so the next Stop
131
+ // finds nothing pending and lets the session end — no block loop.
132
+ return {
133
+ decision: "block",
134
+ reason: `Steering message(s) arrived before you finished:\n\n${additionalContext}`,
135
+ };
136
+ }
137
+
138
+ return {
139
+ hookSpecificOutput: { hookEventName: event, additionalContext },
140
+ };
141
+ }
142
+
143
+ /** stdin/stdout entry point used by the `codex-hook` CLI command. */
144
+ export async function handleCodexHook(): Promise<void> {
145
+ let msg: CodexHookMessage;
146
+ try {
147
+ msg = (await Bun.stdin.json()) as CodexHookMessage;
148
+ } catch {
149
+ return;
150
+ }
151
+ try {
152
+ const output = await handleCodexHookEvent(msg, resolveCodexHookConfig());
153
+ if (output) console.log(JSON.stringify(output));
154
+ } catch {
155
+ // Never fail the harness over steering delivery.
156
+ }
157
+ }
package/src/http/kv.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  listKv,
12
12
  upsertKv,
13
13
  } from "../be/db";
14
+ import { mcpOverflowAuthError } from "../kv-overflow";
14
15
  import { can } from "../rbac";
15
16
  import { agentContextKey, pageContextKey } from "../tasks/context-key";
16
17
  import { KvKeySchema, KvNamespaceSchema, KvValueTypeSchema } from "../types";
@@ -321,6 +322,10 @@ function authorizeWrite(
321
322
  namespace: string,
322
323
  ctx: AuthCtx,
323
324
  ): { status: number; message: string } | null {
325
+ const overflowAuthErr = mcpOverflowAuthError(namespace, ctx.callerAgentId);
326
+ if (overflowAuthErr) {
327
+ return { status: 403, message: overflowAuthErr };
328
+ }
324
329
  if (namespace.startsWith("task:page:")) {
325
330
  if (!ctx.hasPageHeader) {
326
331
  return { status: 403, message: "task:page:* writes require a page-proxy request" };
@@ -349,6 +354,14 @@ function authorizeWrite(
349
354
  return null;
350
355
  }
351
356
 
357
+ function authorizeRead(
358
+ namespace: string,
359
+ ctx: AuthCtx,
360
+ ): { status: number; message: string } | null {
361
+ const overflowAuthErr = mcpOverflowAuthError(namespace, ctx.callerAgentId);
362
+ return overflowAuthErr ? { status: 403, message: overflowAuthErr } : null;
363
+ }
364
+
352
365
  function encodeValueOrError(
353
366
  res: ServerResponse,
354
367
  value: unknown,
@@ -435,7 +448,7 @@ export async function handleKv(
435
448
  if (!ns) return true;
436
449
  const key = decodeKvSegment(res, parsed.params.key, "key");
437
450
  if (!key) return true;
438
- return sendGet(res, ns, key);
451
+ return sendGet(req, res, ns, key);
439
452
  }
440
453
  if (putKvExplicit.match(req.method, pathSegments)) {
441
454
  if (enforceContentLengthCap(req, res, MAX_KV_BODY_BYTES) === BODY_TOO_LARGE) return true;
@@ -461,7 +474,7 @@ export async function handleKv(
461
474
  if (!parsed) return true;
462
475
  const ns = decodeKvSegment(res, parsed.params.namespace, "namespace");
463
476
  if (!ns) return true;
464
- return sendList(res, ns, parsed.query);
477
+ return sendList(req, res, ns, parsed.query);
465
478
  }
466
479
 
467
480
  // ── Header-resolved variants ──────────────────────────────────────────────
@@ -475,7 +488,7 @@ export async function handleKv(
475
488
  }
476
489
  const key = decodeKvSegment(res, parsed.params.key, "key");
477
490
  if (!key) return true;
478
- return sendGet(res, ns, key);
491
+ return sendGet(req, res, ns, key);
479
492
  }
480
493
  if (putKvHeader.match(req.method, pathSegments)) {
481
494
  if (enforceContentLengthCap(req, res, MAX_KV_BODY_BYTES) === BODY_TOO_LARGE) return true;
@@ -510,16 +523,16 @@ export async function handleKv(
510
523
  jsonError(res, "namespace is required (pass X-Source-Task-Id or X-Agent-ID)", 400);
511
524
  return true;
512
525
  }
513
- return sendList(res, ns, parsed.query);
526
+ return sendList(req, res, ns, parsed.query);
514
527
  }
515
528
 
516
529
  return false;
517
530
  }
518
531
 
519
532
  /**
520
- * Reads only ever resolve from headers there's no auth distinction between
521
- * reading your own ns and someone else's (any authenticated caller may read
522
- * any namespace).
533
+ * Reads resolve from headers. Ordinary KV namespaces remain cross-agent
534
+ * readable by design; `sendGet`/`sendList` separately enforce private
535
+ * ownership for the internal `mcp:overflow:<agentId>` namespace family.
523
536
  */
524
537
  function resolveNamespaceForRead(req: IncomingMessage): string | null {
525
538
  return resolveNamespaceFromHeaders(req);
@@ -591,7 +604,17 @@ async function handleIncr(
591
604
  return true;
592
605
  }
593
606
 
594
- function sendGet(res: ServerResponse, namespace: string, key: string): boolean {
607
+ function sendGet(
608
+ req: IncomingMessage,
609
+ res: ServerResponse,
610
+ namespace: string,
611
+ key: string,
612
+ ): boolean {
613
+ const authErr = authorizeRead(namespace, buildAuthCtx(req));
614
+ if (authErr) {
615
+ jsonError(res, authErr.message, authErr.status);
616
+ return true;
617
+ }
595
618
  const entry = getKv(namespace, key);
596
619
  if (!entry) {
597
620
  jsonError(res, "not found", 404);
@@ -661,10 +684,16 @@ function sendDelete(
661
684
  }
662
685
 
663
686
  function sendList(
687
+ req: IncomingMessage,
664
688
  res: ServerResponse,
665
689
  namespace: string,
666
690
  query: z.infer<typeof kvListQuerySchema>,
667
691
  ): boolean {
692
+ const authErr = authorizeRead(namespace, buildAuthCtx(req));
693
+ if (authErr) {
694
+ jsonError(res, authErr.message, authErr.status);
695
+ return true;
696
+ }
668
697
  const limit = Math.min(query.limit ?? 100, MAX_KV_LIST_LIMIT);
669
698
  const offset = query.offset ?? 0;
670
699
  const prefix = query.prefix && query.prefix.length > 0 ? query.prefix : undefined;
@@ -3,6 +3,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { z } from "zod";
4
4
  import { createServer } from "@/server";
5
5
  import { isMcpToolAllowedForScripts } from "../scripts-runtime/sdk-allowlist";
6
+ import { markScriptSdkRequestOrigin } from "../tools/utils";
6
7
  import { route } from "./route-def";
7
8
  import { json, jsonError } from "./utils";
8
9
 
@@ -83,7 +84,7 @@ export async function handleMcpBridge(
83
84
  ? req.headers["x-source-task-id"][0]
84
85
  : (req.headers["x-source-task-id"] as string | undefined);
85
86
 
86
- const extra = {
87
+ const extra = markScriptSdkRequestOrigin({
87
88
  sessionId: "mcp-bridge",
88
89
  requestInfo: {
89
90
  headers: {
@@ -91,7 +92,7 @@ export async function handleMcpBridge(
91
92
  ...(sourceTaskId ? { "x-source-task-id": sourceTaskId } : {}),
92
93
  },
93
94
  },
94
- };
95
+ });
95
96
 
96
97
  try {
97
98
  const result = tool.inputSchema