@chibitek/chibi 0.1.168 → 0.1.170

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 (90) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-DUyzT6Xq.js → abnfDiagram-O67JEVCF-Dwjw0xC0.js} +1 -1
  2. package/dist/assets/{arc-B4-kO642.js → arc-B__BIHzb.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-DgKdnNL2.js → architectureDiagram-NJMV4G6O-DQy7n3Ga.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-B6oV3kDr.js → blockDiagram-BEXU5L5S-moFo2nSA.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Dp0E43jx.js → c4Diagram-YGBWAQC7-C-dF8UVl.js} +1 -1
  6. package/dist/assets/channel-BQM13l0i.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-Cfv1WVUo.js → chunk-2Q5K7J3B-DnF0mxAV.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-_-STY2yO.js → chunk-3FUC2YCW-Dp8Dsifv.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-C6MZm1cm.js → chunk-5VM5RSS4-D8fmZ4d8.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-C__aVIJy.js → chunk-GTNCS2PH-DNH2HZNQ.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-fSrARJuB.js → chunk-GWA4HPMP-BvL6j7pQ.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CJioJB6h.js → chunk-JWPE2WC7-Co0gUA5M.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-o6MBGCq_.js → chunk-NETBCI7D-Deu9tpCF.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-O-TFLbdc.js → chunk-WEXAMYUT-DKy8jAU5.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-DYS9CQbl.js → chunk-XXDRQBXY-DQhvP8zg.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BC4bSH09.js → classDiagram-v2-NBCMYWYE-mhv3hMMV.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-7NKuC3-C.js → cose-bilkent-JH36ORCC-CR1nOQOe.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-CuWv5cOw.js → cynefin-OW5HDTMX-Ddgkhxgt.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-D_l_JrG5.js → cynefinDiagram-VND7K2PF-4KbkrNhB.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-D7eucSmr.js → dagre-6A5THRUB-DNxtjbvT.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-CADR5v1e.js → diagram-22UHCM2B-BRpvnOWX.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-DF_fBOZn.js → diagram-3UASUU5V-DsQVnKUb.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-DE5p59WH.js → diagram-ATOU4E4O-BRZ5455_.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-vthvYooA.js → diagram-CDSNMT55-CidrDKp0.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-ByMHy4S-.js → diagram-MLGK6HIB-1hkZ8SsK.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-C9hKBUnO.js → diagram-MPIPVDR6-D1dWGFqn.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-Djv6hWEl.js → ebnfDiagram-ZINNZB2B-DGjfEPY5.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-CmUjf18z.js → elk-276RUBZZ-C5nvtTy7.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-DkmXQUok.js → erDiagram-OPXOYQCR-CC0QBE1A.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-6pz2poYn.js → flowDiagram-KWPJA3E3-BthbP_HO.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-Cx9Fbw75.js → ganttDiagram-FUAMR5RP-BQ2wLSEI.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-BR5MsYkP.js → gitGraphDiagram-X574FWY7-lTR8tNlq.js} +1 -1
  33. package/dist/assets/{index-BoF5cGZq.js → index-AWsWxgoq.js} +1 -1
  34. package/dist/assets/{index-Ckv52eaP.js → index-VagspC1z.js} +186 -186
  35. package/dist/assets/{infoDiagram-VRGFBTTK-DoF183Am.js → infoDiagram-VRGFBTTK-BgI9ZuT_.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-OU5B5YK6-4T_-hM1-.js → ishikawaDiagram-OU5B5YK6-qPe6Rhk2.js} +1 -1
  37. package/dist/assets/{journeyDiagram-ZHPQQLJL-Ca0T3Ngd.js → journeyDiagram-ZHPQQLJL-CpbadZuP.js} +1 -1
  38. package/dist/assets/{kanban-definition-PNTS6WVX-DNp_DAr9.js → kanban-definition-PNTS6WVX-D9kCb3-t.js} +1 -1
  39. package/dist/assets/{linear-COUoS2bF.js → linear-CdW4LT3g.js} +1 -1
  40. package/dist/assets/{mermaid.core-DQHowMIV.js → mermaid.core-BbPWTT56.js} +5 -5
  41. package/dist/assets/{mindmap-definition-NLK3R4M7-CMgO1Mkh.js → mindmap-definition-NLK3R4M7-BtOaXxvb.js} +1 -1
  42. package/dist/assets/{pegDiagram-GJSIUBJH-BCVCZbsq.js → pegDiagram-GJSIUBJH-BtcTITAK.js} +1 -1
  43. package/dist/assets/{pieDiagram-5QR66LMP-DxaMlX62.js → pieDiagram-5QR66LMP-Ch_--zjt.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-O4NWA36T-qUD0pkur.js → quadrantDiagram-O4NWA36T-Di8ygRal.js} +1 -1
  45. package/dist/assets/{railroadDiagram-XR7U4H2S-Dc6rcAJB.js → railroadDiagram-XR7U4H2S-DxgesAmb.js} +1 -1
  46. package/dist/assets/{requirementDiagram-PLB6GJNP-DMTfI_lR.js → requirementDiagram-PLB6GJNP-DjH-jdCr.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-IPEJSGJF-B0BWZys8.js → sankeyDiagram-IPEJSGJF-ClNLceSc.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-PO4LG4MO-DZVt17mL.js → sequenceDiagram-PO4LG4MO-Cip_EmtV.js} +1 -1
  49. package/dist/assets/{sizeCapture-INFHLROL-BFQztpNW.js → sizeCapture-INFHLROL-BzW2hkq8.js} +1 -1
  50. package/dist/assets/{stateDiagram-v2-GCMORJYK-GeFCVCDr.js → stateDiagram-v2-GCMORJYK-CwHu7P19.js} +1 -1
  51. package/dist/assets/{swimlanes-2SLR337P-DTqQXTvP.js → swimlanes-2SLR337P-DO9E9HUK.js} +1 -1
  52. package/dist/assets/swimlanesDiagram-TC7HE7FX-UC0GNJzN.js +8 -0
  53. package/dist/assets/{timeline-definition-EJHVYXUP-BoiNrkP7.js → timeline-definition-EJHVYXUP-D2_E-i9h.js} +1 -1
  54. package/dist/assets/{usecaseDiagram-POWQR4AR-CdfsSO2g.js → usecaseDiagram-POWQR4AR-oKBQ1FAG.js} +1 -1
  55. package/dist/assets/{vennDiagram-UO4OBE2U-CHc_Fn1O.js → vennDiagram-UO4OBE2U-HEdQsC7L.js} +1 -1
  56. package/dist/assets/{wardleyDiagram-VNRHLVJA-Bv3KWVEF.js → wardleyDiagram-VNRHLVJA-mNpRO8rd.js} +1 -1
  57. package/dist/assets/{xychartDiagram-PMCCYNJV-CQFknLPS.js → xychartDiagram-PMCCYNJV-uURjTK1X.js} +1 -1
  58. package/dist/handbook/atomic-handbook.html +1 -1
  59. package/dist/index.html +1 -1
  60. package/dist-server/container-mcp.js +1 -0
  61. package/dist-server/hooks/plan-hook.js +22 -0
  62. package/dist-server/index.js +1277 -931
  63. package/dist-server/local-computer.js +1 -0
  64. package/dist-server/openmausbot.js +384 -298
  65. package/dist-server/pair-cli.js +384 -298
  66. package/dist-server/proxy-paths.js +1 -0
  67. package/dist-server/server/atomic-engine-release.js +13 -13
  68. package/dist-server/server/bot-work-modes.js +212 -0
  69. package/dist-server/server/config.js +6 -5
  70. package/dist-server/server/drivers/acp/core.js +6 -4
  71. package/dist-server/server/drivers/acp/custom.js +51 -37
  72. package/dist-server/server/drivers/builtIn.js +2 -1
  73. package/dist-server/server/drivers/chat-mcp-tools.js +1 -0
  74. package/dist-server/server/drivers/claude.js +28 -25
  75. package/dist-server/server/drivers/codex-approval-presets.js +34 -0
  76. package/dist-server/server/drivers/codex.js +11 -35
  77. package/dist-server/server/drivers/openai-chat.js +4 -3
  78. package/dist-server/server/hooks/plan-hook.js +17 -0
  79. package/dist-server/server/index.js +73 -57
  80. package/dist-server/server/mcp-interactive-probe.js +60 -0
  81. package/dist-server/server/mcp-remote-bridge.js +13 -6
  82. package/dist-server/server/plan-policy.js +14 -0
  83. package/dist-server/server/proxy-paths.js +1 -0
  84. package/dist-server/server/store.js +4 -4
  85. package/dist-server/shared/approval-mode.js +3 -1
  86. package/dist-server/shared/engine-choices.js +1 -1
  87. package/dist-server/vps-container-mcp.js +1 -0
  88. package/package.json +1 -1
  89. package/dist/assets/channel-BidDehbw.js +0 -1
  90. package/dist/assets/swimlanesDiagram-TC7HE7FX-CRQqFEdH.js +0 -8
