agentbox-app 0.1.12 → 0.1.13
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/main/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +57 -20
- package/main/agent-updates.mjs +9 -22
- package/main/analytics.mjs +30 -0
- package/main/claude-bin.mjs +45 -311
- package/main/claude-input.mjs +9 -3
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +16 -16
- package/main/claude-usage.mjs +82 -25
- package/main/codex-bin.mjs +51 -249
- package/main/codex-session.mjs +38 -3
- package/main/codex-threads.mjs +41 -8
- package/main/codex-watch.mjs +67 -3
- package/main/config.mjs +26 -4
- package/main/corner-tag.mjs +489 -0
- package/main/crash-report.mjs +26 -3
- package/main/engine-bin.mjs +595 -0
- package/main/engine-setup.mjs +4 -15
- package/main/feedback.mjs +11 -6
- package/main/harnesses/claude-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +84 -0
- package/main/harnesses/codex.mjs +65 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +38 -0
- package/main/ipc.mjs +165 -31
- package/main/local-app-frames.mjs +82 -0
- package/main/main.mjs +100 -4
- package/main/memory-gate-history.mjs +51 -12
- package/main/memory-gate-server.mjs +67 -11
- package/main/memory-gate.mjs +49 -8
- package/main/memory-log.mjs +124 -0
- package/main/notify.mjs +47 -4
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-link.mjs +551 -0
- package/main/relay-address.mjs +28 -0
- package/main/relay-client.mjs +246 -0
- package/main/row-label.mjs +6 -6
- package/main/runs-on.mjs +32 -35
- package/main/serve.mjs +113 -9
- package/main/settings.mjs +148 -10
- package/main/shell-path.mjs +86 -0
- package/main/ship-queue.mjs +24 -6
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +27 -1
- package/main/start-hold.mjs +73 -0
- package/main/store.mjs +1 -1
- package/main/supervisor.mjs +832 -428
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +490 -98
- package/main/task-terminals.mjs +37 -7
- package/main/updater.mjs +71 -9
- package/main/worktree-include.mjs +494 -0
- package/package.json +10 -1
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
- package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
- package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
- package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
- package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
- package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
- package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
- package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
- package/renderer/dist/corner-tag.html +13 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- package/shared/agent-import.mjs +2 -0
- package/shared/agents.d.mts +1 -0
- package/shared/bridge-map.mjs +18 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- package/shared/corner-tag.d.mts +14 -0
- package/shared/corner-tag.mjs +141 -0
- package/shared/engines.mjs +20 -5
- package/shared/harness-definitions.mjs +103 -0
- package/shared/notify-rules.d.mts +25 -0
- package/shared/notify-rules.mjs +19 -0
- package/shared/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +2 -0
- package/shared/rank.mjs +50 -0
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- package/shared/runs-on.d.mts +0 -10
- package/shared/runs-on.mjs +25 -49
- package/shared/shortcuts.mjs +10 -4
- package/shared/spawn-trouble.mjs +57 -6
- package/shared/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/usage.d.mts +3 -0
- package/shared/usage.mjs +7 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +25 -1
- package/shared/work-lines.mjs +1 -1
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
- package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// OPENCODE'S OWN TRANSPORT, AND WHY IT IS THE SERVER AND NOT THE CLI.
|
|
2
|
+
//
|
|
3
|
+
// Measured against a real OpenCode 1.18.35 on 2026-10-07. `opencode run` is
|
|
4
|
+
// the obvious road -- it is non-interactive, `--format json` prints one JSON
|
|
5
|
+
// object per line, and that is nearly the shape main/harnesses/claude-stream
|
|
6
|
+
// already reads. It is also the wrong road, and the measurement says so in one
|
|
7
|
+
// line. Run with `permission: { bash: "ask" }` in config, the CLI printed:
|
|
8
|
+
//
|
|
9
|
+
// ! permission requested: bash (echo probe-run-mode); auto-rejecting
|
|
10
|
+
//
|
|
11
|
+
// and handed the model "The user rejected permission to use this specific tool
|
|
12
|
+
// call." With no permission config at all it ran the command with no card.
|
|
13
|
+
// `--auto` approves everything not explicitly denied. So the CLI offers
|
|
14
|
+
// approve-everything or refuse-everything, and an Agentbox card is neither.
|
|
15
|
+
//
|
|
16
|
+
// The HTTP server does carry a real approval. `opencode serve` raises
|
|
17
|
+
// `permission.asked` on its `/event` stream with the command in
|
|
18
|
+
// `metadata.command`, and `POST /session/:id/permissions/:id` with
|
|
19
|
+
// `{ response: "once" }` releases it while `{ response: "reject" }` refuses it.
|
|
20
|
+
// Both measured end to end against the real binary, both answers, 200 each
|
|
21
|
+
// time, the tool running in the first case and coming back
|
|
22
|
+
// "The user rejected permission to use this specific tool call." in the second.
|
|
23
|
+
// That is the same card contract main/codex-approvals.mjs already fills, which
|
|
24
|
+
// is why this file exists and a second stream parser does not.
|
|
25
|
+
//
|
|
26
|
+
// AND IT IS WHY WE DO NOT PASS `--auto` ANYWHERE. A grep for it is a test
|
|
27
|
+
// (tests/opencode-asks-before-it-runs-a-command), because the flag would make
|
|
28
|
+
// every run quietly approve its own file writes and shell commands.
|
|
29
|
+
import { EventEmitter } from 'node:events';
|
|
30
|
+
import { Name } from '../shared/product-name.mjs';
|
|
31
|
+
|
|
32
|
+
/** How long a single HTTP call to the local server may take. */
|
|
33
|
+
const CALL_MS = 60_000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A client for one `opencode serve`.
|
|
37
|
+
*
|
|
38
|
+
* `password` is the server's own basic-auth secret, which Agentbox generates
|
|
39
|
+
* per server and which OpenCode reads from `OPENCODE_SERVER_PASSWORD`. It is
|
|
40
|
+
* NOT a provider credential: OpenCode loads those itself, from its own
|
|
41
|
+
* `~/.local/share/opencode/auth.json`. Agentbox never has one to pass, which
|
|
42
|
+
* is the rule in CLAUDE.md and the reason this harness can exist at all.
|
|
43
|
+
*/
|
|
44
|
+
export function openCodeClient({ baseUrl, password = null, username = 'opencode', fetchImpl = globalThis.fetch } = {}) {
|
|
45
|
+
if (!baseUrl) throw Error('openCodeClient needs the address of a running OpenCode server');
|
|
46
|
+
const root = String(baseUrl).replace(/\/+$/, '');
|
|
47
|
+
const authorization = password ? `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` : null;
|
|
48
|
+
|
|
49
|
+
const request = async (path, { method = 'GET', body, signal, stream = false } = {}) => {
|
|
50
|
+
const response = await fetchImpl(root + path, {
|
|
51
|
+
method,
|
|
52
|
+
headers: {
|
|
53
|
+
...(authorization ? { authorization } : {}),
|
|
54
|
+
...(body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
55
|
+
...(stream ? { accept: 'text/event-stream' } : {}),
|
|
56
|
+
},
|
|
57
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
58
|
+
signal: signal ?? AbortSignal.timeout(CALL_MS),
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) throw Error(`OpenCode answered ${response.status} for ${method} ${path}`);
|
|
61
|
+
if (stream) return response;
|
|
62
|
+
const text = await response.text();
|
|
63
|
+
if (!text) return null;
|
|
64
|
+
try { return JSON.parse(text); } catch { return text; }
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
url: root,
|
|
69
|
+
request,
|
|
70
|
+
createSession: (body = {}) => request('/session', { method: 'POST', body }),
|
|
71
|
+
prompt: (sessionId, body) => request(`/session/${encodeURIComponent(sessionId)}/prompt_async`, { method: 'POST', body }),
|
|
72
|
+
abort: sessionId => request(`/session/${encodeURIComponent(sessionId)}/abort`, { method: 'POST', body: {} }),
|
|
73
|
+
// `{ response }` and not `{ action }`: the field name is off the real
|
|
74
|
+
// OpenAPI document the running server serves at /doc.
|
|
75
|
+
replyPermission: (sessionId, permissionId, response) =>
|
|
76
|
+
request(`/session/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(permissionId)}`, { method: 'POST', body: { response } }),
|
|
77
|
+
summarize: (sessionId, model) =>
|
|
78
|
+
request(`/session/${encodeURIComponent(sessionId)}/summarize`, { method: 'POST', body: splitModel(model) }),
|
|
79
|
+
runCommand: (sessionId, body) => request(`/session/${encodeURIComponent(sessionId)}/command`, { method: 'POST', body }),
|
|
80
|
+
providers: () => request('/config/providers'),
|
|
81
|
+
commands: () => request('/command'),
|
|
82
|
+
config: () => request('/config'),
|
|
83
|
+
health: () => request('/global/health'),
|
|
84
|
+
events: ({ signal } = {}) => eventStream(request, signal),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `provider/model` as the two fields OpenCode's API wants.
|
|
90
|
+
*
|
|
91
|
+
* Its model ids are provider-qualified and the slash is part of the id people
|
|
92
|
+
* see (`opencode/big-pickle`, `anthropic/claude-sonnet-4-5`), but every server
|
|
93
|
+
* endpoint takes `providerID` and `modelID` apart. A model with no slash is not
|
|
94
|
+
* guessed at: it is left for the server's own default to answer, because
|
|
95
|
+
* inventing a provider here is how a run dies on a model nobody chose.
|
|
96
|
+
*/
|
|
97
|
+
export function splitModel(model) {
|
|
98
|
+
const text = typeof model === 'string' ? model.trim() : '';
|
|
99
|
+
const at = text.indexOf('/');
|
|
100
|
+
if (at <= 0 || at === text.length - 1) return {};
|
|
101
|
+
return { providerID: text.slice(0, at), modelID: text.slice(at + 1) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The `/event` stream, as an async iterable of `{ type, properties }`. */
|
|
105
|
+
function eventStream(request, outerSignal) {
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
outerSignal?.addEventListener('abort', () => controller.abort(), { once: true });
|
|
108
|
+
const opened = request('/event', { stream: true, signal: controller.signal });
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
close: () => controller.abort(),
|
|
112
|
+
async *[Symbol.asyncIterator]() {
|
|
113
|
+
const response = await opened;
|
|
114
|
+
const reader = response.body.getReader();
|
|
115
|
+
const decoder = new TextDecoder();
|
|
116
|
+
let buffer = '';
|
|
117
|
+
try {
|
|
118
|
+
while (true) {
|
|
119
|
+
const { value, done } = await reader.read();
|
|
120
|
+
if (done) return;
|
|
121
|
+
buffer += decoder.decode(value, { stream: true });
|
|
122
|
+
let index;
|
|
123
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
124
|
+
const line = buffer.slice(0, index).trim();
|
|
125
|
+
buffer = buffer.slice(index + 1);
|
|
126
|
+
if (!line.startsWith('data:')) continue;
|
|
127
|
+
const payload = line.slice(5).trim();
|
|
128
|
+
if (!payload) continue;
|
|
129
|
+
try { yield JSON.parse(payload); } catch { /* a half-written frame is not news */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
try { await reader.cancel(); } catch { /* already gone */ }
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The permission events, old and new, that mean "a card is wanted". */
|
|
140
|
+
const ASKED = new Set(['permission.asked', 'permission.v2.asked']);
|
|
141
|
+
/** What the reply endpoint calls yes and no. */
|
|
142
|
+
const ALLOW = 'once';
|
|
143
|
+
const DENY = 'reject';
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* ONE OPENCODE RUN, SHAPED LIKE A CHILD PROCESS.
|
|
147
|
+
*
|
|
148
|
+
* The supervisor's spawn path wants something with `stderr`, `on('event')` and
|
|
149
|
+
* one `exit`, because that is what a Claude Code child and a Codex worker both
|
|
150
|
+
* look like from there (main/supervisor.mjs, `_spawnWorker`). This is the third
|
|
151
|
+
* one, and it owns nothing the other two own: its events are OpenCode's own
|
|
152
|
+
* names, passed through untranslated, exactly as the Codex adapter passes its
|
|
153
|
+
* notifications through rather than pretending to be Claude's JSON lines.
|
|
154
|
+
*
|
|
155
|
+
* DENY IS THE ANSWER NOBODY HAS TO CHOOSE. A card that is never answered, a
|
|
156
|
+
* handler that throws, a spool that has gone away, a worker killed mid-question
|
|
157
|
+
* -- every one of those refuses the tool call. The opposite default would mean
|
|
158
|
+
* a crashed inbox silently approves shell commands, which is the one failure
|
|
159
|
+
* here that cannot be taken back.
|
|
160
|
+
*/
|
|
161
|
+
export function createOpenCodeWorker({
|
|
162
|
+
client,
|
|
163
|
+
plan = {},
|
|
164
|
+
cwd = null,
|
|
165
|
+
resumeSessionId = null,
|
|
166
|
+
onApproval = null,
|
|
167
|
+
title = null,
|
|
168
|
+
} = {}) {
|
|
169
|
+
if (!client) throw Error('createOpenCodeWorker needs a client for a running OpenCode server');
|
|
170
|
+
|
|
171
|
+
const worker = new EventEmitter();
|
|
172
|
+
worker.stderr = new EventEmitter();
|
|
173
|
+
worker.stdout = new EventEmitter();
|
|
174
|
+
worker.sessionId = resumeSessionId ?? null;
|
|
175
|
+
|
|
176
|
+
const say = text => {
|
|
177
|
+
const line = String(text ?? '').trim();
|
|
178
|
+
if (line) worker.stderr.emit('data', line);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
let ended = false;
|
|
182
|
+
let stream = null;
|
|
183
|
+
const answered = new Set();
|
|
184
|
+
|
|
185
|
+
const stop = async () => {
|
|
186
|
+
if (ended) return;
|
|
187
|
+
ended = true;
|
|
188
|
+
try { stream?.close(); } catch { /* already closed */ }
|
|
189
|
+
if (worker.sessionId) { try { await client.abort(worker.sessionId); } catch { /* the run is over either way */ } }
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* One card, and the reply that releases or refuses the tool.
|
|
194
|
+
*
|
|
195
|
+
* The id is remembered BEFORE the card is raised, so a request the stream
|
|
196
|
+
* repeats -- which it does, `permission.asked` can arrive again for a request
|
|
197
|
+
* still open -- costs one card and not two.
|
|
198
|
+
*/
|
|
199
|
+
const decide = async (type, properties) => {
|
|
200
|
+
const id = properties?.id;
|
|
201
|
+
const session = properties?.sessionID;
|
|
202
|
+
if (!id || session !== worker.sessionId || answered.has(id)) return;
|
|
203
|
+
answered.add(id);
|
|
204
|
+
|
|
205
|
+
let allowed = false;
|
|
206
|
+
try {
|
|
207
|
+
allowed = onApproval ? await onApproval(type, properties) === true : false;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
say(`${Name} could not ask about ${String(properties?.metadata?.command ?? 'a tool call')}: ${error?.message ?? error}`);
|
|
210
|
+
allowed = false;
|
|
211
|
+
}
|
|
212
|
+
if (ended) allowed = false;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await client.replyPermission(session, id, allowed ? ALLOW : DENY);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
say(`OpenCode did not take the answer to that question: ${error?.message ?? error}`);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const run = async () => {
|
|
222
|
+
try {
|
|
223
|
+
if (!worker.sessionId) {
|
|
224
|
+
const session = await client.createSession({ ...(title ? { title } : {}), ...(cwd ? { directory: cwd } : {}) });
|
|
225
|
+
worker.sessionId = session?.id ?? null;
|
|
226
|
+
if (!worker.sessionId) throw Error('OpenCode did not name the session it opened');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The stream is opened BEFORE the prompt. Opening it after is a race the
|
|
230
|
+
// fast answers win: `permission.asked` for the first tool call can beat
|
|
231
|
+
// our own subscription, and nothing would ever answer that card.
|
|
232
|
+
stream = client.events();
|
|
233
|
+
const reading = (async () => {
|
|
234
|
+
for await (const event of stream) {
|
|
235
|
+
if (ended) return;
|
|
236
|
+
const type = String(event?.type ?? '');
|
|
237
|
+
const properties = event?.properties ?? {};
|
|
238
|
+
worker.emit('event', type, properties);
|
|
239
|
+
if (ASKED.has(type)) { await decide(type, properties); continue; }
|
|
240
|
+
if (properties?.sessionID && properties.sessionID !== worker.sessionId) continue;
|
|
241
|
+
if (type === 'session.error') {
|
|
242
|
+
worker.error = properties?.error ?? 'OpenCode reported an error';
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (type === 'session.idle') return;
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
|
|
249
|
+
await client.prompt(worker.sessionId, {
|
|
250
|
+
...splitModel(plan.model),
|
|
251
|
+
...(plan.agent ? { agent: plan.agent } : {}),
|
|
252
|
+
parts: [{ type: 'text', text: String(plan.prompt ?? '') }],
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await reading;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
worker.error = error?.message ?? String(error);
|
|
258
|
+
say(worker.error);
|
|
259
|
+
} finally {
|
|
260
|
+
const already = ended;
|
|
261
|
+
ended = true;
|
|
262
|
+
try { stream?.close(); } catch { /* already closed */ }
|
|
263
|
+
if (!already && worker.sessionId) { try { await client.abort(worker.sessionId); } catch { /* nothing left to stop */ } }
|
|
264
|
+
worker.emit('exit', worker.error ? 1 : 0, null);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
worker.kill = () => { void stop(); };
|
|
269
|
+
worker.done = run();
|
|
270
|
+
return worker;
|
|
271
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// READING AN OPENCODE RUN, IN OPENCODE'S OWN WORDS.
|
|
2
|
+
//
|
|
3
|
+
// The same five readers Claude Code and Codex each provide -- capture, live
|
|
4
|
+
// text, summary, trace, activity -- over OpenCode's `/event` stream. Nothing
|
|
5
|
+
// here translates an OpenCode event into a pretend Claude JSON line or a
|
|
6
|
+
// pretend Codex notification, for the reason docs/harnesses.md gives: the
|
|
7
|
+
// vocabularies are not the same shape and a fake one loses what the real one
|
|
8
|
+
// carries. OpenCode's `edit` approval, for instance, arrives with a whole
|
|
9
|
+
// unified diff in it, which neither of the other two does.
|
|
10
|
+
//
|
|
11
|
+
// EVERY SHAPE BELOW WAS READ OFF A RUNNING OPENCODE 1.18.35 ON 2026-10-07,
|
|
12
|
+
// not off the docs, which do not enumerate the event payloads at all. The
|
|
13
|
+
// ones that matter:
|
|
14
|
+
//
|
|
15
|
+
// session.updated properties.info.{cost,tokens,title,model,directory}
|
|
16
|
+
// message.part.updated properties.part.type: text | reasoning | tool |
|
|
17
|
+
// step-start | step-finish
|
|
18
|
+
// message.part.delta properties.{partID,field,delta} ("field":"text")
|
|
19
|
+
// permission.asked properties.{id,sessionID,permission,patterns,
|
|
20
|
+
// metadata:{command} | metadata:{filepath,diff}}
|
|
21
|
+
// session.idle properties.sessionID
|
|
22
|
+
// session.error properties.{sessionID,error}
|
|
23
|
+
//
|
|
24
|
+
// A tool part carries `state.status` of pending | running | completed | error,
|
|
25
|
+
// with `state.input`, `state.output` and `state.error` beside it.
|
|
26
|
+
|
|
27
|
+
/** How much streamed prose is kept before it is dropped, as the others do. */
|
|
28
|
+
const SAYING_CAP = 20_000;
|
|
29
|
+
|
|
30
|
+
/** Tool names worth naming on the row, in the words a person would use. */
|
|
31
|
+
const TOOL_WORDS = {
|
|
32
|
+
bash: 'Bash', edit: 'Edit', write: 'Write', read: 'Read', glob: 'Glob',
|
|
33
|
+
grep: 'Grep', list: 'List', webfetch: 'Fetch', websearch: 'Search',
|
|
34
|
+
task: 'Task', todowrite: 'Todo', patch: 'Edit', skill: 'Skill',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const toolWord = name => {
|
|
38
|
+
const id = String(name ?? '').trim();
|
|
39
|
+
if (!id) return null;
|
|
40
|
+
return TOOL_WORDS[id.toLowerCase()] ?? id;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const partOf = properties => (properties && typeof properties === 'object' ? properties.part ?? null : null);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* WHAT THE RUN WAS, kept on the session as it goes.
|
|
47
|
+
*
|
|
48
|
+
* `sessionId` is the one OpenCode resumes by, and it is on nearly every event
|
|
49
|
+
* as `properties.sessionID`. It is taken from the first event that carries one
|
|
50
|
+
* rather than from the session-creation response, so a resumed run and a fresh
|
|
51
|
+
* one record it the same way.
|
|
52
|
+
*
|
|
53
|
+
* The result is the LAST assistant text of the turn. OpenCode has no single
|
|
54
|
+
* "here is the answer" event the way Codex's `turn/completed` does: prose
|
|
55
|
+
* arrives as `text` parts that are updated in place, and the final one is the
|
|
56
|
+
* answer. So each completed text part overwrites the last, and whatever is
|
|
57
|
+
* standing when the session goes idle is the result.
|
|
58
|
+
*/
|
|
59
|
+
export function captureOpenCodeEvent(session, type, properties) {
|
|
60
|
+
try {
|
|
61
|
+
if (!session || typeof type !== 'string') return;
|
|
62
|
+
const p = properties ?? {};
|
|
63
|
+
if (!session.sessionId && typeof p.sessionID === 'string' && p.sessionID) session.sessionId = p.sessionID;
|
|
64
|
+
|
|
65
|
+
if (type === 'session.updated') {
|
|
66
|
+
const info = p.info ?? {};
|
|
67
|
+
if (Number.isFinite(info.cost)) session.opencodeCost = info.cost;
|
|
68
|
+
if (info.tokens && typeof info.tokens === 'object') session.opencodeTokens = info.tokens;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (type === 'session.error') {
|
|
73
|
+
session.result = openCodeErrorText(p.error) ?? 'OpenCode stopped with an error it did not name.';
|
|
74
|
+
session.resultIsError = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (type === 'message.part.updated') {
|
|
79
|
+
const part = partOf(p);
|
|
80
|
+
if (!part) return;
|
|
81
|
+
// A WRITE THIS RUN MADE, so the card can say what changed. The `edit` and
|
|
82
|
+
// `write` tools report the file they touched in `state.input.filePath`,
|
|
83
|
+
// which is the only place a path appears for a change OpenCode has
|
|
84
|
+
// already applied.
|
|
85
|
+
if (part.type === 'tool' && part.state?.status === 'completed') {
|
|
86
|
+
const path = part.state?.input?.filePath ?? part.state?.input?.path;
|
|
87
|
+
if (typeof path === 'string' && path && (part.tool === 'edit' || part.tool === 'write' || part.tool === 'patch')) {
|
|
88
|
+
session.opencodeChange ??= [];
|
|
89
|
+
if (!session.opencodeChange.includes(path)) session.opencodeChange.push(path);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (part.type === 'step-finish') {
|
|
93
|
+
if (Number.isFinite(part.cost)) session.opencodeStepCost = part.cost;
|
|
94
|
+
if (part.tokens && typeof part.tokens === 'object') session.opencodeTokens = part.tokens;
|
|
95
|
+
}
|
|
96
|
+
// Only the assistant's prose is an answer. `reasoning` is the model
|
|
97
|
+
// thinking out loud and is never the result, which is why it is matched
|
|
98
|
+
// by name here rather than falling through to the text branch.
|
|
99
|
+
if (part.type === 'text') {
|
|
100
|
+
const text = String(part.text ?? '').trim();
|
|
101
|
+
if (text) { session.result = text; session.resultIsError = false; }
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// A reader that throws kills a run that was going fine. Same rule as the
|
|
107
|
+
// other two harnesses: read what parses and ignore what does not.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The error on `session.error`, which is an object and not a string. */
|
|
112
|
+
export function openCodeErrorText(error) {
|
|
113
|
+
if (!error) return null;
|
|
114
|
+
if (typeof error === 'string') return error.trim() || null;
|
|
115
|
+
const name = typeof error.name === 'string' ? error.name : '';
|
|
116
|
+
const message = typeof error.data?.message === 'string' ? error.data.message
|
|
117
|
+
: typeof error.message === 'string' ? error.message : '';
|
|
118
|
+
const joined = [name, message].filter(Boolean).join(': ');
|
|
119
|
+
return joined || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* THE PROSE AS IT IS TYPED, for the line on the row that moves.
|
|
124
|
+
*
|
|
125
|
+
* Deltas arrive as `message.part.delta` with a `field` saying which field of
|
|
126
|
+
* which part is growing. Only `text` is shown: `reasoning` deltas are the
|
|
127
|
+
* model's private thinking, and streaming those onto her row would put words
|
|
128
|
+
* on screen that are not what the run decided.
|
|
129
|
+
*/
|
|
130
|
+
export function openCodeStreamingText(session, type, properties) {
|
|
131
|
+
try {
|
|
132
|
+
if (!session) return false;
|
|
133
|
+
const p = properties ?? {};
|
|
134
|
+
|
|
135
|
+
if (type === 'message.part.updated') {
|
|
136
|
+
const part = partOf(p);
|
|
137
|
+
// A tool starting means the talking stopped. What is already typed stays
|
|
138
|
+
// until the trace carries the same words, exactly as Codex's reader does.
|
|
139
|
+
if (part?.type === 'text' && !String(part.text ?? '')) { session.saying = ''; session.sayingAt = Date.now(); return true; }
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (type !== 'message.part.delta') return false;
|
|
144
|
+
if (p.field !== 'text') return false;
|
|
145
|
+
const piece = String(p.delta ?? '');
|
|
146
|
+
if (!piece) return false;
|
|
147
|
+
// A delta for a reasoning part has the same `field: "text"`, so the part it
|
|
148
|
+
// belongs to is what tells them apart. Without a remembered prose part id
|
|
149
|
+
// nothing is streamed, which is the quiet answer rather than the wrong one.
|
|
150
|
+
if (session.opencodeProsePart && p.partID && p.partID !== session.opencodeProsePart) return false;
|
|
151
|
+
if (!session.sayingAt) session.sayingAt = Date.now();
|
|
152
|
+
const next = (session.saying ?? '') + piece;
|
|
153
|
+
session.saying = next.length > SAYING_CAP ? '' : next;
|
|
154
|
+
return true;
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Which part the prose is going into, so reasoning deltas can be told apart. */
|
|
161
|
+
export function rememberOpenCodeProsePart(session, type, properties) {
|
|
162
|
+
if (!session || type !== 'message.part.updated') return;
|
|
163
|
+
const part = partOf(properties);
|
|
164
|
+
if (part?.type === 'text' && typeof part.id === 'string') session.opencodeProsePart = part.id;
|
|
165
|
+
if (part?.type === 'step-start') session.opencodeProsePart = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** One short line for the row: what it just did. */
|
|
169
|
+
export function summarizeOpenCodeEvent(type, properties) {
|
|
170
|
+
try {
|
|
171
|
+
const p = properties ?? {};
|
|
172
|
+
if (type !== 'message.part.updated') return null;
|
|
173
|
+
const part = partOf(p);
|
|
174
|
+
if (!part) return null;
|
|
175
|
+
if (part.type === 'text') {
|
|
176
|
+
const text = String(part.text ?? '').trim();
|
|
177
|
+
return text ? text.slice(0, 300) : null;
|
|
178
|
+
}
|
|
179
|
+
if (part.type === 'tool' && part.state?.status === 'completed') {
|
|
180
|
+
const word = toolWord(part.tool);
|
|
181
|
+
return word ? `tool: ${word}` : null;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** One line for the run trace, the record she can open afterwards. */
|
|
190
|
+
export function traceOpenCodeEvent(type, properties) {
|
|
191
|
+
try {
|
|
192
|
+
const p = properties ?? {};
|
|
193
|
+
const at = new Date().toISOString().slice(11, 19);
|
|
194
|
+
|
|
195
|
+
if (type === 'session.error') {
|
|
196
|
+
const text = openCodeErrorText(p.error);
|
|
197
|
+
return text ? `${at} [error] ${text.slice(0, 300)}` : null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (type !== 'message.part.updated') return null;
|
|
201
|
+
const part = partOf(p);
|
|
202
|
+
if (!part) return null;
|
|
203
|
+
|
|
204
|
+
if (part.type === 'text') {
|
|
205
|
+
const text = String(part.text ?? '').trim();
|
|
206
|
+
return text ? `${at} ${text}` : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (part.type === 'tool' && (part.state?.status === 'completed' || part.state?.status === 'error')) {
|
|
210
|
+
const word = toolWord(part.tool);
|
|
211
|
+
if (!word) return null;
|
|
212
|
+
const input = part.state?.input ?? {};
|
|
213
|
+
const detail = String(input.command ?? input.filePath ?? input.path ?? input.pattern ?? '').trim();
|
|
214
|
+
const failed = part.state?.status === 'error' ? ' (refused)' : '';
|
|
215
|
+
return `${at} [${word}]${failed}${detail ? ` ${detail.slice(0, 200)}` : ''}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** What it is doing right now, for the row's activity line. */
|
|
225
|
+
export function openCodeActivity(type, properties) {
|
|
226
|
+
try {
|
|
227
|
+
if (type !== 'message.part.updated') return null;
|
|
228
|
+
const part = partOf(properties);
|
|
229
|
+
if (part?.type !== 'tool') return null;
|
|
230
|
+
if (part.state?.status !== 'pending' && part.state?.status !== 'running') return null;
|
|
231
|
+
const word = toolWord(part.tool);
|
|
232
|
+
return word ? { tool: word } : null;
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* THE APPROVAL, IN THE WORDS A CARD NEEDS.
|
|
240
|
+
*
|
|
241
|
+
* `bash` asks carry `metadata.command`. `edit` asks carry `metadata.filepath`
|
|
242
|
+
* AND `metadata.diff`, a whole unified diff of the change it wants to make --
|
|
243
|
+
* measured, not assumed. That is strictly more than a Codex patch approval
|
|
244
|
+
* arrives with, and it means an OpenCode card can show the change itself
|
|
245
|
+
* rather than a filename.
|
|
246
|
+
*/
|
|
247
|
+
export function openCodeApprovalCard(properties) {
|
|
248
|
+
const p = properties ?? {};
|
|
249
|
+
const kind = typeof p.permission === 'string' ? p.permission : '';
|
|
250
|
+
const metadata = p.metadata ?? {};
|
|
251
|
+
const command = typeof metadata.command === 'string' ? metadata.command.trim() : '';
|
|
252
|
+
const filepath = typeof metadata.filepath === 'string' ? metadata.filepath.trim() : '';
|
|
253
|
+
const diff = typeof metadata.diff === 'string' ? metadata.diff : '';
|
|
254
|
+
const patterns = Array.isArray(p.patterns) ? p.patterns.filter(x => typeof x === 'string') : [];
|
|
255
|
+
|
|
256
|
+
if (command) return { kind: kind || 'bash', title: 'Run a command', detail: command, command, patterns };
|
|
257
|
+
if (filepath) return { kind: kind || 'edit', title: 'Change a file', detail: filepath, filepath, diff, patterns };
|
|
258
|
+
return { kind: kind || 'tool', title: 'Use a tool', detail: patterns.join(', ') || kind || 'a tool', patterns };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* THE PERMISSION POLICY A WORKER RUNS UNDER, as OpenCode's own config.
|
|
263
|
+
*
|
|
264
|
+
* This is the whole reason Agentbox can draw a card for OpenCode at all.
|
|
265
|
+
* Measured 2026-10-07: with no permission config the server runs shell
|
|
266
|
+
* commands and writes files with no card; with this config it raises
|
|
267
|
+
* `permission.asked` and waits. The server reports back what it loaded at
|
|
268
|
+
* `GET /config`, which is how the adapter can tell the policy really took.
|
|
269
|
+
*
|
|
270
|
+
* `read`, `glob`, `grep` and `list` are deliberately left alone: a card for
|
|
271
|
+
* every file read is the 2026-08-05 card flood again, and reading inside the
|
|
272
|
+
* task's own folder is what the folder is for.
|
|
273
|
+
*/
|
|
274
|
+
export function openCodePermissionConfig() {
|
|
275
|
+
return {
|
|
276
|
+
$schema: 'https://opencode.ai/config.json',
|
|
277
|
+
permission: {
|
|
278
|
+
bash: 'ask',
|
|
279
|
+
edit: 'ask',
|
|
280
|
+
webfetch: 'ask',
|
|
281
|
+
websearch: 'ask',
|
|
282
|
+
external_directory: 'ask',
|
|
283
|
+
task: 'allow',
|
|
284
|
+
todowrite: 'allow',
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|