@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.
- package/dist/assets/{abnfDiagram-O67JEVCF-DUyzT6Xq.js → abnfDiagram-O67JEVCF-Dwjw0xC0.js} +1 -1
- package/dist/assets/{arc-B4-kO642.js → arc-B__BIHzb.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DgKdnNL2.js → architectureDiagram-NJMV4G6O-DQy7n3Ga.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-B6oV3kDr.js → blockDiagram-BEXU5L5S-moFo2nSA.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Dp0E43jx.js → c4Diagram-YGBWAQC7-C-dF8UVl.js} +1 -1
- package/dist/assets/channel-BQM13l0i.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Cfv1WVUo.js → chunk-2Q5K7J3B-DnF0mxAV.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-_-STY2yO.js → chunk-3FUC2YCW-Dp8Dsifv.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-C6MZm1cm.js → chunk-5VM5RSS4-D8fmZ4d8.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-C__aVIJy.js → chunk-GTNCS2PH-DNH2HZNQ.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-fSrARJuB.js → chunk-GWA4HPMP-BvL6j7pQ.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CJioJB6h.js → chunk-JWPE2WC7-Co0gUA5M.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-o6MBGCq_.js → chunk-NETBCI7D-Deu9tpCF.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-O-TFLbdc.js → chunk-WEXAMYUT-DKy8jAU5.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DYS9CQbl.js → chunk-XXDRQBXY-DQhvP8zg.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BC4bSH09.js → classDiagram-v2-NBCMYWYE-mhv3hMMV.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-7NKuC3-C.js → cose-bilkent-JH36ORCC-CR1nOQOe.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CuWv5cOw.js → cynefin-OW5HDTMX-Ddgkhxgt.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-D_l_JrG5.js → cynefinDiagram-VND7K2PF-4KbkrNhB.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-D7eucSmr.js → dagre-6A5THRUB-DNxtjbvT.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CADR5v1e.js → diagram-22UHCM2B-BRpvnOWX.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-DF_fBOZn.js → diagram-3UASUU5V-DsQVnKUb.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DE5p59WH.js → diagram-ATOU4E4O-BRZ5455_.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-vthvYooA.js → diagram-CDSNMT55-CidrDKp0.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-ByMHy4S-.js → diagram-MLGK6HIB-1hkZ8SsK.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-C9hKBUnO.js → diagram-MPIPVDR6-D1dWGFqn.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Djv6hWEl.js → ebnfDiagram-ZINNZB2B-DGjfEPY5.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CmUjf18z.js → elk-276RUBZZ-C5nvtTy7.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DkmXQUok.js → erDiagram-OPXOYQCR-CC0QBE1A.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-6pz2poYn.js → flowDiagram-KWPJA3E3-BthbP_HO.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-Cx9Fbw75.js → ganttDiagram-FUAMR5RP-BQ2wLSEI.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BR5MsYkP.js → gitGraphDiagram-X574FWY7-lTR8tNlq.js} +1 -1
- package/dist/assets/{index-BoF5cGZq.js → index-AWsWxgoq.js} +1 -1
- package/dist/assets/{index-Ckv52eaP.js → index-VagspC1z.js} +186 -186
- package/dist/assets/{infoDiagram-VRGFBTTK-DoF183Am.js → infoDiagram-VRGFBTTK-BgI9ZuT_.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-4T_-hM1-.js → ishikawaDiagram-OU5B5YK6-qPe6Rhk2.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-Ca0T3Ngd.js → journeyDiagram-ZHPQQLJL-CpbadZuP.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DNp_DAr9.js → kanban-definition-PNTS6WVX-D9kCb3-t.js} +1 -1
- package/dist/assets/{linear-COUoS2bF.js → linear-CdW4LT3g.js} +1 -1
- package/dist/assets/{mermaid.core-DQHowMIV.js → mermaid.core-BbPWTT56.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CMgO1Mkh.js → mindmap-definition-NLK3R4M7-BtOaXxvb.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BCVCZbsq.js → pegDiagram-GJSIUBJH-BtcTITAK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DxaMlX62.js → pieDiagram-5QR66LMP-Ch_--zjt.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-qUD0pkur.js → quadrantDiagram-O4NWA36T-Di8ygRal.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-Dc6rcAJB.js → railroadDiagram-XR7U4H2S-DxgesAmb.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DMTfI_lR.js → requirementDiagram-PLB6GJNP-DjH-jdCr.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-B0BWZys8.js → sankeyDiagram-IPEJSGJF-ClNLceSc.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DZVt17mL.js → sequenceDiagram-PO4LG4MO-Cip_EmtV.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BFQztpNW.js → sizeCapture-INFHLROL-BzW2hkq8.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-GeFCVCDr.js → stateDiagram-v2-GCMORJYK-CwHu7P19.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DTqQXTvP.js → swimlanes-2SLR337P-DO9E9HUK.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-UC0GNJzN.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BoiNrkP7.js → timeline-definition-EJHVYXUP-D2_E-i9h.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-CdfsSO2g.js → usecaseDiagram-POWQR4AR-oKBQ1FAG.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CHc_Fn1O.js → vennDiagram-UO4OBE2U-HEdQsC7L.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Bv3KWVEF.js → wardleyDiagram-VNRHLVJA-mNpRO8rd.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-CQFknLPS.js → xychartDiagram-PMCCYNJV-uURjTK1X.js} +1 -1
- package/dist/handbook/atomic-handbook.html +1 -1
- package/dist/index.html +1 -1
- package/dist-server/container-mcp.js +1 -0
- package/dist-server/hooks/plan-hook.js +22 -0
- package/dist-server/index.js +1277 -931
- package/dist-server/local-computer.js +1 -0
- package/dist-server/openmausbot.js +384 -298
- package/dist-server/pair-cli.js +384 -298
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/atomic-engine-release.js +13 -13
- package/dist-server/server/bot-work-modes.js +212 -0
- package/dist-server/server/config.js +6 -5
- package/dist-server/server/drivers/acp/core.js +6 -4
- package/dist-server/server/drivers/acp/custom.js +51 -37
- package/dist-server/server/drivers/builtIn.js +2 -1
- package/dist-server/server/drivers/chat-mcp-tools.js +1 -0
- package/dist-server/server/drivers/claude.js +28 -25
- package/dist-server/server/drivers/codex-approval-presets.js +34 -0
- package/dist-server/server/drivers/codex.js +11 -35
- package/dist-server/server/drivers/openai-chat.js +4 -3
- package/dist-server/server/hooks/plan-hook.js +17 -0
- package/dist-server/server/index.js +73 -57
- package/dist-server/server/mcp-interactive-probe.js +60 -0
- package/dist-server/server/mcp-remote-bridge.js +13 -6
- package/dist-server/server/plan-policy.js +14 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/store.js +4 -4
- package/dist-server/shared/approval-mode.js +3 -1
- package/dist-server/shared/engine-choices.js +1 -1
- package/dist-server/vps-container-mcp.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-BidDehbw.js +0 -1
- 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-
|
|
8
|
+
export const ATOMIC_ENGINE_VERSION = "0.21.4-4cc340e";
|
|
9
9
|
const ASSETS = [
|
|
10
10
|
{
|
|
11
|
-
version: "0.21.4-
|
|
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-
|
|
14
|
-
bytes:
|
|
15
|
-
sha512: "
|
|
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-
|
|
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-
|
|
21
|
-
bytes:
|
|
22
|
-
sha512: "
|
|
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-
|
|
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-
|
|
28
|
-
bytes:
|
|
29
|
-
sha512: "
|
|
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: "
|
|
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
|
-
//
|
|
1489
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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 (
|
|
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
|
+
}
|