@@ -21,6 +21,7 @@ var SPAWNED_PROXIES = {
21
21
  hook: resolveProxy("hooks/omb-hook"),
22
22
  // the RTK add-on's Claude PreToolUse hook (bypassPermissions turns only)
23
23
  rtkHook: resolveProxy("hooks/rtk-hook"),
24
+ planHook: resolveProxy("hooks/plan-hook"),
24
25
  // Loaded by the external `pi` process via `-e`, not by this server — but
25
26
  // resolved through the same single source of truth so the packaged layout
26
27
  // check can assert it ships.
@@ -5,28 +5,28 @@
5
5
  // download whose size or SHA-512 differs from the pin below.
6
6
  //
7
7
  // Generated by scripts/release-engine.mjs. Do not edit by hand.
8
- export const ATOMIC_ENGINE_VERSION = "0.21.4-7eb41cb";
8
+ export const ATOMIC_ENGINE_VERSION = "0.21.4-4cc340e";
9
9
  const ASSETS = [
10
10
  {
11
- version: "0.21.4-7eb41cb",
11
+ version: "0.21.4-4cc340e",
12
12
  platform: "darwin-arm64",
13
- url: "https://ugmbfkqvogfuvufzjxtb.supabase.co/storage/v1/object/public/atomic-updates/atomic/K_5FMcrZFZ5AE2K-Lydn-Tis/atomic-engine-0.21.4-7eb41cb-darwin-arm64.tar.gz",
14
- bytes: 114827855,
15
- sha512: "357648850bc4042960fa0f1ef01415f9066c1beb445f5a0edd66a2f9c05e8fcf68b1d692ec6d81bb45c80b7d7dadedf828721d8020c6e12639de7730370177af",
13
+ url: "https://ugmbfkqvogfuvufzjxtb.supabase.co/storage/v1/object/public/atomic-updates/atomic/K_5FMcrZFZ5AE2K-Lydn-Tis/atomic-engine-0.21.4-4cc340e-darwin-arm64.tar.gz",
14
+ bytes: 115631334,
15
+ sha512: "666626a535dce48f22087e26f7c14702dda4028b2c6759952017cffa73f37d86b16f5d8b6f81fb0efb535cb285ba4ddb6a5da3fd7e709ea0aa8b2b8c783e0c17",
16
16
  },
17
17
  {
18
- version: "0.21.4-7eb41cb",
18
+ version: "0.21.4-4cc340e",
19
19
  platform: "win32-x64",
20
- url: "https://ugmbfkqvogfuvufzjxtb.supabase.co/storage/v1/object/public/atomic-updates/atomic/K_5FMcrZFZ5AE2K-Lydn-Tis/atomic-engine-0.21.4-7eb41cb-win32-x64.tar.gz",
21
- bytes: 102379095,
22
- sha512: "e3f3ff51513df37f79165e9408395d771f6136cfeb3207417741b421347f7f183209a2865c70fe3175a20dc35ef5bc409213bd3bbc5f8b614d7d08f2ff8edd07",
20
+ url: "https://ugmbfkqvogfuvufzjxtb.supabase.co/storage/v1/object/public/atomic-updates/atomic/K_5FMcrZFZ5AE2K-Lydn-Tis/atomic-engine-0.21.4-4cc340e-win32-x64.tar.gz",
21
+ bytes: 103355479,
22
+ sha512: "4a074d1d2254ce697ed7ddf794b6d56e1f62374d1ae5f533f7fea12c7f18af46f47f33fd38f9e09d3ca048ef027882b6b271dd229f40a9ffcb7f3f23db5d1bb0",
23
23
  },
24
24
  {
25
- version: "0.21.4-acc9ee6",
25
+ version: "0.21.4-4cc340e",
26
26
  platform: "darwin-x64",
27
- url: "https://ugmbfkqvogfuvufzjxtb.supabase.co/storage/v1/object/public/atomic-updates/atomic/K_5FMcrZFZ5AE2K-Lydn-Tis/atomic-engine-0.21.4-acc9ee6-darwin-x64.tar.gz",
28
- bytes: 92108962,
29
- sha512: "12c3fcba9ff17ec2a53fdbd39e8f14fb23786be256f2e19ce63ba011c2317231e58052b1b1b0a5bdcd7be2b23d9cf7edaeb267ad312e1ac862e128ac70f3aa00",
27
+ url: "https://ugmbfkqvogfuvufzjxtb.supabase.co/storage/v1/object/public/atomic-updates/atomic/K_5FMcrZFZ5AE2K-Lydn-Tis/atomic-engine-0.21.4-4cc340e-darwin-x64.tar.gz",
28
+ bytes: 92883251,
29
+ sha512: "eec5fcbbcdbe44db76f938cb3dd98fedfc6c644cc054c75d0ad3daaf66e39f60dc6b75fa3f8cdbbc2cc3760a2ea7d3fc608a1cf7a42c4a020e6cd4d6dc823df0",
30
30
  },
31
31
  ];
32
32
  /** The package for this computer, or null when none is published for it. */
@@ -0,0 +1,212 @@
1
+ // Atomic Engineering, 2026-10-31. Per-bot composer modes and paired confirmation.
2
+ // Pending confirmations exist only in memory; restarting never commits one.
3
+ import { randomUUID } from "node:crypto";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { writeFileAtomic } from "./atomic.js";
6
+ import { approvalModeForOrigin } from "./auto-approve.js";
7
+ import { PASS } from "./routes/table.js";
8
+ import { approvalModeFor, supportsApprovalMode } from "../shared/approval-mode.js";
9
+ export const WORK_MODES = ["plan", "ask", "edits", "full"];
10
+ const valid = (value) => WORK_MODES.includes(value);
11
+ export class BotWorkModes {
12
+ selections = {};
13
+ allowed = [...WORK_MODES];
14
+ pending = new Map();
15
+ file;
16
+ constructor(file) {
17
+ this.file = file;
18
+ if (!existsSync(file))
19
+ return;
20
+ try {
21
+ const data = JSON.parse(readFileSync(file, "utf8"));
22
+ if (Array.isArray(data.allowed))
23
+ this.allowed = ["ask", ...data.allowed.filter((mode) => valid(mode) && mode !== "ask")];
24
+ for (const [id, entry] of Object.entries(data.selections ?? {})) {
25
+ const item = entry;
26
+ if (item && valid(item.mode) && typeof item.sessionId === "string")
27
+ this.selections[id] = item;
28
+ }
29
+ }
30
+ catch {
31
+ this.allowed = ["ask"];
32
+ }
33
+ }
34
+ save() { writeFileAtomic(this.file, JSON.stringify({ allowed: this.allowed, selections: this.selections })); }
35
+ selected(botId) { return this.selections[botId]?.mode; }
36
+ availability(drivers) {
37
+ return WORK_MODES.filter(mode => this.allowed.includes(mode) && drivers.length > 0 && drivers.every(driver => supportsApprovalMode(driver, mode)));
38
+ }
39
+ settings() { return { allowed: this.allowed }; }
40
+ permits(mode) { return mode === "plan" || !valid(mode) || this.allowed.includes(mode); }
41
+ expired(live) {
42
+ return [...new Set(Object.values(this.selections).filter(item => item.mode !== "ask" && !live(item.sessionId)).map(item => item.sessionId))].flatMap(id => this.revoke(id));
43
+ }
44
+ configure(modes) {
45
+ if (!Array.isArray(modes) || !modes.every(valid))
46
+ throw new Error("Invalid available modes");
47
+ this.allowed = [...new Set(["ask", ...modes])];
48
+ const changed = Object.keys(this.selections).filter(id => this.selections[id].mode !== "plan" && !this.allowed.includes(this.selections[id].mode));
49
+ for (const id of changed)
50
+ this.selections[id].mode = "ask";
51
+ this.pending.clear();
52
+ this.save();
53
+ return changed;
54
+ }
55
+ set(botId, mode, sessionId) {
56
+ this.selections[botId] = { mode, sessionId };
57
+ for (const [id, item] of this.pending)
58
+ if (item.botId === botId)
59
+ this.pending.delete(id);
60
+ this.save();
61
+ }
62
+ request(botId, mode, sessionId, revision) {
63
+ for (const [id, item] of this.pending)
64
+ if (item.botId === botId || item.expiresAt <= Date.now())
65
+ this.pending.delete(id);
66
+ const nonce = randomUUID(), expiresAt = Date.now() + 120_000;
67
+ this.pending.set(nonce, { botId, mode, sessionId, revision, expiresAt });
68
+ return { nonce, expiresAt, mode, botId };
69
+ }
70
+ confirm(nonce, botId, mode, sessionId, revision) {
71
+ const item = this.pending.get(nonce);
72
+ if (!item || item.botId !== botId || item.mode !== mode || item.sessionId !== sessionId || item.revision !== revision || item.expiresAt <= Date.now())
73
+ return false;
74
+ this.pending.delete(nonce);
75
+ this.set(botId, mode, sessionId);
76
+ return true;
77
+ }
78
+ revoke(sessionId) {
79
+ const ids = Object.keys(this.selections).filter(id => this.selections[id].sessionId === sessionId);
80
+ for (const id of ids)
81
+ if (this.selections[id].mode !== "plan")
82
+ this.selections[id].mode = "ask";
83
+ for (const [id, item] of this.pending)
84
+ if (item.sessionId === sessionId)
85
+ this.pending.delete(id);
86
+ if (ids.length)
87
+ this.save();
88
+ return ids.filter(id => this.selections[id].mode !== "plan");
89
+ }
90
+ }
91
+ export function pairedModeAuthority(auth, desktopManaged) {
92
+ return desktopManaged && auth.kind === "session" && auth.via === "bearer" && auth.scopes.includes("admin");
93
+ }
94
+ export function createBotWorkModeRoutes(deps) {
95
+ return async ({ path, method, auth, req, res, readBody, json }) => {
96
+ const settings = path === "/api/work-modes";
97
+ const match = /^\/api\/bots\/([\w-]+)\/work-mode$/.exec(path);
98
+ if (!settings && !match)
99
+ return PASS;
100
+ if (!pairedModeAuthority(auth, deps.desktopManaged) || auth.kind !== "session" || !deps.live(auth.session.id))
101
+ return json(res, 403, { error: "Mode changes require an Administrator's paired device connected to the desktop app." });
102
+ const body = method === "POST" || method === "PUT" ? await readBody(req) : {};
103
+ if (!deps.live(auth.session.id))
104
+ return json(res, 403, { error: "Pairing expired" });
105
+ if (settings) {
106
+ if (method === "GET")
107
+ return json(res, 200, deps.modes.settings());
108
+ if (method !== "PUT")
109
+ return json(res, 405, { error: "Method not allowed" });
110
+ try {
111
+ if (!Array.isArray(body.allowed) || !body.allowed.every(valid))
112
+ return json(res, 400, { error: "Invalid available modes" });
113
+ const affected = deps.affected(["ask", ...body.allowed]);
114
+ const ids = [...new Set([...affected, ...deps.modes.configure(body.allowed)])];
115
+ for (const id of ids) {
116
+ deps.modes.set(id, "ask", auth.session.id);
117
+ deps.apply(id, "ask", true);
118
+ await deps.stop(id);
119
+ deps.changed(id);
120
+ }
121
+ return json(res, 200, deps.modes.settings());
122
+ }
123
+ catch {
124
+ return json(res, 400, { error: "Could not change available modes" });
125
+ }
126
+ }
127
+ const id = match[1], bot = deps.bot(id);
128
+ if (!bot)
129
+ return json(res, 404, { error: "No such bot" });
130
+ const status = () => ({ current: deps.bot(id)?.current ?? "ask", available: deps.modes.availability(bot.drivers) });
131
+ if (method === "GET")
132
+ return json(res, 200, status());
133
+ if (method !== "POST")
134
+ return json(res, 405, { error: "Method not allowed" });
135
+ if (!valid(body.mode))
136
+ return json(res, 400, { error: "Invalid mode" });
137
+ if (body.mode !== "ask" && !deps.modes.availability(bot.drivers).includes(body.mode))
138
+ return json(res, 409, { error: "This mode is unavailable in Settings or on this engine." });
139
+ if (bot.current === "plan" && body.mode !== "plan") {
140
+ if (bot.busy)
141
+ return json(res, 409, { error: "Stop planning before approving the plan." });
142
+ if (typeof body.nonce !== "string")
143
+ return json(res, 200, { ...status(), confirmation: deps.modes.request(id, body.mode, auth.session.id, bot.revision) });
144
+ if (body.mode === "ask" && !deps.modes.confirm(body.nonce, id, "ask", auth.session.id, bot.revision))
145
+ return json(res, 409, { error: "Plan confirmation expired or changed." });
146
+ }
147
+ if (body.mode === "ask") {
148
+ deps.modes.set(id, "ask", auth.session.id);
149
+ deps.apply(id, "ask");
150
+ await deps.stop(id);
151
+ deps.changed(id);
152
+ return json(res, 200, status());
153
+ }
154
+ if (bot.busy)
155
+ return json(res, 409, { error: "Stop this bot's active threads before changing mode." });
156
+ if (!deps.modes.availability(bot.drivers).includes(body.mode))
157
+ return json(res, 409, { error: "This mode is unavailable in Settings or on one of this bot's engines." });
158
+ if (body.mode === "plan") {
159
+ deps.modes.set(id, "plan", auth.session.id);
160
+ deps.apply(id, "plan");
161
+ deps.changed(id);
162
+ return json(res, 200, status());
163
+ }
164
+ if (typeof body.nonce !== "string")
165
+ return json(res, 200, { ...status(), confirmation: deps.modes.request(id, body.mode, auth.session.id, bot.revision) });
166
+ if (!deps.modes.confirm(body.nonce, id, body.mode, auth.session.id, bot.revision))
167
+ return json(res, 409, { error: "This confirmation expired or the bot changed. Choose the mode again." });
168
+ deps.apply(id, body.mode);
169
+ deps.changed(id);
170
+ return json(res, 200, status());
171
+ };
172
+ }
173
+ export function ownBotApprovalMode(bot, driver, peerInitiated, permitted = true) {
174
+ if (!permitted)
175
+ return "ask";
176
+ const mode = approvalModeForOrigin(approvalModeFor(bot), { peerInitiated });
177
+ if (mode === "plan")
178
+ return mode;
179
+ return supportsApprovalMode(driver, mode) ? mode : "ask";
180
+ }
181
+ export function legacyGodModeCovers(bot, active, overridden) {
182
+ if (!active || overridden || approvalModeFor(bot) === "plan")
183
+ return false;
184
+ const grant = bot.approvalGrant;
185
+ return !(grant && !(grant.threadOnly && grant.threadId !== bot.threadId));
186
+ }
187
+ export function dispatchWorkMode(mode, driver) {
188
+ if (mode === "plan" && !supportsApprovalMode(driver, mode))
189
+ throw new Error("This engine cannot enforce Plan. Choose Ask in the mode picker first.");
190
+ return mode;
191
+ }
192
+ /** Recovery lowers elevated targets without implicitly approving any Plan. */
193
+ export function trustedPlanTransitionAllowed(mode, approved, scope) {
194
+ if (mode === "plan" || approved || (scope.allThreads && mode === "ask"))
195
+ return true;
196
+ const targets = scope.threadOnly ? [scope.thread] : scope.allThreads ? [scope.bot, ...scope.threads] : [scope.bot, scope.thread];
197
+ return !targets.includes("plan");
198
+ }
199
+ export function trustedPlanChangeForStore(store, botId, mode, message) {
200
+ const target = typeof message.threadId === "string" ? store.projectBotForTask(botId, message.threadId) : null;
201
+ return trustedPlanTransitionAllowed(mode, message.approvePlan === true, {
202
+ bot: approvalModeFor(store.bot(botId)), thread: target ? approvalModeFor(target) : undefined,
203
+ threadOnly: message.threadOnly === true, allThreads: message.allThreads === true,
204
+ threads: store.tasks(botId).map(task => approvalModeFor(store.projectBotForTask(botId, task.threadId))),
205
+ });
206
+ }
207
+ export function invalidAllThreadScope(mode, message) {
208
+ return message.allThreads !== undefined && (typeof message.allThreads !== "boolean" ||
209
+ (message.allThreads && (message.threadId !== undefined || message.threadOnly === true ||
210
+ message.modelSelection !== undefined || message.updateBotDefault !== undefined ||
211
+ (mode !== "full" && mode !== "ask"))));
212
+ }
@@ -1386,7 +1386,7 @@ function injectedEnvironment(cfg, driver) {
1386
1386
  // preconfigured for https://ollama.com/v1 and lists as unavailable until an
1387
1387
  // Ollama API key is saved (or OLLAMA_API_KEY is set).
1388
1388
  const ATOMIC_FLEET = {
1389
- "atomic-engine": { driver: "customAcp", displayName: "Atomic Engine", config: { cli: "atomic acp" } },
1389
+ "atomic-engine": { driver: "atomicEngine", displayName: "Atomic Engine", config: { cli: "atomic acp" } },
1390
1390
  ollamaCloud: { driver: "ollamaCloud", displayName: "Ollama Cloud" },
1391
1391
  };
1392
1392
  function plainObject(value) {
@@ -1485,11 +1485,12 @@ export function instanceConfigs(cfg, onDevice = onDeviceFleet(), accountKeys = i
1485
1485
  map[id] = { ...entry };
1486
1486
  }
1487
1487
  }
1488
- // A persisted Atomic fleet entry may predate (or have lost) its defaults:
1489
- // older fleet saves wrote atomic-engine without `config.cli`, which left it
1490
- // "needs its command" and out of the picker. Put the defaults back under
1491
- // whatever is saved; an explicit saved value always wins.
1488
+ // Migrate the shipped Atomic Engine and restore missing CLI defaults.
1489
+ // Preserve explicitly customized commands and other drivers.
1492
1490
  for (const [id, fallback] of Object.entries(ATOMIC_FLEET)) {
1491
+ const savedCli = map[id]?.config?.cli;
1492
+ if (id === "atomic-engine" && map[id]?.driver === "customAcp" && (savedCli === undefined || savedCli === "atomic acp"))
1493
+ map[id] = { ...map[id], driver: "atomicEngine" };
1493
1494
  if (Object.hasOwn(map, id))
1494
1495
  map[id] = withFleetDefaults(map[id], fallback);
1495
1496
  }
@@ -673,6 +673,8 @@ export function createAcpDriver(support) {
673
673
  // never leave an unknown server request hanging — the agent blocks
674
674
  return send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "method not found" } });
