create-walle 0.9.25 → 0.9.26

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 (179) hide show
  1. package/README.md +8 -0
  2. package/bin/create-walle.js +815 -45
  3. package/package.json +2 -2
  4. package/template/bin/ctm-dev-cleanup.js +90 -4
  5. package/template/bin/ctm-launch.sh +49 -1
  6. package/template/bin/dev.sh +45 -1
  7. package/template/bin/ensure-stable-node.js +132 -0
  8. package/template/bin/install-service.sh +9 -0
  9. package/template/claude-task-manager/api-prompts.js +899 -119
  10. package/template/claude-task-manager/approval-agent.js +360 -40
  11. package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
  12. package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
  13. package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
  14. package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
  15. package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
  16. package/template/claude-task-manager/db.js +399 -48
  17. package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
  18. package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
  19. package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
  20. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
  21. package/template/claude-task-manager/lib/approval-hook.js +200 -0
  22. package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
  23. package/template/claude-task-manager/lib/auth-rules.js +11 -0
  24. package/template/claude-task-manager/lib/background-llm.js +32 -4
  25. package/template/claude-task-manager/lib/codesign-identity.js +140 -0
  26. package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
  27. package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
  28. package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
  29. package/template/claude-task-manager/lib/codex-paths.js +73 -0
  30. package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
  31. package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
  32. package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
  33. package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
  34. package/template/claude-task-manager/lib/command-targets.js +163 -0
  35. package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
  36. package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
  37. package/template/claude-task-manager/lib/escalation-review.js +80 -3
  38. package/template/claude-task-manager/lib/flow-control.js +52 -0
  39. package/template/claude-task-manager/lib/fs-watcher.js +24 -15
  40. package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
  41. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
  42. package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
  43. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
  44. package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
  45. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
  46. package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
  47. package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
  48. package/template/claude-task-manager/lib/perf-tracker.js +29 -2
  49. package/template/claude-task-manager/lib/permission-match.js +146 -16
  50. package/template/claude-task-manager/lib/project-slug.js +33 -0
  51. package/template/claude-task-manager/lib/prompt-intent.js +51 -4
  52. package/template/claude-task-manager/lib/read-pool-client.js +48 -3
  53. package/template/claude-task-manager/lib/real-node.js +73 -0
  54. package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
  55. package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
  56. package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
  57. package/template/claude-task-manager/lib/session-history.js +5 -7
  58. package/template/claude-task-manager/lib/session-host-manager.js +19 -0
  59. package/template/claude-task-manager/lib/session-jobs.js +6 -0
  60. package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
  61. package/template/claude-task-manager/lib/session-messages-page.js +211 -0
  62. package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
  63. package/template/claude-task-manager/lib/session-standup.js +8 -0
  64. package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
  65. package/template/claude-task-manager/lib/session-token-usage.js +30 -8
  66. package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
  67. package/template/claude-task-manager/lib/storage-migration.js +2 -1
  68. package/template/claude-task-manager/lib/transcript-store.js +179 -12
  69. package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
  70. package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
  71. package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
  72. package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
  73. package/template/claude-task-manager/package.json +5 -2
  74. package/template/claude-task-manager/prompt-harvest.js +31 -11
  75. package/template/claude-task-manager/providers/claude-code.js +29 -1
  76. package/template/claude-task-manager/providers/codex.js +13 -1
  77. package/template/claude-task-manager/public/css/setup.css +11 -0
  78. package/template/claude-task-manager/public/css/walle-session.css +132 -4
  79. package/template/claude-task-manager/public/css/walle.css +89 -0
  80. package/template/claude-task-manager/public/icon-16.png +0 -0
  81. package/template/claude-task-manager/public/icon-32.png +0 -0
  82. package/template/claude-task-manager/public/icon-512.png +0 -0
  83. package/template/claude-task-manager/public/index.html +2483 -165
  84. package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
  85. package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
  86. package/template/claude-task-manager/public/js/message-renderer.js +60 -1
  87. package/template/claude-task-manager/public/js/prompts.js +13 -1
  88. package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
  89. package/template/claude-task-manager/public/js/setup.js +54 -10
  90. package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
  91. package/template/claude-task-manager/public/js/stream-view.js +78 -0
  92. package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
  93. package/template/claude-task-manager/public/js/tool-state.js +155 -0
  94. package/template/claude-task-manager/public/js/walle-session.js +887 -326
  95. package/template/claude-task-manager/public/js/walle.js +306 -195
  96. package/template/claude-task-manager/public/m/app.css +1 -0
  97. package/template/claude-task-manager/public/m/app.js +33 -3
  98. package/template/claude-task-manager/queue-engine.js +45 -1
  99. package/template/claude-task-manager/server.js +3367 -540
  100. package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
  101. package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
  102. package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
  103. package/template/claude-task-manager/workers/session-host-process.js +10 -0
  104. package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
  105. package/template/package.json +2 -3
  106. package/template/shared/icons/AppIcon-ctm.icns +0 -0
  107. package/template/shared/icons/AppIcon-walle.icns +0 -0
  108. package/template/wall-e/agent.js +139 -18
  109. package/template/wall-e/api-walle.js +201 -22
  110. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
  111. package/template/wall-e/brain.js +1049 -39
  112. package/template/wall-e/chat.js +427 -86
  113. package/template/wall-e/coding/acceptance-contract.js +26 -1
  114. package/template/wall-e/coding/action-memory-policy.js +353 -0
  115. package/template/wall-e/coding/action-memory-store.js +814 -0
  116. package/template/wall-e/coding/initial-messages.js +197 -0
  117. package/template/wall-e/coding/no-progress-guard.js +327 -0
  118. package/template/wall-e/coding/permission-service.js +88 -22
  119. package/template/wall-e/coding/session-workspaces.js +81 -0
  120. package/template/wall-e/coding/shell-sandbox.js +124 -0
  121. package/template/wall-e/coding/stream-processor.js +63 -2
  122. package/template/wall-e/coding/tool-execution-controller.js +14 -1
  123. package/template/wall-e/coding/tool-registry.js +1 -1
  124. package/template/wall-e/coding/transcript-writer.js +3 -0
  125. package/template/wall-e/coding-orchestrator.js +636 -35
  126. package/template/wall-e/coding-prompts.js +51 -2
  127. package/template/wall-e/docs/model-routing-policy.md +59 -0
  128. package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
  129. package/template/wall-e/extraction/knowledge-extractor.js +76 -23
  130. package/template/wall-e/http/chat-api.js +30 -12
  131. package/template/wall-e/http/model-admin.js +93 -1
  132. package/template/wall-e/lib/background-lanes.js +133 -0
  133. package/template/wall-e/lib/boot-profile.js +11 -0
  134. package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
  135. package/template/wall-e/lib/brain-read-pool-client.js +311 -0
  136. package/template/wall-e/lib/diagnostics-flags.js +87 -0
  137. package/template/wall-e/lib/event-loop-monitor.js +74 -3
  138. package/template/wall-e/lib/mcp-integration.js +7 -1
  139. package/template/wall-e/lib/real-node.js +98 -0
  140. package/template/wall-e/lib/runtime-health.js +206 -0
  141. package/template/wall-e/lib/runtime-worker-pool.js +101 -0
  142. package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
  143. package/template/wall-e/lib/scheduler.js +446 -17
  144. package/template/wall-e/lib/service-health.js +61 -2
  145. package/template/wall-e/lib/service-readiness.js +258 -0
  146. package/template/wall-e/lib/usage.js +152 -0
  147. package/template/wall-e/lib/worker-thread-pool.js +389 -0
  148. package/template/wall-e/llm/client.js +81 -4
  149. package/template/wall-e/llm/default-fallback.js +54 -8
  150. package/template/wall-e/llm/mlx.js +536 -73
  151. package/template/wall-e/llm/mlx.plugin.json +1 -1
  152. package/template/wall-e/llm/ollama.js +342 -43
  153. package/template/wall-e/llm/provider-error.js +18 -1
  154. package/template/wall-e/llm/provider-health-state.js +176 -0
  155. package/template/wall-e/llm/routing-policy.js +796 -0
  156. package/template/wall-e/llm/supported-models.js +5 -0
  157. package/template/wall-e/loops/tasks.js +60 -14
  158. package/template/wall-e/loops/think.js +89 -24
  159. package/template/wall-e/mcp-server.js +192 -28
  160. package/template/wall-e/server.js +32 -7
  161. package/template/wall-e/skills/script-skill-runner.js +8 -1
  162. package/template/wall-e/skills/skill-planner.js +64 -1
  163. package/template/wall-e/tools/builtin-middleware.js +67 -2
  164. package/template/wall-e/tools/local-tools.js +116 -26
  165. package/template/wall-e/tools/permission-checker.js +52 -4
  166. package/template/wall-e/tools/permission-rules.js +36 -0
  167. package/template/wall-e/tools/shell-analyzer.js +46 -1
  168. package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
  169. package/template/wall-e/training/real-trajectory-miner.js +2617 -0
  170. package/template/wall-e/training/replay-eval-analysis.js +151 -0
  171. package/template/wall-e/training/run-shell-command-selector.js +277 -0
  172. package/template/wall-e/training/tool-sft-dataset.js +312 -0
  173. package/template/wall-e/training/tool-sft-renderers.js +144 -0
  174. package/template/wall-e/training/tool-trace-harvester.js +1440 -0
  175. package/template/wall-e/training/trajectory-action-selector.js +364 -0
  176. package/template/wall-e/weather-runtime.js +232 -0
  177. package/template/wall-e/workers/brain-owner-worker.js +162 -0
  178. package/template/wall-e/workers/brain-read-worker.js +148 -0
  179. package/template/wall-e/workers/runtime-worker.js +145 -0
