@lucascouts/claude-agent-tui 0.5.1 → 0.6.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/dist/acp-agent.d.ts +187 -13
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +448 -60
- package/dist/agent-catalog.d.ts +96 -0
- package/dist/agent-catalog.d.ts.map +1 -0
- package/dist/agent-catalog.js +287 -0
- package/dist/claude-path.d.ts.map +1 -1
- package/dist/claude-path.js +6 -0
- package/dist/end-of-turn.d.ts +6 -0
- package/dist/end-of-turn.d.ts.map +1 -1
- package/dist/end-of-turn.js +8 -1
- package/dist/engine-lifecycle.d.ts +66 -1
- package/dist/engine-lifecycle.d.ts.map +1 -1
- package/dist/engine-lifecycle.js +43 -4
- package/dist/engine-pty.d.ts +70 -2
- package/dist/engine-pty.d.ts.map +1 -1
- package/dist/engine-pty.js +80 -6
- package/dist/gate/settings-writer.d.ts +34 -3
- package/dist/gate/settings-writer.d.ts.map +1 -1
- package/dist/gate/settings-writer.js +62 -7
- package/dist/image-input.d.ts +31 -0
- package/dist/image-input.d.ts.map +1 -0
- package/dist/image-input.js +79 -0
- package/dist/image-vision-smoke.d.ts +52 -0
- package/dist/image-vision-smoke.d.ts.map +1 -0
- package/dist/image-vision-smoke.js +111 -0
- package/dist/index.js +6 -0
- package/dist/mcp-config-writer.d.ts +61 -0
- package/dist/mcp-config-writer.d.ts.map +1 -0
- package/dist/mcp-config-writer.js +172 -0
- package/dist/model-catalog.d.ts +29 -2
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +50 -10
- package/dist/permissions/gate-wiring.d.ts +13 -1
- package/dist/permissions/gate-wiring.d.ts.map +1 -1
- package/dist/permissions/gate-wiring.js +158 -40
- package/dist/permissions/hook-server.d.ts +15 -0
- package/dist/permissions/hook-server.d.ts.map +1 -1
- package/dist/permissions/hook-server.js +30 -1
- package/dist/permissions/request-permission.d.ts +9 -0
- package/dist/permissions/request-permission.d.ts.map +1 -1
- package/dist/permissions/request-permission.js +20 -5
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +9 -0
- package/dist/tools.d.ts +10 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -1
- package/dist/usage.d.ts +3 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +9 -5
- package/package.json +8 -8
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// OFFLINE: this module spawns NO claude and bills nothing; the http server binds 127.0.0.1 only.
|
|
31
31
|
import * as os from "node:os";
|
|
32
32
|
import * as path from "node:path";
|
|
33
|
-
import { randomUUID } from "node:crypto";
|
|
33
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
34
34
|
import { findFreePort } from "../gate/port.js";
|
|
35
35
|
import { injectHook, restore } from "../gate/settings-writer.js";
|
|
36
36
|
import { startHookServer } from "./hook-server.js";
|
|
@@ -86,6 +86,13 @@ export const DEFAULT_CORRELATION_POLL_MS = 50;
|
|
|
86
86
|
* fail-closed timeout deny. Tracked separately from the poll interval (the poll is 10-50ms; nudging on
|
|
87
87
|
* every poll would hammer the pump) — a nudge fires only once ~250ms has elapsed since the last one. */
|
|
88
88
|
export const DEFAULT_CORRELATION_RENUDGE_MS = 250;
|
|
89
|
+
/**
|
|
90
|
+
* FIX(watchdog-permission): while a permission dialog is pending the claude is BLOCKED and writes
|
|
91
|
+
* nothing to the JSONL, so the end-of-turn watchdog (120s of silence) would mistake a long human
|
|
92
|
+
* decision for a dead turn. Re-arm it every this-many ms via the bound `noteActivity` for as long as
|
|
93
|
+
* the dialog is open. Must stay well under TURN_STALL_WATCHDOG_MS (120_000).
|
|
94
|
+
*/
|
|
95
|
+
export const DEFAULT_PERMISSION_HEARTBEAT_MS = 30_000;
|
|
89
96
|
/** Default window for the native prompt to APPEAR after an allow decision (#52822 sweep, ms).
|
|
90
97
|
* If no marker renders within it, allow-suppression held (the 2.1.161 case) — nothing to clear. */
|
|
91
98
|
export const DEFAULT_PROMPT_APPEAR_MS = 1500;
|
|
@@ -112,6 +119,8 @@ class SessionGateImpl {
|
|
|
112
119
|
constructor(opts) {
|
|
113
120
|
this.opts = opts;
|
|
114
121
|
this.port = 0;
|
|
122
|
+
/** Story 055 (R1.3) — set in {@link start} to a crypto-random per-session secret. */
|
|
123
|
+
this.token = "";
|
|
115
124
|
this.settingsPath = "";
|
|
116
125
|
this.correlator = new ToolUseCorrelator();
|
|
117
126
|
/** Rolling tail of recent PTY output + absolute count of chars ever appended (probe offsets). */
|
|
@@ -141,14 +150,46 @@ class SessionGateImpl {
|
|
|
141
150
|
this.permissionQueue = run.then(() => undefined, () => undefined);
|
|
142
151
|
return run;
|
|
143
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* FIX(watchdog-permission): start a heartbeat that re-arms the end-of-turn watchdog (via the bound
|
|
155
|
+
* `noteActivity`) every {@link DEFAULT_PERMISSION_HEARTBEAT_MS} while a permission dialog is open, so
|
|
156
|
+
* the human decision time is never counted as transcript silence. Self-reschedules through the
|
|
157
|
+
* injectable `schedule` (testable). Returns a stop fn; safe/no-op when `noteActivity` is unset.
|
|
158
|
+
*/
|
|
159
|
+
startPermissionHeartbeat() {
|
|
160
|
+
const note = this.noteActivity;
|
|
161
|
+
if (!note)
|
|
162
|
+
return () => { };
|
|
163
|
+
let stopped = false;
|
|
164
|
+
const tick = () => {
|
|
165
|
+
if (stopped || this.torndown)
|
|
166
|
+
return;
|
|
167
|
+
try {
|
|
168
|
+
note();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// heartbeat is best-effort liveness; a failure never affects the decision
|
|
172
|
+
}
|
|
173
|
+
this.schedule(tick, DEFAULT_PERMISSION_HEARTBEAT_MS);
|
|
174
|
+
};
|
|
175
|
+
this.schedule(tick, DEFAULT_PERMISSION_HEARTBEAT_MS);
|
|
176
|
+
return () => {
|
|
177
|
+
stopped = true;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
144
180
|
/** Start the hook server, then write the scratch settings (server first, so the URL the settings
|
|
145
181
|
* point at is live before claude can ever read them; settings BEFORE the spawn is the caller's
|
|
146
182
|
* ordering contract — blocker c). */
|
|
147
183
|
async start() {
|
|
148
184
|
const findPort = this.opts.findPort ?? findFreePort;
|
|
149
185
|
this.port = await findPort();
|
|
186
|
+
// Story 055 (R1.3): a per-session crypto-random secret bound into the hook URL (after the marker
|
|
187
|
+
// path). The hook-server rejects any PreToolUse POST that does not present it — the compensating
|
|
188
|
+
// control for the relaxed JSONL anti-forgery (decide() now seeds the correlator from the payload).
|
|
189
|
+
this.token = randomBytes(24).toString("hex");
|
|
150
190
|
this.server = await startHookServer({
|
|
151
191
|
port: this.port,
|
|
192
|
+
token: this.token,
|
|
152
193
|
deciderTimeoutMs: this.opts.deciderTimeoutMs,
|
|
153
194
|
onWarn: (m) => this.warn(m),
|
|
154
195
|
onToolCall: (call) => this.decide(call),
|
|
@@ -159,6 +200,7 @@ class SessionGateImpl {
|
|
|
159
200
|
this.backup = await injectHook({
|
|
160
201
|
settingsPath: this.settingsPath,
|
|
161
202
|
port: this.port,
|
|
203
|
+
token: this.token,
|
|
162
204
|
timeout: this.opts.hookTimeoutSeconds,
|
|
163
205
|
});
|
|
164
206
|
}
|
|
@@ -169,10 +211,11 @@ class SessionGateImpl {
|
|
|
169
211
|
throw err;
|
|
170
212
|
}
|
|
171
213
|
}
|
|
172
|
-
bindSession(sessionId, nudge, resolveSubagentRelay) {
|
|
214
|
+
bindSession(sessionId, nudge, resolveSubagentRelay, noteActivity) {
|
|
173
215
|
this.sessionId = sessionId;
|
|
174
216
|
this.nudge = nudge;
|
|
175
217
|
this.resolveSubagentRelay = resolveSubagentRelay;
|
|
218
|
+
this.noteActivity = noteActivity;
|
|
176
219
|
}
|
|
177
220
|
bindPty(pty) {
|
|
178
221
|
this.pty = pty;
|
|
@@ -220,31 +263,45 @@ class SessionGateImpl {
|
|
|
220
263
|
`was bound to an ACP session — denying (cannot raise session/request_permission).`);
|
|
221
264
|
return "deny";
|
|
222
265
|
}
|
|
266
|
+
// FIX(gate-deadlock): the claude flushes the `tool_use` JSONL line only AFTER the hook decision,
|
|
267
|
+
// so correlating against the JSONL deadlocks — the line never reaches the pump during the wait
|
|
268
|
+
// (proven live 2026-06-25: total=9 frozen for 5s, the id only registered post-deny). Seed the
|
|
269
|
+
// correlator from the hook payload, the AUTHORITATIVE tool_use.id source, so the gate decides on
|
|
270
|
+
// the payload (the JSONL stays best-effort enrichment). Mirrors the ACP original's permission
|
|
271
|
+
// callback, which decided directly on the SDK-supplied tool id without a transcript round-trip.
|
|
272
|
+
this.correlator.ensureRegistered(call.toolUseId);
|
|
223
273
|
await this.waitForCorrelation(call.toolUseId);
|
|
224
|
-
// === Story 054 (§9 subagent relay) — AFTER the correlation wait, BEFORE the ACP prompt.
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
// requestPermission call below is byte-identical to today
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
274
|
+
// === Story 054/055 (§9 subagent relay) — AFTER the correlation wait, BEFORE the ACP prompt. =====
|
|
275
|
+
// R2.2 — the dialog LABEL is sourced from the payload's `agent_type` (carried by parsePayload,
|
|
276
|
+
// story 055/2.1): transcript-independent and known NOW, so a subagent tool is always attributed
|
|
277
|
+
// even when its parent Task id is not (yet) resolvable. A main-chain payload has no agent_type →
|
|
278
|
+
// `subagentLabel` stays undefined and the requestPermission call below is byte-identical to today
|
|
279
|
+
// (U1: bare tool name, dialog under the inner id).
|
|
280
|
+
//
|
|
281
|
+
// R2.3 — parent-Task GROUPING is BEST-EFFORT. The hook payload carries NO parent id; the only
|
|
282
|
+
// source is the session's sidechainParentMap, read lazily via the bound resolver and populated by
|
|
283
|
+
// the (transcript-lagging) pump. WHEN it resolves a non-null parent, the dialog attaches under that
|
|
284
|
+
// parent Task id Zed already rendered. For a subagent whose row has NOT landed yet, give the pump a
|
|
285
|
+
// brief re-nudged window to register it mid-wait (the join is pump-fed, like the inner correlation);
|
|
286
|
+
// on expiry we relay the LABELLED dialog under the INNER id. An orphan (parentId === null) or an
|
|
287
|
+
// unresolved subagent is therefore PROMPTED (labelled), NEVER silently denied — this REPLACES the
|
|
288
|
+
// story-054 orphan/uncorrelated visible deny. The only deny here is requestPermission's own
|
|
289
|
+
// fail-closed (transport error / cancelled / duplicate id), propagated unchanged.
|
|
290
|
+
let subagentLabel = call.agentType;
|
|
291
|
+
let relay = this.resolveSubagentRelay?.(call.toolUseId);
|
|
292
|
+
if (!relay && call.agentType !== undefined) {
|
|
293
|
+
// A subagent tool (agent_type present) whose parent row may still be in flight: best-effort wait.
|
|
294
|
+
relay = await this.waitForSubagentParent(call.toolUseId);
|
|
295
|
+
}
|
|
234
296
|
let dialogToolCallId;
|
|
235
|
-
let subagentLabel;
|
|
236
297
|
if (relay) {
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
: "never correlated in the JSONL within the wait window"} — denying (R4 visible deny).`);
|
|
244
|
-
return "deny";
|
|
298
|
+
// Prefer the payload label; fall back to the resolver's best-effort label (a pre-055 caller / a
|
|
299
|
+
// subagent payload that somehow omitted agent_type).
|
|
300
|
+
subagentLabel = call.agentType ?? relay.subagentLabel;
|
|
301
|
+
// Group ONLY under a real, resolvable parent; an orphan (null) falls through to the inner-id relay.
|
|
302
|
+
if (relay.parentId !== null) {
|
|
303
|
+
dialogToolCallId = relay.parentId;
|
|
245
304
|
}
|
|
246
|
-
dialogToolCallId = relay.parentId;
|
|
247
|
-
subagentLabel = relay.subagentLabel;
|
|
248
305
|
}
|
|
249
306
|
// === Story 054 (R5) — SERIALIZE only the raise+inject critical section. ========================
|
|
250
307
|
// dialogToolCallId/subagentLabel were computed above in the CONCURRENT prelude, so each enqueued
|
|
@@ -253,24 +310,33 @@ class SessionGateImpl {
|
|
|
253
310
|
// requestPermission + armAllowSweep one at a time — no native-prompt keystroke crossing on the
|
|
254
311
|
// shared PTY. A single sequential main-chain tool is a no-op through the queue (U1).
|
|
255
312
|
return this.enqueuePermission(async () => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
313
|
+
// FIX(watchdog-permission): the claude is blocked on this response with the JSONL silent, so
|
|
314
|
+
// re-arm the end-of-turn watchdog for as long as the dialog is open (a slow human decision is
|
|
315
|
+
// NOT a dead turn). Always cleared in `finally`, on success or throw.
|
|
316
|
+
const stopHeartbeat = this.startPermissionHeartbeat();
|
|
317
|
+
try {
|
|
318
|
+
const decision = await requestPermission({
|
|
319
|
+
client: this.opts.client,
|
|
320
|
+
sessionId,
|
|
321
|
+
toolCall: {
|
|
322
|
+
toolUseId: call.toolUseId,
|
|
323
|
+
toolName: call.toolName,
|
|
324
|
+
toolInput: call.toolInput,
|
|
325
|
+
},
|
|
326
|
+
correlator: this.correlator,
|
|
327
|
+
onWarn: (m) => this.warn(m),
|
|
328
|
+
dialogToolCallId,
|
|
329
|
+
subagentLabel,
|
|
330
|
+
});
|
|
331
|
+
if (decision === "allow") {
|
|
332
|
+
// Return the allow body FIRST (claude is blocked on this response); sweep out of band.
|
|
333
|
+
this.armAllowSweep(call);
|
|
334
|
+
}
|
|
335
|
+
return decision;
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
stopHeartbeat();
|
|
272
339
|
}
|
|
273
|
-
return decision;
|
|
274
340
|
});
|
|
275
341
|
}
|
|
276
342
|
/** Bounded poll until the pump has registered `toolUseId` as a clean single JSONL match. On
|
|
@@ -318,6 +384,58 @@ class SessionGateImpl {
|
|
|
318
384
|
this.schedule(poll, pollMs);
|
|
319
385
|
});
|
|
320
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Story 055 (R2.3) — BEST-EFFORT parent grouping for a subagent inner tool. The `agent_id → parent
|
|
389
|
+
* tool_use.id` join lives only in the (transcript-lagging) pump-fed `sidechainParentMap`, so the
|
|
390
|
+
* parent may not be registered at decide()-time. Poll the lazy resolver, re-nudging the pump on the
|
|
391
|
+
* same {@link DEFAULT_CORRELATION_RENUDGE_MS} cadence so a sidechain row landing MID-WAIT is caught,
|
|
392
|
+
* and resolve with the relay AS SOON AS it appears. On expiry resolve `undefined` — the caller then
|
|
393
|
+
* relays the LABELLED dialog under the inner id (never a deny). Bounded by the same correlation
|
|
394
|
+
* window since the join is pump-fed exactly like the inner correlation; a torn-down gate resolves
|
|
395
|
+
* `undefined` immediately. Only ever called for a subagent payload (agent_type present), so a
|
|
396
|
+
* main-chain tool never incurs this wait.
|
|
397
|
+
*/
|
|
398
|
+
waitForSubagentParent(innerToolUseId) {
|
|
399
|
+
const resolveNow = () => this.resolveSubagentRelay?.(innerToolUseId);
|
|
400
|
+
const immediate = resolveNow();
|
|
401
|
+
if (immediate)
|
|
402
|
+
return Promise.resolve(immediate);
|
|
403
|
+
const waitMs = this.opts.correlationWaitMs ?? DEFAULT_CORRELATION_WAIT_MS;
|
|
404
|
+
const pollMs = this.opts.correlationPollMs ?? DEFAULT_CORRELATION_POLL_MS;
|
|
405
|
+
const renudgeMs = this.opts.correlationRenudgeMs ?? DEFAULT_CORRELATION_RENUDGE_MS;
|
|
406
|
+
return new Promise((resolve) => {
|
|
407
|
+
let elapsed = 0;
|
|
408
|
+
let sinceNudge = 0;
|
|
409
|
+
const poll = () => {
|
|
410
|
+
if (this.torndown) {
|
|
411
|
+
resolve(undefined);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const r = resolveNow();
|
|
415
|
+
if (r) {
|
|
416
|
+
resolve(r);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
elapsed += pollMs;
|
|
420
|
+
sinceNudge += pollMs;
|
|
421
|
+
if (sinceNudge >= renudgeMs) {
|
|
422
|
+
sinceNudge = 0;
|
|
423
|
+
try {
|
|
424
|
+
this.nudge?.();
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// best-effort: a nudge failure only widens the grouping window; never rejects or decides
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (elapsed >= waitMs) {
|
|
431
|
+
resolve(undefined); // best-effort expiry — relay under the inner id (labelled), not a deny
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this.schedule(poll, pollMs);
|
|
435
|
+
};
|
|
436
|
+
this.schedule(poll, pollMs);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
321
439
|
/**
|
|
322
440
|
* #52822 sweep (allow path, best-effort by design): wait a bounded window for the native TUI
|
|
323
441
|
* prompt to APPEAR in the post-decision PTY output; if it never renders, allow-suppression held
|
|
@@ -10,6 +10,9 @@ export interface PreToolUsePayload {
|
|
|
10
10
|
tool_name?: string;
|
|
11
11
|
tool_input?: unknown;
|
|
12
12
|
tool_use_id?: string;
|
|
13
|
+
/** Story 055 (R2.1) — present ONLY on a subagent-internal tool: the subagent's own id and type. */
|
|
14
|
+
agent_id?: string;
|
|
15
|
+
agent_type?: string;
|
|
13
16
|
}
|
|
14
17
|
/** The normalized tool call forwarded to the decider (camelCase, the shape request-permission uses). */
|
|
15
18
|
export interface ForwardedToolCall {
|
|
@@ -18,6 +21,11 @@ export interface ForwardedToolCall {
|
|
|
18
21
|
toolUseId: string;
|
|
19
22
|
sessionId?: string;
|
|
20
23
|
permissionMode?: string;
|
|
24
|
+
/** Story 055 (R2.1) — the subagent's own id (`agent_id`); undefined for a main-chain tool. */
|
|
25
|
+
agentId?: string;
|
|
26
|
+
/** Story 055 (R2.1/R2.2) — the subagent's type (`agent_type`); the dialog label source. Undefined
|
|
27
|
+
* for a main-chain tool. There is NO parent tool_use.id in the payload (grouping is best-effort). */
|
|
28
|
+
agentType?: string;
|
|
21
29
|
}
|
|
22
30
|
/** The decider the server forwards each tool call to; returns the enforced `'allow'`/`'deny'`. */
|
|
23
31
|
export type ToolCallDecider = (call: ForwardedToolCall) => Promise<"allow" | "deny"> | "allow" | "deny";
|
|
@@ -46,6 +54,13 @@ export interface StartHookServerOptions {
|
|
|
46
54
|
createHttpServer?: () => Server;
|
|
47
55
|
/** Optional diagnostics sink for fail-closed events (defaults to no-op). */
|
|
48
56
|
onWarn?: (message: string) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Story 055 (R1.3) — per-session secret token. WHEN set, a POST whose request path is not exactly
|
|
59
|
+
* `${FORK_HOOK_MARKER_PATH}/<token>` FAILS CLOSED (deny) WITHOUT invoking the decider — the
|
|
60
|
+
* compensating control for the relaxed JSONL anti-forgery. WHEN undefined, no token is enforced
|
|
61
|
+
* (the pre-055 behavior, used by the unit tests that construct the server directly).
|
|
62
|
+
*/
|
|
63
|
+
token?: string;
|
|
49
64
|
}
|
|
50
65
|
/** Default decider timeout (ms) — long enough for an async Zed decision relay, short of a hang. */
|
|
51
66
|
export declare const DEFAULT_DECIDER_TIMEOUT_MS = 600000;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hook-server.d.ts","sourceRoot":"","sources":["../../src/permissions/hook-server.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"hook-server.d.ts","sourceRoot":"","sources":["../../src/permissions/hook-server.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAMjG;sGACsG;AACtG,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wGAAwG;AACxG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;0GACsG;IACtG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,kGAAkG;AAClG,MAAM,MAAM,eAAe,GAAG,CAC5B,IAAI,EAAE,iBAAiB,KACpB,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,OAAO,GAAG,MAAM,CAAC;AAElD,sCAAsC;AACtC,MAAM,WAAW,UAAU;IACzB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAC3C,qCAAqC;IACrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB;IACrC,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,MAAM,MAAM,CAAC;IAChC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,mGAAmG;AACnG,eAAO,MAAM,0BAA0B,SAAU,CAAC;AAsBlD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CA6BlE;AAiCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CA8GjF"}
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
// user's real config — that wiring is the engine-integration concern; this module is the standalone
|
|
19
19
|
// server seam.
|
|
20
20
|
import { createServer } from "node:http";
|
|
21
|
+
import { appendFileSync } from "node:fs";
|
|
21
22
|
import { LOOPBACK_HOST } from "../gate/port.js";
|
|
23
|
+
import { FORK_HOOK_MARKER_PATH } from "../gate/settings-writer.js";
|
|
22
24
|
import { allowDecision, denyDecision } from "./deny.js";
|
|
23
25
|
/** Default decider timeout (ms) — long enough for an async Zed decision relay, short of a hang. */
|
|
24
26
|
export const DEFAULT_DECIDER_TIMEOUT_MS = 600_000;
|
|
@@ -70,6 +72,10 @@ export function parsePayload(raw) {
|
|
|
70
72
|
toolUseId,
|
|
71
73
|
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : undefined,
|
|
72
74
|
permissionMode: typeof parsed.permission_mode === "string" ? parsed.permission_mode : undefined,
|
|
75
|
+
// R2.1 — carry the subagent attribution the §9 payload already ships (today they are dropped). A
|
|
76
|
+
// main-chain payload omits them → both stay undefined.
|
|
77
|
+
agentId: typeof parsed.agent_id === "string" ? parsed.agent_id : undefined,
|
|
78
|
+
agentType: typeof parsed.agent_type === "string" ? parsed.agent_type : undefined,
|
|
73
79
|
};
|
|
74
80
|
}
|
|
75
81
|
/** Run the decider with a timeout; on timeout or throw, resolve `'deny'` (fail closed). */
|
|
@@ -110,7 +116,7 @@ async function decideWithTimeout(decider, call, timeoutMs, onWarn) {
|
|
|
110
116
|
* fails fast rather than spawning claude with an ungated hook URL).
|
|
111
117
|
*/
|
|
112
118
|
export function startHookServer(opts) {
|
|
113
|
-
const { port, deciderTimeoutMs = DEFAULT_DECIDER_TIMEOUT_MS, createHttpServer = createServer, onWarn, } = opts;
|
|
119
|
+
const { port, deciderTimeoutMs = DEFAULT_DECIDER_TIMEOUT_MS, createHttpServer = createServer, onWarn, token, } = opts;
|
|
114
120
|
let decider = opts.onToolCall;
|
|
115
121
|
const server = createHttpServer();
|
|
116
122
|
server.on("request", (req, res) => {
|
|
@@ -119,6 +125,29 @@ export function startHookServer(opts) {
|
|
|
119
125
|
let call = null;
|
|
120
126
|
try {
|
|
121
127
|
const raw = await readBody(req);
|
|
128
|
+
// DIAGNOSTIC (env-gated, off by default): capture EVERY raw PreToolUse POST the fork's
|
|
129
|
+
// hook-server receives — including the subagent-attributed fields (agent_id/agent_type) that
|
|
130
|
+
// parsePayload drops — so a live run can verify whether subagent-internal tools POST at all.
|
|
131
|
+
if (process.env.FORK_HOOK_LOG_RAW) {
|
|
132
|
+
try {
|
|
133
|
+
appendFileSync(process.env.FORK_HOOK_LOG_RAW, raw + "\n");
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// best-effort diagnostic — never let logging fail the request
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// R1.3 (story 055): validate the per-session token from the request path BEFORE the decider.
|
|
140
|
+
// A POST that does not present `${FORK_HOOK_MARKER_PATH}/<token>` is denied without ever
|
|
141
|
+
// reaching the decider — the forge-resistance that compensates the payload-seeded correlation.
|
|
142
|
+
if (token !== undefined) {
|
|
143
|
+
const urlPath = (req.url ?? "").split("?")[0];
|
|
144
|
+
if (urlPath !== `${FORK_HOOK_MARKER_PATH}/${token}`) {
|
|
145
|
+
onWarn?.(`[gate §9] FAIL CLOSED: PreToolUse POST presented a missing/incorrect per-session token ` +
|
|
146
|
+
`(url path "${urlPath}") — denying WITHOUT invoking the decider.`);
|
|
147
|
+
writeDecision(res, denyDecision({ toolName: "(unauthorized)", toolUseId: "(unauthorized)" }));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
122
151
|
call = parsePayload(raw);
|
|
123
152
|
if (call === null) {
|
|
124
153
|
onWarn?.(`[gate §9] FAIL CLOSED: unparseable or identity-less PreToolUse payload — denying.`);
|
|
@@ -70,6 +70,15 @@ export declare class ToolUseCorrelator {
|
|
|
70
70
|
* observation marks it duplicate. Returns true on the FIRST registration, false on a duplicate.
|
|
71
71
|
*/
|
|
72
72
|
register(toolUseId: string): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* FIX(gate-deadlock): seed the correlator from the AUTHORITATIVE PreToolUse hook payload when the
|
|
75
|
+
* JSONL has not observed the id yet. The claude buffers the `tool_use` line and only flushes it to
|
|
76
|
+
* the transcript AFTER the hook decision, so waiting on the JSONL deadlocks (the line never arrives
|
|
77
|
+
* during the wait). Idempotent and safe: marks count=1 ONLY when unseen (0) — it never pushes an
|
|
78
|
+
* already-clean (1) id to duplicate (2), nor touches a genuine duplicate (>1). The pump's later
|
|
79
|
+
* observation of the same id (post-decision) is harmless: the id is already consumed by then.
|
|
80
|
+
*/
|
|
81
|
+
ensureRegistered(toolUseId: string): void;
|
|
73
82
|
/** True iff `toolUseId` was observed EXACTLY once in the JSONL and has not yet been consumed. */
|
|
74
83
|
isCleanMatch(toolUseId: string): boolean;
|
|
75
84
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-permission.d.ts","sourceRoot":"","sources":["../../src/permissions/request-permission.ts"],"names":[],"mappings":"AAoBA,yEAAyE;AACzE,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,cAAc,GAAG,aAAa,GAAG,eAAe,CAAC;AAEnG,oGAAoG;AACpG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,oBAAoB,CAAC;CAC5B;AAED,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,yDAAyD;AACzD,MAAM,MAAM,wBAAwB,GAChC;IAAE,OAAO,EAAE,WAAW,CAAA;CAAE,GACxB;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,8DAA8D;AAC9D,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,wBAAwB,CAAC;CACnC;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iBAAiB,CAAC,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACtF;AAED,iGAAiG;AACjG,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wFAAwF;AACxF,eAAO,MAAM,eAAe,UAAU,CAAC;AACvC,eAAO,MAAM,cAAc,SAAS,CAAC;AAErC,gFAAgF;AAChF,wBAAgB,sBAAsB,IAAI,gBAAgB,EAAE,CAK3D;AAED;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAC5B,8FAA8F;IAC9F,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6B;IAClD,qGAAqG;IACrG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAE9C;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAMpC,iGAAiG;IACjG,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIxC;;;;;OAKG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM;CAS9C;AAED,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,gBAAgB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,sFAAsF;IACtF,UAAU,EAAE,iBAAiB,CAAC;IAC9B,sGAAsG;IACtG,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC;oGACgG;IAChG,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"request-permission.d.ts","sourceRoot":"","sources":["../../src/permissions/request-permission.ts"],"names":[],"mappings":"AAoBA,yEAAyE;AACzE,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,cAAc,GAAG,aAAa,GAAG,eAAe,CAAC;AAEnG,oGAAoG;AACpG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,oBAAoB,CAAC;CAC5B;AAED,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,yDAAyD;AACzD,MAAM,MAAM,wBAAwB,GAChC;IAAE,OAAO,EAAE,WAAW,CAAA;CAAE,GACxB;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,8DAA8D;AAC9D,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,wBAAwB,CAAC;CACnC;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iBAAiB,CAAC,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACtF;AAED,iGAAiG;AACjG,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wFAAwF;AACxF,eAAO,MAAM,eAAe,UAAU,CAAC;AACvC,eAAO,MAAM,cAAc,SAAS,CAAC;AAErC,gFAAgF;AAChF,wBAAgB,sBAAsB,IAAI,gBAAgB,EAAE,CAK3D;AAED;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAC5B,8FAA8F;IAC9F,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6B;IAClD,qGAAqG;IACrG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAE9C;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAMpC;;;;;;;OAOG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIzC,iGAAiG;IACjG,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIxC;;;;;OAKG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM;CAS9C;AAED,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,gBAAgB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,sFAAsF;IACtF,UAAU,EAAE,iBAAiB,CAAC;IAC9B,sGAAsG;IACtG,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC;oGACgG;IAChG,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,CA0DjG"}
|
|
@@ -52,6 +52,18 @@ export class ToolUseCorrelator {
|
|
|
52
52
|
this.seen.set(toolUseId, count + 1);
|
|
53
53
|
return count === 0;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* FIX(gate-deadlock): seed the correlator from the AUTHORITATIVE PreToolUse hook payload when the
|
|
57
|
+
* JSONL has not observed the id yet. The claude buffers the `tool_use` line and only flushes it to
|
|
58
|
+
* the transcript AFTER the hook decision, so waiting on the JSONL deadlocks (the line never arrives
|
|
59
|
+
* during the wait). Idempotent and safe: marks count=1 ONLY when unseen (0) — it never pushes an
|
|
60
|
+
* already-clean (1) id to duplicate (2), nor touches a genuine duplicate (>1). The pump's later
|
|
61
|
+
* observation of the same id (post-decision) is harmless: the id is already consumed by then.
|
|
62
|
+
*/
|
|
63
|
+
ensureRegistered(toolUseId) {
|
|
64
|
+
if ((this.seen.get(toolUseId) ?? 0) === 0)
|
|
65
|
+
this.seen.set(toolUseId, 1);
|
|
66
|
+
}
|
|
55
67
|
/** True iff `toolUseId` was observed EXACTLY once in the JSONL and has not yet been consumed. */
|
|
56
68
|
isCleanMatch(toolUseId) {
|
|
57
69
|
return this.seen.get(toolUseId) === 1 && !this.consumed.has(toolUseId);
|
|
@@ -96,12 +108,15 @@ export async function requestPermission(opts) {
|
|
|
96
108
|
result = await client.requestPermission({
|
|
97
109
|
sessionId,
|
|
98
110
|
toolCall: {
|
|
99
|
-
// Story 054: a subagent inner tool relays under the PARENT Task id Zed already rendered
|
|
100
|
-
// (dialogToolCallId)
|
|
101
|
-
//
|
|
111
|
+
// Story 054/055: a subagent inner tool relays under the PARENT Task id Zed already rendered
|
|
112
|
+
// (dialogToolCallId) WHEN the parent is resolvable; otherwise it keeps the inner id. Story 055
|
|
113
|
+
// (R2.2) DECOUPLES the attributed title from dialogToolCallId: the title is labelled whenever
|
|
114
|
+
// `subagentLabel` is set (sourced from the payload's agent_type), so a fast subagent whose
|
|
115
|
+
// parent has not yet been resolved still renders `<tool> · from the <agent_type> agent` under
|
|
116
|
+
// the inner id. A main-chain call (no subagentLabel) stays byte-identical: bare tool name.
|
|
102
117
|
toolCallId: dialogToolCallId ?? toolCall.toolUseId,
|
|
103
|
-
title:
|
|
104
|
-
? `${toolCall.toolName} · from the ${subagentLabel
|
|
118
|
+
title: subagentLabel !== undefined
|
|
119
|
+
? `${toolCall.toolName} · from the ${subagentLabel} agent`
|
|
105
120
|
: toolCall.toolName,
|
|
106
121
|
rawInput: toolCall.toolInput,
|
|
107
122
|
},
|
package/dist/settings.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,QAAQ,EACd,MAAM,gCAAgC,CAAC;AA0BxC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;QAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;CAC7E;AAED;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAAa;IAC9B,OAAO,CAAC,MAAM,CAAqE;IACnF,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAA8C;IACnE,OAAO,CAAC,WAAW,CAA8B;gBAErC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB;IAMzD;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBjC;;;OAGG;IACH,OAAO,CAAC,eAAe;IASvB;;;OAGG;YACW,eAAe;IAU7B;;OAEG;IACH,OAAO,CAAC,aAAa;
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,QAAQ,EACd,MAAM,gCAAgC,CAAC;AA0BxC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;QAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;CAC7E;AAED;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAAa;IAC9B,OAAO,CAAC,MAAM,CAAqE;IACnF,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAA8C;IACnE,OAAO,CAAC,WAAW,CAA8B;gBAErC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB;IAMzD;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBjC;;;OAGG;IACH,OAAO,CAAC,eAAe;IASvB;;;OAGG;YACW,eAAe;IAU7B;;OAEG;IACH,OAAO,CAAC,aAAa;IAgCrB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAuB5B;;OAEG;IACH,WAAW,IAAI,QAAQ;IAIvB;;OAEG;IACH,MAAM,IAAI,MAAM;IAIhB;;OAEG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUxC;;OAEG;IACH,OAAO,IAAI,IAAI;CAehB"}
|
package/dist/settings.js
CHANGED
|
@@ -106,6 +106,13 @@ export class SettingsManager {
|
|
|
106
106
|
this.handleSettingsChange();
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
|
+
// Story 059: a best-effort settings watcher must NOT keep the Node event loop alive on its
|
|
110
|
+
// own. A settingsManager that escapes dispose() (an initialize/re-spawn race — see the
|
|
111
|
+
// pre-existing-leak note in acp-agent.ts createSession) otherwise leaves a live fs.watch
|
|
112
|
+
// handle that hangs `node:test` on process exit (the live ACP process is held open by its
|
|
113
|
+
// stdio, never by this watcher). unref() detaches it from the loop's keep-alive set while
|
|
114
|
+
// leaving the subscription fully functional.
|
|
115
|
+
watcher.unref?.();
|
|
109
116
|
watcher.on("error", (error) => {
|
|
110
117
|
this.logger.error(`Settings watcher error for ${filePath}:`, error);
|
|
111
118
|
});
|
|
@@ -139,6 +146,8 @@ export class SettingsManager {
|
|
|
139
146
|
this.logger.error("Failed to reload settings:", error);
|
|
140
147
|
}
|
|
141
148
|
}, 100);
|
|
149
|
+
// Story 059: same rationale as the watcher unref — a pending debounce timer must not block exit.
|
|
150
|
+
this.debounceTimer?.unref?.();
|
|
142
151
|
}
|
|
143
152
|
/**
|
|
144
153
|
* Returns the current merged settings
|
package/dist/tools.d.ts
CHANGED
|
@@ -50,8 +50,16 @@ export declare function toDisplayPath(filePath: string, cwd?: string): string;
|
|
|
50
50
|
export declare function toolCallIdFor(toolUseId: string, sessionId: string, options?: {
|
|
51
51
|
namespaced?: boolean;
|
|
52
52
|
}): string;
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
/** Minimal shape of a raw JSONL `tool_use` block consumed by the tool mappers. */
|
|
54
|
+
interface RawToolUse {
|
|
55
|
+
name: string;
|
|
56
|
+
id: string;
|
|
57
|
+
input?: unknown;
|
|
58
|
+
}
|
|
59
|
+
export declare function toolInfoFromToolUse(toolUse: RawToolUse, supportsTerminalOutput?: boolean, cwd?: string): ToolInfo;
|
|
60
|
+
export declare function toolUpdateFromToolResult(toolResult: ToolResultBlockParam | BetaToolResultBlockParam | BetaWebSearchToolResultBlockParam | BetaWebFetchToolResultBlockParam | WebSearchToolResultBlockParam | BetaCodeExecutionToolResultBlockParam | BetaBashCodeExecutionToolResultBlockParam | BetaTextEditorCodeExecutionToolResultBlockParam | BetaRequestMCPToolResultBlockParam | BetaToolSearchToolResultBlockParam, toolUse: {
|
|
61
|
+
name?: string;
|
|
62
|
+
} | undefined, supportsTerminalOutput?: boolean): ToolUpdate;
|
|
55
63
|
export type ClaudePlanEntry = {
|
|
56
64
|
content: string;
|
|
57
65
|
status: "pending" | "in_progress" | "completed";
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,SAAS,EACT,eAAe,EACf,gBAAgB,EAChB,QAAQ,EACT,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAQL,eAAe,EACf,gBAAgB,EAChB,eAAe,EAIhB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAGL,oBAAoB,EAEpB,6BAA6B,EAE9B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAEL,yCAAyC,EAGzC,qCAAqC,EAGrC,kCAAkC,EAGlC,+CAA+C,EAI/C,wBAAwB,EACxB,kCAAkC,EAIlC,gCAAgC,EAEhC,iCAAiC,EAClC,MAAM,sCAAsC,CAAC;AAE9C,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AA0BxC,UAAU,QAAQ;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAChC;AAED,UAAU,UAAU;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC/B,KAAK,CAAC,EAAE;QACN,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,MAAM,CAAC;SACrB,CAAC;QACF,eAAe,CAAC,EAAE;YAChB,WAAW,EAAE,MAAM,CAAC;YACpB,IAAI,EAAE,MAAM,CAAC;SACd,CAAC;QACF,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,MAAM,CAAC;YACpB,SAAS,EAAE,MAAM,CAAC;YAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;SACvB,CAAC;KACH,CAAC;CACH;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAQpE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,MAAM,CAER;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.js
CHANGED
|
@@ -325,7 +325,11 @@ export function toolUpdateFromToolResult(toolResult, toolUse, supportsTerminalOu
|
|
|
325
325
|
if ("is_error" in toolResult &&
|
|
326
326
|
toolResult.is_error &&
|
|
327
327
|
toolResult.content &&
|
|
328
|
-
toolResult.content.length > 0
|
|
328
|
+
toolResult.content.length > 0 &&
|
|
329
|
+
// Story 056 (#776): a FAILED Bash with a negotiated terminal must still render as terminal
|
|
330
|
+
// output (terminal_output + terminal_exit exit_code 1 via `case "Bash"`), not markdown — so it
|
|
331
|
+
// is excluded from this error-only early-return. Every other errored result is unchanged.
|
|
332
|
+
!(toolUse?.name === "Bash" && supportsTerminalOutput)) {
|
|
329
333
|
// Only return errors
|
|
330
334
|
return toAcpContentUpdate(toolResult.content, true);
|
|
331
335
|
}
|
package/dist/usage.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ export interface UsageOptions {
|
|
|
32
32
|
* • R3.1 — the optional `cost` field is INTENTIONALLY OMITTED and must NEVER be fabricated:
|
|
33
33
|
* the JSONL usage block carries only token counts, and `cost` is optional in the Zed v1 struct,
|
|
34
34
|
* so omitting it is contract-correct (an invented cost would violate §1 "never fabricate").
|
|
35
|
+
* Story 059 EVIDENCE (2026-06-28): an audit of 5169 local transcripts found 0 carrying
|
|
36
|
+
* `total_cost_usd` as a JSON key — it lives only in the SDK `result` envelope (`claude -p`), never
|
|
37
|
+
* in the interactive PTY+tail JSONL mapped here (the 68 substring hits were all conversation text).
|
|
35
38
|
*/
|
|
36
39
|
export declare function toUsageUpdate(message: UsageCarrier, options?: UsageOptions): UsageUpdateNotification | undefined;
|
|
37
40
|
export interface UsageFlagOptions extends UsageOptions {
|
package/dist/usage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,iFAAiF;AACjF,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,aAAa,EAAE;IAAE,aAAa,EAAE,cAAc,CAAA;CAAE,CAAC,CAAC;AAEhG,yFAAyF;AACzF,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CAChF;AAED,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE,YAAiB,GACzB,uBAAuB,GAAG,SAAS,CAUrC;AAED,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,wFAAwF;IACxF,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE,gBAAqB,GAC7B,uBAAuB,EAAE,CAI3B"}
|
package/dist/usage.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Story 025 / Group 3 (R3.1, R3.2) — the optional
|
|
2
|
-
//
|
|
1
|
+
// Story 025 / Group 3 (R3.1, R3.2) — the optional UNSTABLE `usage_update` mapping.
|
|
2
|
+
// `usage_update` exists ONLY in the @agentclientprotocol/sdk 0.22.1 schema and is
|
|
3
3
|
// best-effort per §1; it maps the JSONL message-event `usage.{input,output}_tokens` into the
|
|
4
|
-
// SDK's { sessionUpdate:"usage_update", size, used } shape.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// SDK's { sessionUpdate:"usage_update", size, used } shape. Story 042 FLIPPED the default to ON
|
|
5
|
+
// (story 039 confirmed the user's Zed ACCEPTS+RENDERS it by code); only the explicit opt-out
|
|
6
|
+
// USAGE_UPDATE=0/false disables it (see usage-env.ts). A rejected emission is still suppressed by
|
|
7
|
+
// the pump's per-session reject latch (Task 5.1, R8).
|
|
7
8
|
//
|
|
8
9
|
// Kept a self-contained module (cf. diff-source.ts) imported by the acp-agent pump; it does
|
|
9
10
|
// NOT go through lib.ts (the frozen upstream export surface).
|
|
@@ -21,6 +22,9 @@
|
|
|
21
22
|
* • R3.1 — the optional `cost` field is INTENTIONALLY OMITTED and must NEVER be fabricated:
|
|
22
23
|
* the JSONL usage block carries only token counts, and `cost` is optional in the Zed v1 struct,
|
|
23
24
|
* so omitting it is contract-correct (an invented cost would violate §1 "never fabricate").
|
|
25
|
+
* Story 059 EVIDENCE (2026-06-28): an audit of 5169 local transcripts found 0 carrying
|
|
26
|
+
* `total_cost_usd` as a JSON key — it lives only in the SDK `result` envelope (`claude -p`), never
|
|
27
|
+
* in the interactive PTY+tail JSONL mapped here (the 68 substring hits were all conversation text).
|
|
24
28
|
*/
|
|
25
29
|
export function toUsageUpdate(message, options = {}) {
|
|
26
30
|
const input = message.usage?.input_tokens;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.6.0",
|
|
7
7
|
"description": "Run the official Claude Code TUI in Zed's agent panel on your Claude Pro/Max subscription — an ACP-over-PTY bridge created for Anthropic's billing split (paused June 15; see README).",
|
|
8
8
|
"main": "dist/lib.js",
|
|
9
9
|
"types": "dist/lib.d.ts",
|
|
@@ -60,20 +60,20 @@
|
|
|
60
60
|
"author": "lucascouts",
|
|
61
61
|
"license": "Apache-2.0",
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@agentclientprotocol/sdk": "0.
|
|
64
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
63
|
+
"@agentclientprotocol/sdk": "1.0.0",
|
|
64
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.191",
|
|
65
65
|
"node-pty": "1.1.0",
|
|
66
66
|
"zod": "^3.25.0 || ^4.0.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@anthropic-ai/sdk": "0.
|
|
69
|
+
"@anthropic-ai/sdk": "0.106.0",
|
|
70
70
|
"@eslint/js": "10.0.1",
|
|
71
|
-
"@types/node": "26.0.
|
|
72
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
73
|
-
"@typescript-eslint/parser": "8.
|
|
71
|
+
"@types/node": "26.0.1",
|
|
72
|
+
"@typescript-eslint/eslint-plugin": "8.62.0",
|
|
73
|
+
"@typescript-eslint/parser": "8.62.0",
|
|
74
74
|
"eslint": "10.5.0",
|
|
75
75
|
"eslint-config-prettier": "10.1.8",
|
|
76
|
-
"globals": "17.
|
|
76
|
+
"globals": "17.7.0",
|
|
77
77
|
"prettier": "3.8.4",
|
|
78
78
|
"ts-node": "10.9.2",
|
|
79
79
|
"typescript": "6.0.3"
|