675
675
  }
676
+ if (current.turn.approvalMode === "plan")
677
+ return send({ jsonrpc: "2.0", id: msg.id, result: { outcome: { outcome: "cancelled" } } });
676
678
  const params = msg.params ?? {};
677
679
  current.flushAssistantText();
678
680
  const options = Array.isArray(params.options) ? params.options : [];
@@ -1008,11 +1010,11 @@ export function createAcpDriver(support) {
1008
1010
  };
1009
1011
  const sendTurn = async (turn) => {
1010
1012
  const { threadId } = turn;
1013
+ if (turn.approvalMode === "plan" && !supportsApprovalMode(DRIVER_KIND, "plan"))
1014
+ throw new Error("Plan is not available for this engine yet");
1011
1015
  if (active.has(threadId))
1012
1016
  throw new Error("a turn is already running on this thread");
1013
- // Provider-instance `fullAuto` predates per-bot approval levels. Every
1014
- // harness turn now carries the bot's mode, so Ask/Auto must explicitly
1015
- // put the native agent back into its interactive mode. Otherwise a
1017
+ // Explicit bot modes override legacy instance fullAuto. Otherwise a
1016
1018
  // legacy Grok bypassPermissions / Cursor --force / Droid auto-high /
1017
1019
  // Antigravity yolo setting would silently outrank the selector. Calls
1018
1020
  // that omit approvalMode retain the old adapter-level behavior for
@@ -1041,7 +1043,7 @@ export function createAcpDriver(support) {
1041
1043
  let cliTurn = resolvedModel !== undefined && resolvedModel !== turn.model
1042
1044
  ? { ...turn, model: resolvedModel }
1043
1045
  : turn;
1044
- const mcpServers = acpMcpServers(turn);
1046
+ const mcpServers = turn.approvalMode === "plan" ? [] : acpMcpServers(turn);
1045
1047
  let launch;
1046
1048
  try {
1047
1049
  launch = support.resolveCommand
@@ -153,41 +153,55 @@ const support = {
153
153
  },
154
154
  snapshot: (env, config, instanceId) => customAcpSnapshot(env, config.cli, instanceId),
155
155
  };
156
- const base = createAcpDriver(support);
157
- export const CustomAcpDriver = {
158
- ...base,
159
- // decodeAcpConfig never throws (a missing cli falls back to defaultCli,
160
- // which is empty here). Turn every blank-command path into the same
161
- // teaching shadow row instead of a bare spawn ENOENT at first send.
162
- decodeConfig(raw) {
163
- const decoded = base.decodeConfig(raw);
164
- if (!decoded.cli.trim())
165
- throw new Error(CONFIGURE_HINT);
166
- return decoded;
167
- },
168
- async create(input) {
169
- if (!input.config.cli.trim())
170
- throw new Error(CONFIGURE_HINT);
171
- const instance = await base.create(input);
172
- const asset = atomicEngineAsset();
173
- if (!asset || !isAtomicEngine(input.instanceId, input.config.cli) || input.config.cli.trim() !== DEFAULT_ATOMIC_ENGINE_CLI)
174
- return instance;
175
- // Atomic Engine is installed by Atomic itself, the same way on every OS.
176
- const install = {
177
- managed: { label: "Install Atomic Engine", downloadBytes: asset.bytes },
178
- };
179
- return {
180
- ...instance,
181
- get models() {
182
- return instance.models;
183
- },
184
- install,
185
- installRuntime: async (onProgress) => {
186
- await installAtomicEngine({
187
- // "verifying" is part of the download as far as setup's progress bar goes
188
- onProgress: (progress) => onProgress?.({ ...progress, phase: progress.phase === "verifying" ? "downloading" : progress.phase }),
189
- });
190
- },
191
- };
156
+ function customDriver(options) {
157
+ const base = createAcpDriver(options);
158
+ return {
159
+ ...base,
160
+ // decodeAcpConfig never throws (a missing cli falls back to defaultCli,
161
+ // which is empty here). Turn every blank-command path into the same
162
+ // teaching shadow row instead of a bare spawn ENOENT at first send.
163
+ decodeConfig(raw) {
164
+ const decoded = base.decodeConfig(raw);
165
+ if (!decoded.cli.trim())
166
+ throw new Error(CONFIGURE_HINT);
167
+ return decoded;
168
+ },
169
+ async create(input) {
170
+ if (!input.config.cli.trim())
171
+ throw new Error(CONFIGURE_HINT);
172
+ const instance = await base.create(input);
173
+ const asset = atomicEngineAsset();
174
+ if (!asset || !isAtomicEngine(input.instanceId, input.config.cli) || input.config.cli.trim() !== DEFAULT_ATOMIC_ENGINE_CLI)
175
+ return instance;
176
+ // Atomic Engine is installed by Atomic itself, the same way on every OS.
177
+ const install = {
178
+ managed: { label: "Install Atomic Engine", downloadBytes: asset.bytes },
179
+ };
180
+ return {
181
+ ...instance,
182
+ get models() {
183
+ return instance.models;
184
+ },
185
+ install,
186
+ installRuntime: async (onProgress) => {
187
+ await installAtomicEngine({
188
+ // "verifying" is part of the download as far as setup's progress bar goes
189
+ onProgress: (progress) => onProgress?.({ ...progress, phase: progress.phase === "verifying" ? "downloading" : progress.phase }),
190
+ });
191
+ },
192
+ };
193
+ },
194
+ };
195
+ }
196
+ export const CustomAcpDriver = customDriver(support);
197
+ export const AtomicEngineDriver = customDriver({
198
+ ...support, driverKind: "atomicEngine", displayName: "Atomic Engine", defaultCli: DEFAULT_ATOMIC_ENGINE_CLI,
199
+ async configureSession(context) {
200
+ await support.configureSession?.(context);
201
+ const modeId = context.turn.approvalMode === "plan" ? "atomic-plan-v1" : "default";
202
+ const reply = await context.request("session/set_mode", { sessionId: context.sessionId, modeId });
203
+ if (modeId === "atomic-plan-v1" && (reply?._meta?.atomicPlanVersion !== 1 || reply?._meta?.mode !== modeId)) {
204
+ throw new Error("Update Atomic Engine before using Plan: this build does not enforce read-only tools");
205
+ }
192
206
  },
193
- };
207
+ });
@@ -13,7 +13,7 @@ import { MuseCodeDriver } from "./muse-code.js";
13
13
  import { OpenCodeDriver } from "./acp/opencode-go.js";
14
14
  import { QwenAgentDriver } from "./acp/qwen.js";
15
15
  import { VibeAgentDriver } from "./acp/vibe.js";
16
- import { CustomAcpDriver } from "./acp/custom.js";
16
+ import { AtomicEngineDriver, CustomAcpDriver } from "./acp/custom.js";
17
17
  import { HermesAgentDriver } from "./acp/hermes.js";
18
18
  import { OpenAICompatDriver } from "./openai-compat.js";
19
19
  import { PiDriver } from "./pi.js";
@@ -35,6 +35,7 @@ export const ALL_BUILT_IN_DRIVERS = [
35
35
  VibeAgentDriver,
36
36
  HermesAgentDriver,
37
37
  CustomAcpDriver,
38
+ AtomicEngineDriver,
38
39
  PiDriver,
39
40
  OpenAICompatDriver,
40
41
  ClaudeDriver,
@@ -425,6 +425,7 @@ export async function mountChatTools(integrations, signal, computerUse = false,
425
425
  return {
426
426
  definitions, validate, close,
427
427
  customTool: (name) => customTools.get(name),
428
+ planReadOnly: (name) => { const tool = registered.get(name); return tool?.client instanceof ChatBoxClient && ["screenshot", "get_screen_size"].includes(tool.name); },
428
429
  async execute(name, args, callSignal) {
429
430
  validate(name, args);
430
431
  if (callSignal.aborted) {
@@ -1,3 +1,4 @@
1
+ import { claudePlanHook, planToolAllowed, PLAN_REFUSAL } from "../plan-policy.js";
1
2
  import { firstText } from "./claude-message.js";
2
3
  // Claude driver — upstream ClaudeDriver skeleton over agentcal's
3
4
  // drivers/claude.js runtime (stream-json both directions, prompt over
@@ -624,6 +625,12 @@ export async function createPermissionBroker(opts) {
624
625
  continue;
625
626
  }
626
627
  const ask = { id: askId, kind, tool: msg.tool ?? "tool", input: msg.input ?? {}, at: Date.now() };
628
+ if (opts.plan && kind === "permission" && !planToolAllowed(ask.tool)) {
629
+ const behavior = "deny";
630
+ conn.write(JSON.stringify({ t: "answer", id: askId, behavior, source: "system", message: PLAN_REFUSAL }) + "\n");
631
+ opts.onResolve({ ...ask, behavior, source: "system" });
632
+ continue;
633
+ }
627
634
  const sessionKey = kind === "permission" && opts.sessionAllows && (opts.canAllowForSession?.(ask) ?? true)
628
635
  ? sessionAllowKey(ask)
629
636
  : null;
@@ -1002,11 +1009,9 @@ export const ClaudeDriver = {
1002
1009
  const relaunch = logicalTurnId !== undefined;
1003
1010
  if (active.has(threadId) && !relaunch)
1004
1011
  throw new Error("a turn is already running on this thread");
1005
- // A bot-level mode is authoritative for this turn. In particular, an
1006
- // old provider instance may still be configured with
1007
- // `bypassPermissions`; Ask/Auto must restore Claude's interactive
1008
- // broker instead of inheriting that silent bypass. Calls without a
1009
- // per-turn mode keep the legacy adapter behavior.
1012
+ const plan = turn.approvalMode === "plan";
1013
+ if (plan)
1014
+ turn = { ...turn, integrations: undefined, mcpFromUserConfig: false, pluginDirs: [] };
1010
1015
  const permissionMode = turn.approvalMode === undefined
1011
1016
  ? config.permissionMode
1012
1017
  : turn.approvalMode === "full" ? "bypassPermissions"
@@ -1049,7 +1054,9 @@ export const ClaudeDriver = {
1049
1054
  ];
1050
1055
  // A Team Room turn without shared files: no built-in tools at all
1051
1056
  // (an empty --tools list), so only the turn's MCP servers remain.
1052
- if (turn.builtinTools === false)
1057
+ if (plan)
1058
+ args.push("--tools", "Read,Glob,Grep", "--strict-mcp-config", "--setting-sources", "");
1059
+ else if (turn.builtinTools === false)
1053
1060
  args.push("--tools", "");
1054
1061
  else if (config.tools !== undefined)
1055
1062
  args.push("--tools", config.tools.join(","));
@@ -1075,22 +1082,8 @@ export const ClaudeDriver = {
1075
1082
  cliVersionChecked = true;
1076
1083
  }
1077
1084
  }
1078
- const isolated = !inheritsUserConfig(turnEnvironment);
1079
- if (isolated) {
1080
- // A bot gets the tools and instructions its owner gave it, not
1081
- // whatever this machine's Claude Code happens to be set up with.
1082
- // Without these the CLI silently adds, to EVERY turn of every bot:
1083
- // the desktop's own MCP servers and claude.ai connectors (one
1084
- // measured desktop mounted 407 extra tools, ~10k tokens), its skill
1085
- // and agent listings, its hooks, and its personal CLAUDE.md. Every
1086
- // model call in the session then re-reads all of it.
1087
- // Each flag only on a CLI that accepts it: an unknown flag is an
1088
- // argument error that would fail every turn (CLAUDE_FLAG_FLOORS).
1089
- // The MCP half has a switch (Plugins → MCP servers → "Also use my
1090
- // Claude Code MCP servers"): with it on, the CLI loads the servers
1091
- // and connectors from the person's own Claude Code config — the way
1092
- // Codex's `features.codexUserMcp` does for config.toml — while skills, hooks and the
1093
- // personal CLAUDE.md stay out.
1085
+ const isolated = plan || !inheritsUserConfig(turnEnvironment);
1086
+ if (isolated && !plan) {
1094
1087
  if (!turn.mcpFromUserConfig && claudeCliSupports(cliVersion, "--strict-mcp-config"))
1095
1088
  args.push("--strict-mcp-config");
1096
1089
  if (claudeCliSupports(cliVersion, "--setting-sources"))
@@ -1207,7 +1200,7 @@ export const ClaudeDriver = {
1207
1200
  // of MCP servers, so a server the bot's OWN project declares would
1208
1201
  // otherwise vanish with the machine's. Merge it last: a project file
1209
1202
  // can add servers but never shadow a harness-owned mount.
1210
- if (isolated && turn.cwd) {
1203
+ if (isolated && turn.cwd && !plan) {
1211
1204
  for (const [name, server] of Object.entries(projectMcpServers(turn.cwd))) {
1212
1205
  if (Object.hasOwn(mcpServers, name))
1213
1206
  continue;
@@ -1250,6 +1243,12 @@ export const ClaudeDriver = {
1250
1243
  args.push("--allowedTools", allowed.join(","));
1251
1244
  }
1252
1245
  const env = environment(turnModel);
1246
+ if (plan) {
1247
+ env.OMB_PLAN_NODE = process.execPath;
1248
+ env.OMB_PLAN_HOOK = SPAWNED_PROXIES.planHook;
1249
+ if (process.versions.electron)
1250
+ env.ELECTRON_RUN_AS_NODE = "1";
1251
+ }
1253
1252
  const authSettings = isolated && !injected.injected
1254
1253
  ? readClaudeAuthSettings(env, input.environment) : {};
1255
1254
  // Harness hooks (item 0.2): one helper command for the events the
@@ -1286,6 +1285,7 @@ export const ClaudeDriver = {
1286
1285
  const hookBlock = {
1287
1286
  ...(hooks ? claudeHookSettings(HOOK_HELPER_PATH) : {}),
1288
1287
  ...(rtkHook ? { PreToolUse: rtkHookSettings(RTK_HOOK_PATH) } : {}),
1288
+ ...(plan ? { PreToolUse: claudePlanHook(process.execPath, SPAWNED_PROXIES.planHook) } : {}),
1289
1289
  };
1290
1290
  if (Object.keys(hookBlock).length)
1291
1291
  settings.hooks = hookBlock;
@@ -1404,10 +1404,9 @@ export const ClaudeDriver = {
1404
1404
  // process keeps its existing proxy connection and broker across turns.
1405
1405
  if (socketPath) {
1406
1406
  // remembers which tool each pending ask came from, so the resolved
1407
- // event can scope approvals to real desktop-control tools only
1408
1407
  const askTools = new Map();
1409
1408
  broker = await createPermissionBroker({
1410
- socketPaths: brokerSocketCandidates(threadId, botId),
1409
+ socketPaths: brokerSocketCandidates(threadId, botId), plan,
1411
1410
  isActive: () => Boolean(sessions.get(threadId)?.turn),
1412
1411
  inputOnAllow: (ask) => {
1413
1412
  const current = sessions.get(threadId)?.turn?.input ?? turn;
@@ -1455,6 +1454,10 @@ export const ClaudeDriver = {
1455
1454
  });
1456
1455
  },
1457
1456
  onResolve: (resolved) => {
1457
+ if (plan && resolved.behavior === "deny") {
1458
+ emit({ ...base(threadId, turnId), type: "item.started", itemType: "tool", itemId: resolved.id, title: "Plan blocked an action" });
1459
+ emit({ ...base(threadId, turnId), type: "item.completed", itemType: "tool", itemId: resolved.id, ok: false, output: PLAN_REFUSAL });
1460
+ }
1458
1461
  const eventTurnId = sessions.get(threadId)?.turn?.turnId ?? turnId;
1459
1462
  emit({
1460
1463
  ...base(threadId, eventTurnId),
@@ -0,0 +1,34 @@
1
+ // Explicit on start, resume and every turn so elevated permissions never stick.
2
+ export function namedApprovalParams(mode) {
3
+ if (mode === "plan")
4
+ return {
5
+ thread: { approvalPolicy: "never", approvalsReviewer: "user", sandbox: "read-only" },
6
+ turn: { approvalPolicy: "never", approvalsReviewer: "user", sandboxPolicy: { type: "readOnly" } },
7
+ };
8
+ if (mode === "full") {
9
+ return {
10
+ thread: {
11
+ approvalPolicy: "never",
12
+ approvalsReviewer: "user",
13
+ sandbox: "danger-full-access",
14
+ },
15
+ turn: {
16
+ approvalPolicy: "never",
17
+ approvalsReviewer: "user",
18
+ sandboxPolicy: { type: "dangerFullAccess" },
19
+ },
20
+ };
21
+ }
22
+ return {
23
+ thread: {
24
+ approvalPolicy: "on-request",
25
+ approvalsReviewer: mode === "auto" ? "auto_review" : "user",
26
+ sandbox: "workspace-write",
27
+ },
28
+ turn: {
29
+ approvalPolicy: "on-request",
30
+ approvalsReviewer: mode === "auto" ? "auto_review" : "user",
31
+ sandboxPolicy: { type: "workspaceWrite" },
32
+ },
33
+ };
34
+ }