@@ -0,0 +1,84 @@
1
+ # Tier 3: containment via Claude's native Bash sandbox (Phase 4)
2
+
3
+ ## Why this shape (not CTM-side sandboxing)
4
+
5
+ The research consensus is that approval prompts should be the *escalation path out of a
6
+ sandbox*, not the primary defense — Anthropic's own sandboxing cut permission prompts
7
+ ~84%. But **CTM does not execute the agent's tool commands** — Claude does, in its own
8
+ process — so CTM cannot wrap individual command execution from outside.
9
+
10
+ The clean, low-risk lever is **Claude Code's built-in OS-level Bash sandbox** (macOS
11
+ Seatbelt; Linux bubblewrap+socat), configured through `settings.json` — which CTM
12
+ *already injects* for hook-governed sessions. So Phase 4 just turns that sandbox on for
13
+ CTM sessions. Containment is enforced by the OS on the running process, so it holds
14
+ even when an auto-approved command does more than its name suggests — exactly the
15
+ defense-in-depth the allowlist alone cannot give.
16
+
17
+ It even fits CTM's worktree workflow: Claude's sandbox special-cases linked git
18
+ worktrees (it allows writes to the main repo's shared `.git` so `git commit` works).
19
+
20
+ ## What shipped
21
+
22
+ - `buildClaudeHookSettings(port, { sandbox })`: when `sandbox` is truthy, the injected
23
+ `--settings` also carries `sandbox` — `true` → `{ enabled: true }` (safe default:
24
+ keeps the unsandboxed-fallback escape hatch, warns-not-fails if the sandbox can't
25
+ start), or a full sandbox config object passed through verbatim.
26
+ - server.js: gated on a **separate** env flag `CTM_APPROVAL_HOOK_SANDBOX=1` (in addition
27
+ to `CTM_APPROVAL_HOOK=1`). **Default OFF.**
28
+
29
+ ## Why a separate, default-off flag
30
+
31
+ Enabling the sandbox changes execution semantics and can add friction:
32
+ - **Network**: no domains are pre-allowed; the first time a command needs a new domain
33
+ it prompts (which, for us, routes back through the hook). A build that fetches from
34
+ many hosts will generate approvals until the domains are allow-listed via
35
+ `sandbox.network.allowedDomains`.
36
+ - **Writes outside cwd**: tools that write to `~/.kube`, `~/.cache`, etc. need
37
+ `sandbox.filesystem.allowWrite` entries or they fail (then fall back / retry
38
+ unsandboxed via the escape hatch).
39
+ - **Known-incompatible tools** (`docker`, `watchman`/`jest --watch`, some Go CLIs on
40
+ macOS TLS) need `excludedCommands`.
41
+
42
+ So this must be validated against the user's real projects before it goes on the live
43
+ workbench. The default-off flag makes it explicit opt-in, exactly like `CTM_APPROVAL_HOOK`.
44
+
45
+ ## Validated (2026-06-12)
46
+
47
+ Confirmed on this machine that Claude's Seatbelt sandbox **contains writes**: a
48
+ `touch` to the home directory was blocked ("file writes restricted to the allowed
49
+ working directory"), while writes to the cwd succeed. Notably this held even *without*
50
+ CTM's flag — Claude Code 2.1.x already confines Bash writes to the cwd here (default /
51
+ org policy). So on this machine the `CTM_APPROVAL_HOOK_SANDBOX` flag is **redundant**
52
+ (the underlying sandbox is already active) and was intentionally **not** enabled on the
53
+ primary — flipping a redundant flag via a disruptive restart buys nothing. The flag's
54
+ value is for environments where Claude's sandbox is **not** already on; the capability
55
+ is built, tested, and proven to work.
56
+
57
+ ## How to enable + validate
58
+
59
+ 1. On `/ctm-dev` (or a throwaway session), set both flags:
60
+ `CTM_APPROVAL_HOOK=1 CTM_APPROVAL_HOOK_SANDBOX=1`.
61
+ 2. Drive a real build / test / git workflow; confirm commands run (Seatbelt contains
62
+ them) and that legitimate network/write needs are either allowed by default (cwd +
63
+ temp + worktree `.git`) or surfaced as approvals you can allow-list.
64
+ 3. Add project-specific `sandbox.filesystem.allowWrite` / `sandbox.network.allowedDomains`
65
+ / `excludedCommands` as needed (a future increment can let CTM compose these per
66
+ project). Then flip the env flag on the primary (launchd plist) like CTM_APPROVAL_HOOK.
67
+
68
+ ## Hardening options (not enabled by default — they can break legit use)
69
+
70
+ - `sandbox.filesystem.denyRead: ["~/.ssh", "~/.aws"]` — the default read policy still
71
+ allows reading credential files; deny them if the agent never needs to. (aws/gcloud
72
+ CLIs read `~/.aws`/`~/.config/gcloud`, so denying breaks them — scope carefully.)
73
+ - `sandbox.allowUnsandboxedCommands: false` (strict) — removes the escape hatch so a
74
+ command can't be retried outside the sandbox. Strong, but turns "incompatible tool"
75
+ into a hard failure; pair with `excludedCommands`.
76
+ - `sandbox.failIfUnavailable: true` — refuse to start unsandboxed (managed-deployment
77
+ gate). Default false (warn + run unsandboxed) is the safer interactive default.
78
+
79
+ ## Reference
80
+
81
+ Claude Code sandbox docs: https://code.claude.com/docs/en/sandboxing and the
82
+ `sandbox.*` keys in https://code.claude.com/docs/en/settings#sandbox-settings.
83
+ The same primitives are the standalone `@anthropic-ai/sandbox-runtime` (used to wrap
84
+ the *whole* agent process — a heavier alternative we deliberately did not take).
@@ -0,0 +1,72 @@
1
+ # Codex app-server approval bridge (Phase 3)
2
+
3
+ ## Why this is a *bridge foundation*, not a drop-in like Claude hooks
4
+
5
+ Claude Code lets an **external** process (CTM) answer approvals via a PreToolUse HTTP
6
+ hook **while the user keeps using the TUI**. Codex has **no equivalent TUI-compatible
7
+ hook**. Its programmatic surface is `codex app-server` (JSON-RPC over stdio), and the
8
+ Codex TUI *is itself* an app-server client. So routing Codex approvals through CTM's
9
+ classifier requires CTM to **be** the app-server client — i.e. own the conversation —
10
+ which means a *CTM-driven Codex mode* (CTM renders the turn/items), not an interception
11
+ layer under the existing PTY TUI.
12
+
13
+ That is a larger product decision than the Claude work. This phase therefore delivers
14
+ the **correctness-critical protocol layer** — tested and validated against the real
15
+ binary — so the driven-mode wiring is a straightforward follow-on, not a research task.
16
+
17
+ ## What shipped
18
+
19
+ - `lib/codex-approval-bridge.js` — pure mapping (unit-tested, 13 cases):
20
+ - `parseCodexApprovalRequest(method, params)` normalizes the **five** server→client
21
+ approval methods to `{ kind, protocol, context }` for the shared `decideApproval`
22
+ cascade. Handles both protocol generations the 0.132.0 binary emits:
23
+ - **v2:** `item/commandExecution/requestApproval`, `item/fileChange/requestApproval`,
24
+ `item/permissions/requestApproval`
25
+ - **legacy:** `execCommandApproval`, `applyPatchApproval`
26
+ - `buildCodexApprovalResponse(decision, protocol)` emits the right vocabulary:
27
+ v2 camelCase `accept|decline|cancel`; legacy snake_case `approved|denied|abort`.
28
+ - `decisionFromVerdict(verdict)` maps a `decideApproval` result → `allow|deny|abort`
29
+ (blocklist → `abort` = deny + interrupt the turn).
30
+ - Real-binary validation: the `codex app-server` handshake (`initialize` → `initialized`)
31
+ was driven against codex-cli **0.132.0** and returns the documented result
32
+ (`userAgent, codexHome, platformFamily, platformOs`). The protocol layer is correct
33
+ against the shipping binary.
34
+
35
+ ## What also shipped: the driver engine
36
+
37
+ - `lib/codex-app-server-client.js` (unit-tested, 10 cases): the stdio JSON-RPC client.
38
+ - `spawnCodexTransport()` spawns `codex app-server` and frames newline-delimited JSON;
39
+ a `transport` injection makes the dispatch testable without a real process.
40
+ - `createCodexAppServerClient({ transport, decide, onItem, onNotification, onApproval })`
41
+ does the handshake (`initialize` → `initialized`), `startThread` (defaults route
42
+ approvals to us: `approvalPolicy:"on-request"`, `approvalsReviewer:"user"`,
43
+ `sandbox:"workspace-write"`), `startTurn`, id-keyed request/response correlation, and
44
+ dispatches server-initiated approval requests through the bridge to the async
45
+ `decide(context, kind)` callback — answering keyed by the request id. Fail-closed:
46
+ `decide` throwing → deny; unknown server requests get an empty result so the agent
47
+ never wedges.
48
+ - Validated **end-to-end against codex-cli 0.132.0**: the client drives a real
49
+ `initialize → thread/start → turn/start` and answers a live approval request via the
50
+ bridge (see the e2e probe). The protocol engine works against the shipping binary.
51
+
52
+ ## Remaining (the product surface)
53
+
54
+ 1. **CTM-driven-Codex session type.** Wire the client into a CTM session: render the
55
+ `item/*` / `turn/*` stream (CTM owns the UI — reuse the structured renderer used for
56
+ Wall-E chat), route `decide` through the shared `decideApproval` cascade, and **park**
57
+ human-needed approvals exactly like the Claude hook (hold the open JSON-RPC request,
58
+ raise the existing escalation card, resolve from the local/remote answer; dangerous →
59
+ cancel).
60
+ 2. **Product decision:** opt-in CTM-driven Codex sessions vs. the current PTY TUI (which
61
+ keeps the scrape path). Until that ships, Codex TUI sessions keep the scrape approver.
62
+
63
+ The engine is also the right substrate for any **programmatic** Codex consumer (e.g. a
64
+ Wall-E coding backend), where there is no TUI and driven mode is the *only* mode — there
65
+ the remaining UI item (1) does not even apply.
66
+
67
+ ## Reference
68
+
69
+ Exact wire contract (regenerate from the installed binary):
70
+ `codex app-server generate-json-schema --out <dir>` → `ServerRequest.json`,
71
+ `v2/ThreadStartParams.json`, `CommandExecutionRequestApprovalParams.json`, etc.
72
+ See memory `ctm-approver-protocol-hook-prototype-validated` for the full protocol notes.
@@ -0,0 +1,47 @@
1
+ # Codex: native sandbox + on-request approvals (opt-in)
2
+
3
+ The cheap Codex win that does NOT require driven mode. When CTM spawns a Codex session
4
+ and `CTM_CODEX_SANDBOX=1`, CTM injects Codex's **own** sandbox + approval policy so it
5
+ auto-runs safe commands *inside its native OS sandbox* (Seatbelt on macOS, landlock on
6
+ Linux) and prompts far less — keeping the Codex TUI, no UI rebuild.
7
+
8
+ - Injected (prepended, before any subcommand): `--sandbox workspace-write
9
+ --ask-for-approval on-request`.
10
+ - **Never overrides an operator choice:** if the launch args already carry a sandbox /
11
+ approval / bypass / full-auto flag (or a `-c sandbox_mode=…` / `-c approval_policy=…`),
12
+ CTM leaves them untouched.
13
+ - Default **OFF** (`CTM_CODEX_SANDBOX` unset). Enable per-environment by setting the env
14
+ var (launchd plist on the primary, like `CTM_APPROVAL_HOOK`).
15
+
16
+ `lib/codex-sandbox-args.js` (`injectCodexSandboxArgs`) holds the pure logic (unit-tested);
17
+ server.js applies it at the Codex spawn in `handleCreate`. Flags validated against
18
+ codex-cli 0.132.0; the behavior was confirmed via the app-server e2e (workspace-write +
19
+ on-request auto-ran a shell command without prompting).
20
+
21
+ ## What this is NOT (read before enabling)
22
+
23
+ This reduces the *number* of Codex prompts and contains execution. It does **not**:
24
+ - route the remaining Codex approvals through CTM's classifier (blocklist / learned
25
+ rules / verifier) — those still appear as Codex's own TUI widget (the scrape path);
26
+ - give the unified "Needs You" card, remote/phone approval, or park-and-resolve for
27
+ Codex. **Only driven mode** (`docs/codex-app-server-approvals.md`) does that.
28
+
29
+ ## Cons / catches
30
+
31
+ - **More autonomous.** Codex auto-runs reads + in-workspace writes without asking
32
+ (contained by the sandbox). That's the point, but it's a behavior change — hence
33
+ opt-in, and we never override an operator who deliberately runs Codex cautiously.
34
+ - **Network is OFF** under `workspace-write`. Commands needing the network (`npm install`,
35
+ `git push`, `curl`) will fail-in-sandbox and escalate (→ prompt) unless you also enable
36
+ `[sandbox_workspace_write] network_access = true` in `~/.codex/config.toml` — which
37
+ weakens containment. No free lunch; default is the contained one.
38
+ - **Scrape path stays** for the remaining prompts (fewer of them, but not eliminated).
39
+ - **Codex-version-dependent** flags (`-s/--sandbox`, `-a/--ask-for-approval`) — stable in
40
+ 0.132.0.
41
+
42
+ ## To enable on the primary
43
+
44
+ Add `CTM_CODEX_SANDBOX=1` to the launchd plist `EnvironmentVariables` and bootout/bootstrap
45
+ (KeepAlive/restart won't re-read plist env). Validate against your real Codex workflows
46
+ first — a network-heavy project may want `network_access = true` or `--sandbox
47
+ danger-full-access` for specific sessions.
@@ -243,6 +243,15 @@ CREATE INDEX idx_chat_messages_edited_from ON chat_messages(edited_from_id);
243
243
  `chat_branches` can remain temporarily as a compatibility payload, but new code
244
244
  should prefer `chat_branch_heads` plus message parent links.
245
245
 
246
+ Current transition state: Wall-E sessions persist branch UI state through
247
+ `chat_branch_events` (append-only audit/event log) and `chat_branch_state`
248
+ (latest projection). The projection stores durable `chat_messages.id` references
249
+ for transcript messages and only stores inline message bodies for optimistic
250
+ client-only edits that do not have a durable row yet. Do not restore the old
251
+ behavior where CTM rewrites a full `{branches, active, history}` transcript blob
252
+ on every edit; that path hit request limits once images and long answers were in
253
+ the conversation.
254
+
246
255
  ### CTM Cache Tables
247
256
 
248
257
  Add nullable tree fields to `session_messages`:
@@ -347,6 +356,11 @@ Existing APIs:
347
356
  - `/api/wall-e/chat/history` should continue returning active-path messages.
348
357
  - `/api/wall-e/chat/branches` can remain as legacy compatibility, but should
349
358
  eventually be backed by first-class branch tables.
359
+ - `/api/wall-e/chat/branch-state` writes the compact ref projection and appends
360
+ an event. New CTM Wall-E session code should use this instead of posting a full
361
+ branch/history blob to `/api/wall-e/chat/branches`.
362
+ - `/api/wall-e/chat/branch-events` exposes the append-only event stream for
363
+ debugging and future incremental clients.
350
364
 
351
365
  ### CTM Session Messages
352
366
 
@@ -517,7 +531,10 @@ Backfill algorithm:
517
531
 
518
532
  ### Phase 5: Advanced Cleanup
519
533
 
520
- - Retire `chat_branches` JSON blob or make it a derived cache.
534
+ - Retire `chat_branches` JSON blob or make it a derived cache. The current
535
+ Wall-E browser UIs write compact state/events, so the remaining work is
536
+ migration/backfill and removing legacy API compatibility once old clients are
537
+ gone.
521
538
  - Add branch-aware memory ranking.
522
539
  - Add branch-aware export/import.
523
540
  - Add UI for branch rename/delete/hide.
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+ // Claude Code PreToolUse hook integration for CTM's auto-approver.
4
+ //
5
+ // Instead of screen-scraping the rendered approval widget and injecting keystrokes,
6
+ // CTM injects a PreToolUse HTTP hook into the Claude sessions it spawns (via
7
+ // `claude --settings '<json>'`). Claude then POSTs every governed tool call to CTM's
8
+ // endpoint with STRUCTURED input ({tool_name, tool_input:{command}, session_id, cwd}),
9
+ // and CTM answers with a permissionDecision. This eliminates the parse/quoting/clip/
10
+ // TOCTOU failure class of the scrape path; the literal command is handed to us.
11
+ //
12
+ // Proven against claude 2.1.173 (see memory ctm-approver-protocol-hook-prototype-validated):
13
+ // - permissionDecision "allow" suppresses the interactive widget entirely;
14
+ // - "ask" falls through to Claude's native prompt (and shows our reason);
15
+ // - a hook ERROR or TIMEOUT degrades to the native prompt (FAIL-CLOSED / safe);
16
+ // - `--settings` MERGES with the user's config rather than replacing it.
17
+ //
18
+ // This module holds the PURE pieces (settings builder + decision→output mapper) so
19
+ // they unit-test without a server. The route wiring lives in server.js.
20
+
21
+ // Which Claude tools the hook governs (Claude matches tool_name against this regex).
22
+ // Bash is where the auto-approval pain is; Edit/Write/Read/Glob/Grep are low-risk and
23
+ // our classifier auto-allows them; `mcp__.*` brings MCP tool calls under the same
24
+ // cascade (read-only → allow, mutating → human), so their widgets are suppressed too.
25
+ const DEFAULT_MATCHER = 'Bash|Edit|Write|Read|Glob|Grep|NotebookEdit|WebFetch|mcp__.*';
26
+
27
+ // Build the `--settings` JSON (string) that wires a PreToolUse HTTP hook back to CTM.
28
+ // `port` is CTM's own HTTP port; the hook posts to /api/approval/hook on it.
29
+ // timeoutSec is the per-hook budget — kept generous so a parked (awaiting-human)
30
+ // request resolves on the structured channel before Claude falls back to its widget.
31
+ function buildClaudeHookSettings(port, { matcher = DEFAULT_MATCHER, timeoutSec = 600, path = '/api/approval/hook', token = '', sandbox = false } = {}) {
32
+ const p = Number(port);
33
+ if (!Number.isFinite(p) || p <= 0) throw new Error('buildClaudeHookSettings: invalid port');
34
+ const hook = { type: 'http', url: `http://127.0.0.1:${p}${path}`, timeout: Math.max(1, Math.floor(timeoutSec)) };
35
+ // Phase 5: a per-session bearer token so only THIS CTM-spawned session can drive a
36
+ // decision through the (localhost, otherwise-unauthenticated) endpoint. Validated
37
+ // server-side against the session's stored token; a mismatch fails closed.
38
+ if (token) hook.headers = { Authorization: `Bearer ${token}` };
39
+ const settings = { hooks: { PreToolUse: [{ matcher, hooks: [hook] }] } };
40
+ // Phase 4 (Tier 3): enable Claude Code's built-in OS-level Bash sandbox (macOS
41
+ // Seatbelt / Linux bubblewrap) so an auto-approved command is CONTAINED — writes
42
+ // limited to the cwd + temp, network gated. CTM cannot sandbox execution it does not
43
+ // own; this turns on Claude's native sandbox via the settings we already inject.
44
+ // `sandbox === true` → safe default (enabled; keeps the unsandboxed-fallback escape
45
+ // hatch and warns-not-fails if unavailable). Or pass a full sandbox config object.
46
+ if (sandbox) settings.sandbox = (sandbox === true) ? { enabled: true } : sandbox;
47
+ return JSON.stringify(settings);
48
+ }
49
+
50
+ // Check that a request's Authorization header matches the session's expected hook
51
+ // token. A plain string compare (not constant-time) — the token only gates a
52
+ // same-machine, per-session approval decision over localhost, so a timing side channel
53
+ // is not in scope. Returns true when no token is required (backward-compatible:
54
+ // sessions launched before tokens, or non-hook sessions, are not rejected).
55
+ function hookTokenOk(expectedToken, authorizationHeader) {
56
+ if (!expectedToken) return true; // nothing to enforce
57
+ return String(authorizationHeader || '') === `Bearer ${expectedToken}`;
58
+ }
59
+
60
+ // Map our shared decideApproval() result to a Claude PreToolUse hook output object.
61
+ // decideApproval returns { decision: 'allow' | 'ask', decidedBy, riskLevel, reason, ... }.
62
+ // - 'allow' -> permissionDecision 'allow' (widget suppressed)
63
+ // - 'ask' from the blocklist -> permissionDecision 'deny' (hard refuse + tell the model why)
64
+ // - 'ask' otherwise (escalation) -> permissionDecision 'ask' (Phase 1a: native widget;
65
+ // Phase 1b parks instead and returns allow/deny)
66
+ // Returns the object Claude expects under hookSpecificOutput, plus our internal
67
+ // `_route` tag ('allow'|'deny'|'ask') so the caller can decide whether to park.
68
+ function mapDecisionToHookOutput(d) {
69
+ const decision = d && d.decision;
70
+ const reason = (d && d.reason) || '';
71
+ if (decision === 'allow') {
72
+ return { _route: 'allow', permissionDecision: 'allow', permissionDecisionReason: reason || 'Auto-approved by CTM' };
73
+ }
74
+ // Everything else is "not auto-approved". A blocklist hit is an objective refusal.
75
+ if (d && d.decidedBy === 'blocklist') {
76
+ return { _route: 'deny', permissionDecision: 'deny', permissionDecisionReason: reason || 'Blocked by CTM dangerous-command blocklist' };
77
+ }
78
+ return { _route: 'ask', permissionDecision: 'ask', permissionDecisionReason: reason || 'CTM routed this to manual review' };
79
+ }
80
+
81
+ // Wrap a hookSpecificOutput payload into the full hook response body.
82
+ function hookResponseBody(output) {
83
+ const { _route, ...rest } = output || {};
84
+ return { hookSpecificOutput: { hookEventName: 'PreToolUse', ...rest } };
85
+ }
86
+
87
+ // Map a Claude PreToolUse payload's tool_name + tool_input to the context object the
88
+ // approver cascade expects. Claude tools: Bash -> command; Edit/Write/Read -> file path.
89
+ const CLAUDE_TOOL_TO_CONTEXT = {
90
+ Bash: (ti) => ({ toolName: 'Bash command', command: String((ti && ti.command) || '') }),
91
+ Edit: (ti) => ({ toolName: 'Edit', command: String((ti && ti.file_path) || '') }),
92
+ Write: (ti) => ({ toolName: 'Write', command: String((ti && ti.file_path) || '') }),
93
+ NotebookEdit: (ti) => ({ toolName: 'NotebookEdit', command: String((ti && ti.notebook_path) || '') }),
94
+ Read: (ti) => ({ toolName: 'Read', command: String((ti && ti.file_path) || '') }),
95
+ Glob: (ti) => ({ toolName: 'Glob', command: String((ti && ti.pattern) || '') }),
96
+ Grep: (ti) => ({ toolName: 'Grep', command: String((ti && ti.pattern) || '') }),
97
+ WebFetch: (ti) => ({ toolName: 'WebFetch', command: String((ti && ti.url) || '') }),
98
+ };
99
+
100
+ // MCP tool calls (tool_name `mcp__<server>__<tool>`) carry their args in tool_input.
101
+ // Serialize a compact descriptor so reviewWithHeuristics (which keys on the tool name
102
+ // for read-only-vs-mutating) and the verifier both have the full call to judge.
103
+ function _mcpContext(toolName, toolInput) {
104
+ let args = '';
105
+ try { args = JSON.stringify(toolInput || {}); } catch { args = ''; }
106
+ if (args === '{}') args = '';
107
+ const command = args ? `${toolName} ${args}`.slice(0, 2000) : toolName;
108
+ return { toolName, command };
109
+ }
110
+
111
+ function contextFromHookPayload(payload) {
112
+ const toolName = payload && payload.tool_name;
113
+ const toolInput = (payload && payload.tool_input) || {};
114
+ const mapper = CLAUDE_TOOL_TO_CONTEXT[toolName];
115
+ let base;
116
+ if (mapper) base = mapper(toolInput);
117
+ else if (/^(?:mcp__|plugin:)/i.test(String(toolName || ''))) base = _mcpContext(toolName, toolInput);
118
+ else base = { toolName: toolName || 'Unknown', command: '' };
119
+ return { ...base, warning: '', fullContext: base.command };
120
+ }
121
+
122
+ // Park manager (Phase 1b): the Map + promise + timeout mechanics for holding a
123
+ // hook request open while a human decides. Pure of CTM internals — the server
124
+ // injects the card raise/clear as side effects (onTimeout) and decides the requestId.
125
+ // park() resolves to 'allow' | 'deny' (resolve() called) or 'ask' (deadline elapsed).
126
+ // One parked request per session; a second park() hands the prior one to 'ask' (the
127
+ // native-widget fallback) so a request is never silently dropped.
128
+ function createHookApprovalPark({ deadlineMs = 590000, onTimeout, now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout } = {}) {
129
+ const map = new Map(); // sessionId -> { resolve, requestId, timer, createdAt }
130
+ function park(sessionId, requestId) {
131
+ const existing = map.get(sessionId);
132
+ if (existing) {
133
+ map.delete(sessionId);
134
+ if (existing.timer) clearTimer(existing.timer);
135
+ try { existing.resolve('ask'); } catch { /* prior awaiter gone */ }
136
+ }
137
+ return new Promise((resolve) => {
138
+ // Register the entry BEFORE arming the timer so the timeout callback always sees
139
+ // it — a synchronous timer (tests) would otherwise fire before the map.set and
140
+ // find nothing, never resolving. (Real setTimeout is async, but this is the
141
+ // correct invariant regardless.)
142
+ const entry = { resolve, requestId, timer: null, createdAt: now() };
143
+ map.set(sessionId, entry);
144
+ entry.timer = setTimer(() => {
145
+ const cur = map.get(sessionId);
146
+ if (!cur || cur.requestId !== requestId) return;
147
+ map.delete(sessionId);
148
+ try { if (typeof onTimeout === 'function') onTimeout(sessionId, requestId); } catch { /* never break the resolve */ }
149
+ resolve('ask');
150
+ }, deadlineMs);
151
+ if (entry.timer && typeof entry.timer.unref === 'function') entry.timer.unref();
152
+ });
153
+ }
154
+ function resolve(sessionId, decision) {
155
+ const p = map.get(sessionId);
156
+ if (!p) return null;
157
+ map.delete(sessionId);
158
+ if (p.timer) clearTimer(p.timer);
159
+ try { p.resolve(decision); } catch { /* awaiter gone */ }
160
+ return p;
161
+ }
162
+ return {
163
+ park,
164
+ resolve,
165
+ has: (sessionId) => map.has(sessionId),
166
+ size: () => map.size,
167
+ };
168
+ }
169
+
170
+ // Shape the approval_decisions row for a parked hook escalation — parity with
171
+ // the scrape path's escalation records so the Permission "Pending" tab stays
172
+ // populated as sessions migrate to the hook. `parts` is the operative-command
173
+ // parse (approval-agent escalationCommandParts) so the row titles with the
174
+ // COMMAND, not a reason label. Pure: the caller owns the DB write.
175
+ function escalationRecordFromPark(sessionId, context, decision, label, parts) {
176
+ const p = parts || {};
177
+ return {
178
+ sessionId,
179
+ toolName: (context && context.toolName) || '',
180
+ commandSummary: p.title || String(label || '').slice(0, 160) || 'command',
181
+ fullContext: String((context && (context.fullContext || context.command)) || '').slice(0, 2000),
182
+ warning: (context && context.warning) || '',
183
+ decision: 'escalated',
184
+ reasoning: (decision && decision.reason) || 'Escalated by the approval hook',
185
+ decidedBy: (decision && decision.decidedBy) || 'hook',
186
+ riskLevel: (decision && decision.riskLevel) || 'medium',
187
+ commandSignature: p.signature || '',
188
+ };
189
+ }
190
+
191
+ module.exports = {
192
+ DEFAULT_MATCHER,
193
+ buildClaudeHookSettings,
194
+ hookTokenOk,
195
+ mapDecisionToHookOutput,
196
+ hookResponseBody,
197
+ contextFromHookPayload,
198
+ createHookApprovalPark,
199
+ escalationRecordFromPark,
200
+ };
@@ -162,6 +162,7 @@ module.exports = {
162
162
  learnFromInterrupt,
163
163
  learnFromApproveAfterEscalation,
164
164
  runSelfAdaptMaintenance,
165
+ _escapeRegex,
165
166
  _sqliteTimeMs,
166
167
  DEFAULT_INTERRUPT_WINDOW_MS,
167
168
  DEFAULT_APPROVE_WINDOW_MS,
@@ -16,12 +16,23 @@ const WS_AUTH_RULES = Object.freeze({
16
16
  reconcile: rule('read', false, 'never'),
17
17
  resize: rule('read', false, 'never'),
18
18
  reflow: rule('read', false, 'never'),
19
+ // Backpressure flow control: a viewer asks to pause/resume its own output
20
+ // stream (the server further gates an actual PTY pause to raw-eligible
21
+ // primaries). Same render-control class as reconcile/reflow — read scope.
22
+ flow: rule('read', false, 'never'),
23
+ // Client-reported render diagnostics (rows-flap attribution probe) — telemetry
24
+ // into the per-session diagnostics ring; rate-limited + scalar-whitelisted in
25
+ // handleClientDiag, so viewer (read) scope is sufficient.
26
+ 'client-diag': rule('read', false, 'never'),
19
27
  'walle-history-request': rule('read', false, 'never'),
20
28
 
21
29
  input: rule('respond', true, 'remote'),
22
30
  'walle-message': rule('respond', true, 'remote'),
23
31
  'walle-cancel': rule('respond', true, 'remote'),
24
32
  'walle-permission-response': rule('respond', true, 'remote'),
33
+ // Resolve a parked protocol-hook approval (allow/deny) — same class as
34
+ // answering any other approval prompt.
35
+ 'approval-resolve': rule('respond', true, 'remote'),
25
36
 
26
37
  create: rule('create', true, 'remote'),
27
38
 
@@ -23,16 +23,28 @@ const MODEL_TIER_ENV = {
23
23
  // so a Wall-E restart doesn't pin us to env for long.
24
24
  const WALLE_DEFAULTS_TTL_MS = 30000;
25
25
  const WALLE_DEFAULTS_NEGATIVE_TTL_MS = 5000;
26
- let _walleDefaultsCache = null; // { at, value: {provider, model} | null }
26
+ let _walleDefaultsCache = null; // { at, value: {provider, model} | null, authoritative }
27
+ // Stale-while-revalidate: the last starred default a status fetch ever returned. When
28
+ // Wall-E is slow (>timeout) or briefly down — exactly when the box is busiest — a stale
29
+ // starred default is far better than falling through to env.WALLE_PROVIDER, which launchd
30
+ // pins to whatever the plist said at bootstrap (observed: a stale deepseek pin surviving
31
+ // KeepAlive respawns → 402 churn). The starred default changes rarely and only via a
32
+ // Wall-E that is up, so serving the last-seen value while retrying is always safe.
33
+ let _lastGoodWalleDefaults = null;
27
34
 
28
35
  function _resetWalleDefaultsCache() {
29
36
  _walleDefaultsCache = null;
37
+ _lastGoodWalleDefaults = null;
30
38
  }
31
39
 
32
40
  async function fetchWalleStatusDefaults(env) {
33
41
  const port = Number(env?.WALLE_PORT) || 3457;
42
+ // 2500ms matches the setup handler's Wall-E status budget: a loaded Wall-E routinely
43
+ // answers in 0.6–1.2s, and a timeout here means falling back to the (possibly stale)
44
+ // env pin or last-good value — be generous before giving up.
45
+ const timeoutMs = Number(env?.CTM_WALLE_DEFAULTS_FETCH_TIMEOUT_MS) || 2500;
34
46
  const response = await fetch(`http://127.0.0.1:${port}/api/wall-e/status`, {
35
- signal: AbortSignal.timeout(1500),
47
+ signal: AbortSignal.timeout(timeoutMs),
36
48
  });
37
49
  if (!response.ok) throw new Error(`Wall-E status returned ${response.status}`);
38
50
  const json = await response.json();
@@ -44,21 +56,37 @@ async function getWalleDefaultSelection(options = {}) {
44
56
  if (String(env.CTM_BACKGROUND_LLM_WALLE_DEFAULTS || '').trim() === '0') return null;
45
57
  const now = Date.now();
46
58
  if (_walleDefaultsCache) {
47
- const ttl = _walleDefaultsCache.value ? WALLE_DEFAULTS_TTL_MS : WALLE_DEFAULTS_NEGATIVE_TTL_MS;
59
+ // Stale-served results keep the short negative TTL so a recovered Wall-E is
60
+ // consulted again promptly; authoritative results get the full TTL. Both are
61
+ // env-tunable (and 0 disables caching — used by tests to force refetches).
62
+ const _ttlOf = (key, dflt) => {
63
+ const n = Number(env[key]);
64
+ return Number.isFinite(n) && n >= 0 ? n : dflt;
65
+ };
66
+ const ttl = _walleDefaultsCache.authoritative
67
+ ? _ttlOf('CTM_WALLE_DEFAULTS_TTL_MS', WALLE_DEFAULTS_TTL_MS)
68
+ : _ttlOf('CTM_WALLE_DEFAULTS_NEGATIVE_TTL_MS', WALLE_DEFAULTS_NEGATIVE_TTL_MS);
48
69
  if (now - _walleDefaultsCache.at < ttl) return _walleDefaultsCache.value;
49
70
  }
50
71
  let value = null;
72
+ let authoritative = false;
51
73
  try {
52
74
  const fetchStatus = options.fetchWalleStatus || fetchWalleStatusDefaults;
53
75
  const status = await fetchStatus(env);
54
76
  const provider = sanitizeSetupProviderType(status?.walle_provider || '');
55
77
  if (provider) {
56
78
  value = { provider, model: typeof status?.walle_model === 'string' ? status.walle_model : '' };
79
+ authoritative = true;
57
80
  }
58
81
  } catch {
59
82
  value = null;
60
83
  }
61
- _walleDefaultsCache = { at: now, value };
84
+ if (authoritative) {
85
+ _lastGoodWalleDefaults = value;
86
+ } else {
87
+ value = _lastGoodWalleDefaults;
88
+ }
89
+ _walleDefaultsCache = { at: now, value, authoritative };
62
90
  return value;
63
91
  }
64
92