viberoom 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +12 -0
- package/README.md +119 -96
- package/dist/hub.js +18 -0
- package/dist/launcher.js +22 -0
- package/dist/main.js +4 -3
- package/dist/persona.js +1 -1
- package/dist/recipes.js +37 -14
- package/dist/room.js +3 -2
- package/dist/server.js +5 -0
- package/package.json +8 -5
- package/scripts/vendor-acp.mjs +85 -0
- package/ui/app.css +7 -0
- package/ui/app.js +47 -2
- package/ui/index.html +2 -0
- package/vendor/acp/claude-agent-acp/LICENSE +191 -0
- package/vendor/acp/claude-agent-acp/dist/acp-agent.js +7694 -0
- package/vendor/acp/claude-agent-acp/dist/acp-subagents.js +13 -0
- package/vendor/acp/claude-agent-acp/dist/air-extension.js +63 -0
- package/vendor/acp/claude-agent-acp/dist/async-tasks.js +613 -0
- package/vendor/acp/claude-agent-acp/dist/clear-context-coordinator.js +80 -0
- package/vendor/acp/claude-agent-acp/dist/elicitation.js +304 -0
- package/vendor/acp/claude-agent-acp/dist/exit-plan.js +154 -0
- package/vendor/acp/claude-agent-acp/dist/file-change-audit.js +350 -0
- package/vendor/acp/claude-agent-acp/dist/fork-session.js +41 -0
- package/vendor/acp/claude-agent-acp/dist/goal-extension.js +50 -0
- package/vendor/acp/claude-agent-acp/dist/index.js +98 -0
- package/vendor/acp/claude-agent-acp/dist/lib.js +5 -0
- package/vendor/acp/claude-agent-acp/dist/native-subagents.js +422 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/effects.js +166 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/modes.js +41 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/normalization.js +100 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/filesystem.js +124 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/shared.js +64 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/shell.js +100 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/tools.js +135 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options.js +60 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/presentation.js +83 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/response.js +19 -0
- package/vendor/acp/claude-agent-acp/dist/session-config-ids.js +4 -0
- package/vendor/acp/claude-agent-acp/dist/session-failure-extension.js +324 -0
- package/vendor/acp/claude-agent-acp/dist/session-mode.js +234 -0
- package/vendor/acp/claude-agent-acp/dist/session-titles.js +199 -0
- package/vendor/acp/claude-agent-acp/dist/settings.js +185 -0
- package/vendor/acp/claude-agent-acp/dist/tool-result-meta.js +19 -0
- package/vendor/acp/claude-agent-acp/dist/tools.js +1235 -0
- package/vendor/acp/claude-agent-acp/dist/utils.js +81 -0
- package/vendor/acp/claude-agent-acp/package.json +7 -0
- package/vendor/acp/claude-agent-sdk/LICENSE.md +1 -0
- package/vendor/acp/claude-agent-sdk/agentSdkTypes.d.ts +1 -0
- package/vendor/acp/claude-agent-sdk/bridge.d.ts +378 -0
- package/vendor/acp/claude-agent-sdk/bridge.mjs +221 -0
- package/vendor/acp/claude-agent-sdk/browser-sdk.d.ts +107 -0
- package/vendor/acp/claude-agent-sdk/browser-sdk.js +185 -0
- package/vendor/acp/claude-agent-sdk/extractFromBunfs.d.ts +1 -0
- package/vendor/acp/claude-agent-sdk/extractFromBunfs.js +156 -0
- package/vendor/acp/claude-agent-sdk/manifest.json +65 -0
- package/vendor/acp/claude-agent-sdk/manifest.zst.json +73 -0
- package/vendor/acp/claude-agent-sdk/package.json +7 -0
- package/vendor/acp/claude-agent-sdk/sdk-tools.d.ts +4129 -0
- package/vendor/acp/claude-agent-sdk/sdk.d.ts +8687 -0
- package/vendor/acp/claude-agent-sdk/sdk.mjs +204 -0
- package/vendor/acp/codex-acp/LICENSE +190 -0
- package/vendor/acp/codex-acp/dist/index.js +34238 -0
- package/vendor/acp/codex-acp/package.json +7 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
const MAX_PENDING_PARENTS = 64;
|
|
2
|
+
const MAX_PENDING_UPDATES = 256;
|
|
3
|
+
const MAX_PENDING_UPDATES_PER_PARENT = 32;
|
|
4
|
+
/**
|
|
5
|
+
* Owns the connection-local native subagent registry and all ACP lifecycle
|
|
6
|
+
* ordering. The main agent only supplies SDK facts and delivers routed output.
|
|
7
|
+
*/
|
|
8
|
+
export class NativeSubagentRuntime {
|
|
9
|
+
rootSessionId;
|
|
10
|
+
session;
|
|
11
|
+
publish;
|
|
12
|
+
logger;
|
|
13
|
+
enabled;
|
|
14
|
+
children;
|
|
15
|
+
taskByToolUse;
|
|
16
|
+
parentByToolUse;
|
|
17
|
+
identityByToolUse = new Map();
|
|
18
|
+
controlByToolUse = new Map();
|
|
19
|
+
childByParentToolUse = new Map();
|
|
20
|
+
taskFinishPromises = new Map();
|
|
21
|
+
generationByTaskId = new Map();
|
|
22
|
+
pending = new Map();
|
|
23
|
+
pendingCount = 0;
|
|
24
|
+
constructor(enabled, rootSessionId, session, publish, logger) {
|
|
25
|
+
this.rootSessionId = rootSessionId;
|
|
26
|
+
this.session = session;
|
|
27
|
+
this.publish = publish;
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
this.enabled = enabled;
|
|
30
|
+
this.children = session.nativeSubagentsByTaskId ??= new Map();
|
|
31
|
+
this.taskByToolUse = session.nativeSubagentTaskIdByToolUseId ??= new Map();
|
|
32
|
+
this.parentByToolUse = session.nativeSubagentParentByToolUseId ??= new Map();
|
|
33
|
+
for (const child of this.children.values()) {
|
|
34
|
+
if (child.parentToolUseId) {
|
|
35
|
+
this.childByParentToolUse.set(child.parentToolUseId, child);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async route(notification, deliver, forcedSessionId) {
|
|
40
|
+
const { update } = notification;
|
|
41
|
+
const claudeMeta = update._meta?.claudeCode;
|
|
42
|
+
const isControl = isNativeSubagentControlUpdate(update);
|
|
43
|
+
if (!this.enabled)
|
|
44
|
+
return notification;
|
|
45
|
+
if (this.enabled && isControl) {
|
|
46
|
+
const toolCallId = update.toolCallId;
|
|
47
|
+
if (update.sessionUpdate === "tool_call") {
|
|
48
|
+
this.controlByToolUse.set(toolCallId, notification);
|
|
49
|
+
}
|
|
50
|
+
const identity = subagentIdentity(update.rawInput);
|
|
51
|
+
if (identity) {
|
|
52
|
+
this.identityByToolUse.set(toolCallId, mergeSubagentIdentity(this.identityByToolUse.get(toolCallId), identity));
|
|
53
|
+
}
|
|
54
|
+
const parentSessionId = claudeMeta?.parentToolUseId
|
|
55
|
+
? this.childByParentToolUse.get(claudeMeta.parentToolUseId)?.sessionId
|
|
56
|
+
: this.rootSessionId;
|
|
57
|
+
this.parentByToolUse.set(toolCallId, parentSessionId ?? this.rootSessionId);
|
|
58
|
+
const child = this.childByParentToolUse.get(toolCallId);
|
|
59
|
+
if (child && !child.announced) {
|
|
60
|
+
child.parentSessionId = parentSessionId ?? this.rootSessionId;
|
|
61
|
+
applySubagentIdentity(child, this.identityByToolUse.get(toolCallId));
|
|
62
|
+
await announceNativeSubagent(child, this.publish);
|
|
63
|
+
for (const pending of this.takePending(toolCallId))
|
|
64
|
+
await deliver(pending);
|
|
65
|
+
}
|
|
66
|
+
if (!child && isFailedToolCallUpdate(update)) {
|
|
67
|
+
this.takePending(toolCallId);
|
|
68
|
+
const initial = this.controlByToolUse.get(toolCallId);
|
|
69
|
+
this.cleanupControl(toolCallId);
|
|
70
|
+
if (forcedSessionId) {
|
|
71
|
+
return { ...notification, sessionId: forcedSessionId };
|
|
72
|
+
}
|
|
73
|
+
return failedControlFallback(initial, notification, parentSessionId ?? this.rootSessionId);
|
|
74
|
+
}
|
|
75
|
+
return forcedSessionId ? { ...notification, sessionId: forcedSessionId } : null;
|
|
76
|
+
}
|
|
77
|
+
// A permission request may have had to create the tool call before native
|
|
78
|
+
// child ownership was known. Keep every later update in that original ACP
|
|
79
|
+
// session; moving a lifecycle after its initial call creates an orphan in
|
|
80
|
+
// both transcripts.
|
|
81
|
+
if (forcedSessionId)
|
|
82
|
+
return { ...notification, sessionId: forcedSessionId };
|
|
83
|
+
if (this.enabled && claudeMeta?.parentToolUseId) {
|
|
84
|
+
const child = this.childByParentToolUse.get(claudeMeta.parentToolUseId);
|
|
85
|
+
if (!child || !child.announced) {
|
|
86
|
+
this.buffer(claudeMeta.parentToolUseId, notification);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (child.terminalState !== undefined || child.terminalPromise) {
|
|
90
|
+
this.logger.log(`Session ${this.rootSessionId}: ignoring late update for terminal subagent ${child.sessionId}`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return { ...notification, sessionId: child.sessionId };
|
|
94
|
+
}
|
|
95
|
+
return notification;
|
|
96
|
+
}
|
|
97
|
+
async taskStarted(task, deliver) {
|
|
98
|
+
if (!this.enabled)
|
|
99
|
+
return;
|
|
100
|
+
if (!task.subagentType) {
|
|
101
|
+
if (task.toolUseId) {
|
|
102
|
+
this.takePending(task.toolUseId);
|
|
103
|
+
this.cleanupControl(task.toolUseId);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const previous = this.children.get(task.taskId);
|
|
108
|
+
if (previous && previous.terminalState === undefined)
|
|
109
|
+
return;
|
|
110
|
+
const knownParentSessionId = task.toolUseId
|
|
111
|
+
? this.parentByToolUse.get(task.toolUseId)
|
|
112
|
+
: undefined;
|
|
113
|
+
const identity = task.toolUseId ? this.identityByToolUse.get(task.toolUseId) : undefined;
|
|
114
|
+
const child = {
|
|
115
|
+
sessionId: this.nextChildSessionId(task.taskId, previous),
|
|
116
|
+
parentSessionId: knownParentSessionId ?? this.rootSessionId,
|
|
117
|
+
parentToolUseId: task.toolUseId ?? undefined,
|
|
118
|
+
name: subagentDisplayName(identity?.name, identity?.description ?? task.description, identity?.subagentType ?? task.subagentType, task.taskId),
|
|
119
|
+
task: subagentDescription(identity?.prompt ?? task.prompt, identity?.description ?? task.description),
|
|
120
|
+
};
|
|
121
|
+
this.children.set(task.taskId, child);
|
|
122
|
+
if (task.toolUseId) {
|
|
123
|
+
this.taskByToolUse.set(task.toolUseId, task.taskId);
|
|
124
|
+
this.childByParentToolUse.set(task.toolUseId, child);
|
|
125
|
+
this.controlByToolUse.delete(task.toolUseId);
|
|
126
|
+
}
|
|
127
|
+
// A nested child must wait for the spawning Agent/Task frame to establish
|
|
128
|
+
// its immediate parent. Root children without a tool id can be announced.
|
|
129
|
+
if (knownParentSessionId || !task.toolUseId) {
|
|
130
|
+
await announceNativeSubagent(child, this.publish);
|
|
131
|
+
for (const pending of task.toolUseId ? this.takePending(task.toolUseId) : []) {
|
|
132
|
+
await deliver(pending);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async finishTask(taskId, status, deliver, toolUseId) {
|
|
137
|
+
if (!this.enabled)
|
|
138
|
+
return;
|
|
139
|
+
const state = nativeSubagentState(status);
|
|
140
|
+
const child = toolUseId ? this.childByParentToolUse.get(toolUseId) : this.children.get(taskId);
|
|
141
|
+
if (child && toolUseId && this.taskByToolUse.get(toolUseId) !== taskId)
|
|
142
|
+
return;
|
|
143
|
+
if (!state || !child || child.terminalState !== undefined)
|
|
144
|
+
return;
|
|
145
|
+
const existing = this.taskFinishPromises.get(taskId);
|
|
146
|
+
if (existing)
|
|
147
|
+
return existing;
|
|
148
|
+
const finish = Promise.resolve().then(async () => {
|
|
149
|
+
try {
|
|
150
|
+
await announceNativeSubagent(child, this.publish);
|
|
151
|
+
if (child.parentToolUseId) {
|
|
152
|
+
for (const pending of this.takePending(child.parentToolUseId))
|
|
153
|
+
await deliver(pending);
|
|
154
|
+
}
|
|
155
|
+
await finishNativeSubagent(this.session, taskId, state, this.publish);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
if (child.parentToolUseId) {
|
|
159
|
+
this.cleanupControl(child.parentToolUseId);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
this.taskFinishPromises.set(taskId, finish);
|
|
164
|
+
try {
|
|
165
|
+
await finish;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
if (this.taskFinishPromises.get(taskId) === finish)
|
|
169
|
+
this.taskFinishPromises.delete(taskId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async finishAll(state, deliver) {
|
|
173
|
+
const errors = [];
|
|
174
|
+
try {
|
|
175
|
+
for (const taskId of [...this.children.keys()].reverse()) {
|
|
176
|
+
try {
|
|
177
|
+
await this.finishTask(taskId, state, deliver);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
errors.push(error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
this.pending.clear();
|
|
186
|
+
this.pendingCount = 0;
|
|
187
|
+
this.identityByToolUse.clear();
|
|
188
|
+
this.controlByToolUse.clear();
|
|
189
|
+
this.parentByToolUse.clear();
|
|
190
|
+
}
|
|
191
|
+
if (errors.length === 1)
|
|
192
|
+
throw errors[0];
|
|
193
|
+
if (errors.length > 1)
|
|
194
|
+
throw new AggregateError(errors, "Failed to finish native subagents");
|
|
195
|
+
}
|
|
196
|
+
discardPending(parentToolUseId) {
|
|
197
|
+
this.takePending(parentToolUseId);
|
|
198
|
+
}
|
|
199
|
+
clear() {
|
|
200
|
+
this.children.clear();
|
|
201
|
+
this.taskByToolUse.clear();
|
|
202
|
+
this.parentByToolUse.clear();
|
|
203
|
+
this.identityByToolUse.clear();
|
|
204
|
+
this.controlByToolUse.clear();
|
|
205
|
+
this.childByParentToolUse.clear();
|
|
206
|
+
this.taskFinishPromises.clear();
|
|
207
|
+
this.generationByTaskId.clear();
|
|
208
|
+
this.pending.clear();
|
|
209
|
+
this.pendingCount = 0;
|
|
210
|
+
}
|
|
211
|
+
takePending(parentToolUseId) {
|
|
212
|
+
const updates = this.pending.get(parentToolUseId) ?? [];
|
|
213
|
+
if (updates.length > 0) {
|
|
214
|
+
this.pending.delete(parentToolUseId);
|
|
215
|
+
this.pendingCount -= updates.length;
|
|
216
|
+
}
|
|
217
|
+
return updates;
|
|
218
|
+
}
|
|
219
|
+
buffer(parentToolUseId, notification) {
|
|
220
|
+
const updates = this.pending.get(parentToolUseId);
|
|
221
|
+
if (this.pendingCount >= MAX_PENDING_UPDATES ||
|
|
222
|
+
(updates === undefined && this.pending.size >= MAX_PENDING_PARENTS) ||
|
|
223
|
+
(updates?.length ?? 0) >= MAX_PENDING_UPDATES_PER_PARENT) {
|
|
224
|
+
this.logger.log(`Session ${this.rootSessionId}: dropping unattributed subagent update for ${parentToolUseId}; pending buffer limit reached`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (updates)
|
|
228
|
+
updates.push(notification);
|
|
229
|
+
else
|
|
230
|
+
this.pending.set(parentToolUseId, [notification]);
|
|
231
|
+
this.pendingCount++;
|
|
232
|
+
}
|
|
233
|
+
cleanupControl(toolUseId) {
|
|
234
|
+
this.identityByToolUse.delete(toolUseId);
|
|
235
|
+
this.controlByToolUse.delete(toolUseId);
|
|
236
|
+
this.parentByToolUse.delete(toolUseId);
|
|
237
|
+
}
|
|
238
|
+
nextChildSessionId(taskId, previous) {
|
|
239
|
+
if (!previous) {
|
|
240
|
+
this.generationByTaskId.set(taskId, 1);
|
|
241
|
+
return taskId;
|
|
242
|
+
}
|
|
243
|
+
const generation = (this.generationByTaskId.get(taskId) ?? 1) + 1;
|
|
244
|
+
this.generationByTaskId.set(taskId, generation);
|
|
245
|
+
return `${taskId}:generation:${generation}`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
export async function announceNativeSubagent(child, publish) {
|
|
249
|
+
if (child.announced)
|
|
250
|
+
return;
|
|
251
|
+
if (child.announcePromise)
|
|
252
|
+
return child.announcePromise;
|
|
253
|
+
const announce = Promise.resolve().then(async () => {
|
|
254
|
+
await publish({
|
|
255
|
+
sessionId: child.parentSessionId,
|
|
256
|
+
update: {
|
|
257
|
+
sessionUpdate: "subagent_spawned",
|
|
258
|
+
subagentSessionId: child.sessionId,
|
|
259
|
+
name: child.name,
|
|
260
|
+
task: child.task,
|
|
261
|
+
capabilities: {},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
child.announced = true;
|
|
265
|
+
});
|
|
266
|
+
child.announcePromise = announce;
|
|
267
|
+
try {
|
|
268
|
+
await announce;
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
if (child.announcePromise === announce)
|
|
272
|
+
child.announcePromise = undefined;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export async function finishNativeSubagent(session, taskId, state, publish) {
|
|
276
|
+
const child = session.nativeSubagentsByTaskId?.get(taskId);
|
|
277
|
+
if (!child || child.terminalState !== undefined)
|
|
278
|
+
return;
|
|
279
|
+
if (child.terminalPromise)
|
|
280
|
+
return child.terminalPromise;
|
|
281
|
+
const finish = Promise.resolve().then(async () => {
|
|
282
|
+
await announceNativeSubagent(child, publish);
|
|
283
|
+
await publish({
|
|
284
|
+
sessionId: child.parentSessionId,
|
|
285
|
+
update: {
|
|
286
|
+
sessionUpdate: "subagent_state_update",
|
|
287
|
+
subagentSessionId: child.sessionId,
|
|
288
|
+
state,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
child.terminalState = state;
|
|
292
|
+
});
|
|
293
|
+
child.terminalPromise = finish;
|
|
294
|
+
try {
|
|
295
|
+
await finish;
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
if (child.terminalPromise === finish)
|
|
299
|
+
child.terminalPromise = undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export function nativeSubagentState(status) {
|
|
303
|
+
if (status === "completed")
|
|
304
|
+
return "completed";
|
|
305
|
+
if (status === "failed")
|
|
306
|
+
return "failed";
|
|
307
|
+
if (status === "disconnected")
|
|
308
|
+
return "disconnected";
|
|
309
|
+
if (status === "killed" || status === "cancelled" || status === "stopped")
|
|
310
|
+
return "cancelled";
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
export function isNativeSubagentControlUpdate(update) {
|
|
314
|
+
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
const claudeMeta = update._meta?.claudeCode;
|
|
318
|
+
return claudeMeta?.subagent === true || isNativeSubagentControlTool(claudeMeta?.toolName);
|
|
319
|
+
}
|
|
320
|
+
export function isNativeSubagentControlTool(toolName) {
|
|
321
|
+
return toolName === "Agent" || toolName === "Task";
|
|
322
|
+
}
|
|
323
|
+
function isFailedToolCallUpdate(update) {
|
|
324
|
+
return ((update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") &&
|
|
325
|
+
update.status === "failed");
|
|
326
|
+
}
|
|
327
|
+
function failedControlFallback(initial, terminal, sessionId) {
|
|
328
|
+
if (terminal.update.sessionUpdate !== "tool_call" &&
|
|
329
|
+
terminal.update.sessionUpdate !== "tool_call_update") {
|
|
330
|
+
return { ...terminal, sessionId };
|
|
331
|
+
}
|
|
332
|
+
if (!initial || initial.update.sessionUpdate !== "tool_call") {
|
|
333
|
+
const claudeMeta = terminal.update._meta?.claudeCode;
|
|
334
|
+
return {
|
|
335
|
+
...terminal,
|
|
336
|
+
sessionId,
|
|
337
|
+
update: {
|
|
338
|
+
...terminal.update,
|
|
339
|
+
sessionUpdate: "tool_call",
|
|
340
|
+
status: "failed",
|
|
341
|
+
title: typeof terminal.update.title === "string" && terminal.update.title.length > 0
|
|
342
|
+
? terminal.update.title
|
|
343
|
+
: claudeMeta?.toolName === "Task"
|
|
344
|
+
? "Task"
|
|
345
|
+
: "Agent",
|
|
346
|
+
_meta: ordinaryToolMeta(terminal.update._meta),
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
...initial,
|
|
352
|
+
sessionId,
|
|
353
|
+
update: {
|
|
354
|
+
...initial.update,
|
|
355
|
+
...terminal.update,
|
|
356
|
+
sessionUpdate: "tool_call",
|
|
357
|
+
status: "failed",
|
|
358
|
+
title: typeof terminal.update.title === "string" && terminal.update.title.trim().length > 0
|
|
359
|
+
? terminal.update.title
|
|
360
|
+
: initial.update.title,
|
|
361
|
+
_meta: {
|
|
362
|
+
...initial.update._meta,
|
|
363
|
+
...terminal.update._meta,
|
|
364
|
+
...ordinaryToolMeta(initial.update._meta, terminal.update._meta),
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function ordinaryToolMeta(...values) {
|
|
370
|
+
const merged = Object.assign({}, ...values);
|
|
371
|
+
const claudeCode = Object.assign({}, ...values.map((value) => value?.claudeCode ?? {}));
|
|
372
|
+
delete claudeCode.subagent;
|
|
373
|
+
return { ...merged, claudeCode };
|
|
374
|
+
}
|
|
375
|
+
function subagentDisplayName(explicitName, description, type, taskId) {
|
|
376
|
+
for (const value of [explicitName, description, type]) {
|
|
377
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
378
|
+
return value.trim();
|
|
379
|
+
}
|
|
380
|
+
const suffix = taskId.length > 8 ? taskId.slice(-8) : taskId;
|
|
381
|
+
return `Agent ${suffix}`;
|
|
382
|
+
}
|
|
383
|
+
function subagentDescription(prompt, description) {
|
|
384
|
+
for (const value of [prompt, description]) {
|
|
385
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
386
|
+
return value.trim();
|
|
387
|
+
}
|
|
388
|
+
return "Delegated task";
|
|
389
|
+
}
|
|
390
|
+
function subagentIdentity(input) {
|
|
391
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
392
|
+
return undefined;
|
|
393
|
+
const value = input;
|
|
394
|
+
const identity = {
|
|
395
|
+
name: nonBlankString(value.name),
|
|
396
|
+
description: nonBlankString(value.description),
|
|
397
|
+
prompt: nonBlankString(value.prompt),
|
|
398
|
+
subagentType: nonBlankString(value.subagent_type),
|
|
399
|
+
};
|
|
400
|
+
return Object.values(identity).some(Boolean) ? identity : undefined;
|
|
401
|
+
}
|
|
402
|
+
function mergeSubagentIdentity(previous, next) {
|
|
403
|
+
return {
|
|
404
|
+
name: next.name ?? previous?.name,
|
|
405
|
+
description: next.description ?? previous?.description,
|
|
406
|
+
prompt: next.prompt ?? previous?.prompt,
|
|
407
|
+
subagentType: next.subagentType ?? previous?.subagentType,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function applySubagentIdentity(child, identity) {
|
|
411
|
+
if (!identity)
|
|
412
|
+
return;
|
|
413
|
+
if (identity.name || identity.description) {
|
|
414
|
+
child.name = subagentDisplayName(identity.name, identity.description, identity.subagentType, child.sessionId);
|
|
415
|
+
}
|
|
416
|
+
if (identity.prompt || identity.description) {
|
|
417
|
+
child.task = subagentDescription(identity.prompt, identity.description);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function nonBlankString(value) {
|
|
421
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
422
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { PERMISSION_OPTION_ID } from "./options.js";
|
|
2
|
+
/** Parse the ACP envelope once before dispatching to a tool-specific effect. */
|
|
3
|
+
export function parseClaudePermissionSelection(response, toolName) {
|
|
4
|
+
if (response.outcome?.outcome !== "selected")
|
|
5
|
+
throw new Error("Tool use aborted");
|
|
6
|
+
const optionId = response.outcome.optionId;
|
|
7
|
+
const contextResetMode = toolName === "ExitPlanMode" ? exitPlanClearContextMode(optionId) : undefined;
|
|
8
|
+
return { optionId, ...(contextResetMode ? { contextResetMode } : {}) };
|
|
9
|
+
}
|
|
10
|
+
function allow(context, updatedPermissions, permanent = false) {
|
|
11
|
+
return {
|
|
12
|
+
behavior: "allow",
|
|
13
|
+
updatedInput: context.input,
|
|
14
|
+
...(updatedPermissions ? { updatedPermissions } : {}),
|
|
15
|
+
toolUseID: context.toolUseID,
|
|
16
|
+
decisionClassification: permanent ? "user_permanent" : "user_temporary",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function deny(context, message = "User refused permission to run tool", interrupt = false) {
|
|
20
|
+
return {
|
|
21
|
+
behavior: "deny",
|
|
22
|
+
message,
|
|
23
|
+
...(interrupt ? { interrupt: true } : {}),
|
|
24
|
+
toolUseID: context.toolUseID,
|
|
25
|
+
decisionClassification: "user_reject",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function skillName(input) {
|
|
29
|
+
const value = typeof input.skill === "string" ? input.skill.trim() : "";
|
|
30
|
+
if (!value || value.length > 1_000)
|
|
31
|
+
throw new Error("Invalid Skill permission input");
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function localAllowRuleUpdate(toolName, ruleContent) {
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
type: "addRules",
|
|
38
|
+
rules: [{ toolName, ...(ruleContent === undefined ? {} : { ruleContent }) }],
|
|
39
|
+
behavior: "allow",
|
|
40
|
+
destination: "localSettings",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function webFetchPermissionUpdate(input) {
|
|
45
|
+
if (typeof input.url !== "string")
|
|
46
|
+
throw new Error("Invalid WebFetch permission input");
|
|
47
|
+
let hostname;
|
|
48
|
+
try {
|
|
49
|
+
hostname = new URL(input.url).hostname;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error("Invalid WebFetch permission input");
|
|
53
|
+
}
|
|
54
|
+
if (!hostname)
|
|
55
|
+
throw new Error("Invalid WebFetch permission input");
|
|
56
|
+
return localAllowRuleUpdate("WebFetch", `domain:${hostname}`);
|
|
57
|
+
}
|
|
58
|
+
function applySkillSelection(selection, context) {
|
|
59
|
+
switch (selection.optionId) {
|
|
60
|
+
case PERMISSION_OPTION_ID.allowSkillExact: {
|
|
61
|
+
return allow(context, localAllowRuleUpdate("Skill", skillName(context.input)), true);
|
|
62
|
+
}
|
|
63
|
+
case PERMISSION_OPTION_ID.allowSkillPrefix: {
|
|
64
|
+
const skill = skillName(context.input);
|
|
65
|
+
const spaceIndex = skill.indexOf(" ");
|
|
66
|
+
if (spaceIndex <= 0)
|
|
67
|
+
throw new Error("Skill prefix permission requires arguments");
|
|
68
|
+
return allow(context, localAllowRuleUpdate("Skill", `${skill.slice(0, spaceIndex)}:*`), true);
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
return applyCommonSelection(selection, context);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function exitPlanMode(optionId) {
|
|
75
|
+
switch (optionId) {
|
|
76
|
+
case PERMISSION_OPTION_ID.exitPlanBypass:
|
|
77
|
+
return "bypassPermissions";
|
|
78
|
+
case PERMISSION_OPTION_ID.exitPlanAuto:
|
|
79
|
+
return "auto";
|
|
80
|
+
case PERMISSION_OPTION_ID.exitPlanAcceptEdits:
|
|
81
|
+
return "acceptEdits";
|
|
82
|
+
case PERMISSION_OPTION_ID.exitPlanDefault:
|
|
83
|
+
return "default";
|
|
84
|
+
default:
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function exitPlanClearContextMode(optionId) {
|
|
89
|
+
switch (optionId) {
|
|
90
|
+
case PERMISSION_OPTION_ID.exitPlanClearAuto:
|
|
91
|
+
return "auto";
|
|
92
|
+
case PERMISSION_OPTION_ID.exitPlanClearBypass:
|
|
93
|
+
return "bypassPermissions";
|
|
94
|
+
case PERMISSION_OPTION_ID.exitPlanClearAcceptEdits:
|
|
95
|
+
return "acceptEdits";
|
|
96
|
+
default:
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function applyExitPlanModeSelection(selection, context) {
|
|
101
|
+
if (selection.contextResetMode) {
|
|
102
|
+
// The adapter consumes this interrupt and continues the same ACP turn on a
|
|
103
|
+
// fresh Claude query. Returning allow here would execute ExitPlanMode in
|
|
104
|
+
// the old context before the handoff.
|
|
105
|
+
return deny(context, "User accepted the plan and requested a fresh context", true);
|
|
106
|
+
}
|
|
107
|
+
const mode = exitPlanMode(selection.optionId);
|
|
108
|
+
if (mode) {
|
|
109
|
+
return allow(context, [{ type: "setMode", mode, destination: "session" }], mode !== "default");
|
|
110
|
+
}
|
|
111
|
+
if (selection.optionId === PERMISSION_OPTION_ID.reject) {
|
|
112
|
+
// A regular deny lets Claude continue planning and ask another question.
|
|
113
|
+
// Interrupt stops this ACP turn; the adapter maps Claude's internal
|
|
114
|
+
// diagnostic for that intentional stop back to cancellation.
|
|
115
|
+
return deny(context, "User chose to keep planning", true);
|
|
116
|
+
}
|
|
117
|
+
return applyCommonSelection(selection, context);
|
|
118
|
+
}
|
|
119
|
+
function applyCommonSelection(selection, context) {
|
|
120
|
+
switch (selection.optionId) {
|
|
121
|
+
case PERMISSION_OPTION_ID.allowOnce:
|
|
122
|
+
return allow(context);
|
|
123
|
+
case PERMISSION_OPTION_ID.allowWithUpdates:
|
|
124
|
+
if (!context.durableChangeSet)
|
|
125
|
+
throw new Error("Invalid durable permission selection");
|
|
126
|
+
return allow(context, context.durableChangeSet.updates, true);
|
|
127
|
+
case PERMISSION_OPTION_ID.reject:
|
|
128
|
+
return deny(context);
|
|
129
|
+
default:
|
|
130
|
+
throw new Error(`Unknown permission option: ${selection.optionId}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function applyGeneratedDurableSelection(selection, context, updates) {
|
|
134
|
+
if (selection.optionId !== PERMISSION_OPTION_ID.allowWithUpdates) {
|
|
135
|
+
return applyCommonSelection(selection, context);
|
|
136
|
+
}
|
|
137
|
+
return allow(context, updates(), true);
|
|
138
|
+
}
|
|
139
|
+
/** Apply a parsed selection using the semantics of the tool that produced it. */
|
|
140
|
+
export function applyClaudePermissionSelection(selection, context) {
|
|
141
|
+
switch (context.toolName) {
|
|
142
|
+
case "Skill":
|
|
143
|
+
return applySkillSelection(selection, context);
|
|
144
|
+
case "WebFetch":
|
|
145
|
+
return applyGeneratedDurableSelection(selection, context, () => webFetchPermissionUpdate(context.input));
|
|
146
|
+
case "EnterPlanMode":
|
|
147
|
+
return applyCommonSelection(selection, context);
|
|
148
|
+
case "ExitPlanMode":
|
|
149
|
+
return applyExitPlanModeSelection(selection, context);
|
|
150
|
+
case "Read":
|
|
151
|
+
case "Bash":
|
|
152
|
+
case "PowerShell":
|
|
153
|
+
case "Glob":
|
|
154
|
+
case "Grep":
|
|
155
|
+
case "Edit":
|
|
156
|
+
case "Write":
|
|
157
|
+
case "NotebookEdit":
|
|
158
|
+
case "SandboxNetworkAccess":
|
|
159
|
+
return applyCommonSelection(selection, context);
|
|
160
|
+
default:
|
|
161
|
+
if (context.toolName.startsWith("mcp__")) {
|
|
162
|
+
return applyCommonSelection(selection, context);
|
|
163
|
+
}
|
|
164
|
+
return applyGeneratedDurableSelection(selection, context, () => localAllowRuleUpdate(context.toolName));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Bypass Permissions doesn't work if we are a root/sudo user.
|
|
2
|
+
const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0;
|
|
3
|
+
export const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX;
|
|
4
|
+
const PERMISSION_MODE_ALIASES = {
|
|
5
|
+
// Settings use user-facing, case-insensitive spellings while the SDK uses
|
|
6
|
+
// camel-cased wire values. Keep legacy shorthand accepted by Claude Code too.
|
|
7
|
+
auto: "auto",
|
|
8
|
+
default: "default",
|
|
9
|
+
// Claude Code 2.1.200 renamed the "default" mode to "Manual" and accepts
|
|
10
|
+
// `"defaultMode": "manual"` in settings.json; honor the same alias here.
|
|
11
|
+
manual: "default",
|
|
12
|
+
acceptedits: "acceptEdits",
|
|
13
|
+
dontask: "dontAsk",
|
|
14
|
+
plan: "plan",
|
|
15
|
+
bypasspermissions: "bypassPermissions",
|
|
16
|
+
bypass: "bypassPermissions",
|
|
17
|
+
};
|
|
18
|
+
export function resolvePermissionMode(defaultMode, logger = console) {
|
|
19
|
+
if (defaultMode === undefined) {
|
|
20
|
+
return "default";
|
|
21
|
+
}
|
|
22
|
+
if (typeof defaultMode !== "string") {
|
|
23
|
+
logger.error("Ignoring permissions.defaultMode from settings: expected a string.");
|
|
24
|
+
return "default";
|
|
25
|
+
}
|
|
26
|
+
const normalized = defaultMode.trim().toLowerCase();
|
|
27
|
+
if (normalized === "") {
|
|
28
|
+
logger.error("Ignoring permissions.defaultMode from settings: expected a non-empty string.");
|
|
29
|
+
return "default";
|
|
30
|
+
}
|
|
31
|
+
const mapped = PERMISSION_MODE_ALIASES[normalized];
|
|
32
|
+
if (!mapped) {
|
|
33
|
+
logger.error(`Ignoring permissions.defaultMode from settings: unknown value '${defaultMode}'.`);
|
|
34
|
+
return "default";
|
|
35
|
+
}
|
|
36
|
+
if (mapped === "bypassPermissions" && !ALLOW_BYPASS) {
|
|
37
|
+
logger.error("Ignoring permissions.defaultMode from settings: bypassPermissions is not available when running as root.");
|
|
38
|
+
return "default";
|
|
39
|
+
}
|
|
40
|
+
return mapped;
|
|
41
|
+
}
|