@te-river/opencode-team-mode 1.5.1 → 1.5.2

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.
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Unified approval gate — Layer 2 (timeout auto-reject) + Layer 3 (SDK
3
+ * fallback) of the R6 + R2 approval flow.
4
+ *
5
+ * The host only shows its official confirmation dialog for command patterns
6
+ * declared `ask` in an agent's permission config (Layer 1, envprotect.ts).
7
+ * When a dialog opens it SUSPENDS the tool call and waits for a human; on the
8
+ * OpenCode DESKTOP that human is present, but a request can also be left
9
+ * unanswered. This module owns ONE timer that watches the host permission
10
+ * events and closes abandoned requests on the safe side.
11
+ *
12
+ * REAL host contract (1.18.29, probed live via SSE + the plugin event hook —
13
+ * see gate-test report-p5):
14
+ * permission.asked → properties = { id, sessionID, permission: "bash",
15
+ * patterns: [<concrete command segment>, …], metadata:
16
+ * { command }, always: [<host's "always" generalization
17
+ * proposal>], tool: { messageID, callID } } — the props
18
+ * carry NO `type` field, and the tool name sits in
19
+ * `permission`. A props matching one of OUR injected
20
+ * bash ask sets means the dialog is ours: audit "ask",
21
+ * arm the timeout, and — for the R6 env face — mark
22
+ * the session deferrable (canDefer, see below).
23
+ * permission.replied → properties = { sessionID, requestID, reply } on the
24
+ * wire, but the plugin-side observer has also seen it
25
+ * with ONLY { sessionID }. The gate therefore cancels
26
+ * EVERY pending timer of that session on a reply: an
27
+ * already-approved command left on a live timer would
28
+ * get a second reject after the timeout, the SDK call
29
+ * would hit a dead id (4xx) and flip the gate
30
+ * permanently degraded.
31
+ *
32
+ * Older/typed spellings (`permission.updated`, `pattern`, `permissionID`,
33
+ * `response`) keep being accepted — the shipped SDK d.ts still names the
34
+ * events that way, and some builds omit the reply id entirely (a short
35
+ * ghost-suppression window per session then absorbs late/duplicate `asked`
36
+ * replays instead of re-arming a ghost timer).
37
+ *
38
+ * Hard rules (HUMAN-approved):
39
+ * - the plugin NEVER self-allows: `reply` is fixed to "reject". The only
40
+ * way a gated command runs is a human approving the dialog;
41
+ * - on `TM_ENV_PROTECT=off` the gate is not armed (index.ts decides);
42
+ * - deferral is SESSION-SCOPED (canDefer): only sessions registered as
43
+ * carrying our injected ask set may have env reads passed through —
44
+ * registration comes from an exec-role user prompt (the live-proven
45
+ * `message.updated` UserMessage{role,agent,sessionID} signal, plus the
46
+ * documented `chat.message` hook) or from an R6-env-classified asked
47
+ * event. Stock build/plan sessions never register, so the global R6
48
+ * hook keeps hard-throwing there — closing the R6 bypass the global
49
+ * `isArmed()` deferral allowed;
50
+ * - if the SDK reply FAILS (network/host — including the v1
51
+ * `throwOnError:false` envelope resolving with `{ error }`), we cannot
52
+ * close the dialog, so the gate flips itself permanently "degraded" —
53
+ * the R6 hook stops deferring env reads and hard-throws them again
54
+ * (fail-closed), and the event is audited "degraded".
55
+ *
56
+ * SDK surface (probed live on 1.18.x): the plugin's `input.client` is the v1
57
+ * `OpencodeClient`, which has NO `permission` namespace and NO list/create
58
+ * endpoint; the reply path is the top-level
59
+ * `postSessionIdPermissionsPermissionId({ path: { id: sessionID,
60
+ * permissionID }, body: { response } })`. `client.permission.reply` /
61
+ * `client.permission.list` are still tried first so a richer host build is
62
+ * supported, but on v1 they are simply absent and the poll fallback is a
63
+ * no-op (events are the sole pending signal there).
64
+ */
65
+ import type { PermissionEvent } from "./types.js";
66
+ /** Default timeout, in minutes, before an unanswered dialog is auto-rejected. */
67
+ export declare const DEFAULT_ASK_TIMEOUT_MIN = 10;
68
+ /**
69
+ * Hard floor (minutes) for the auto-reject timeout. Measured live on the
70
+ * OpenCode Desktop: a `permission.replied` (human answered "once") reaches
71
+ * the plugin through the host event bus ~120s LATE — a 1-minute timer
72
+ * therefore fires BEFORE the cancel arrives and auto-rejects the just-
73
+ * approved request on a dead id (4xx → permanent degraded, the D4
74
+ * double-reject race). 3 min = observed ~120s lag + margin; in-range
75
+ * values below the floor clamp UP to it (a longer timeout is always the
76
+ * safe direction — the dialog just stays open a bit longer).
77
+ */
78
+ export declare const MIN_ASK_TIMEOUT_MIN = 3;
79
+ /** The verdict vocabulary recorded in the audit trail (privacy: no command
80
+ * text, path, variable name or value is ever logged). */
81
+ export type ApprovalVerdict = "ask" | "allowed-once" | "allowed-always" | "rejected" | "timeout-rejected" | "degraded";
82
+ /**
83
+ * Resolve `TM_ASK_TIMEOUT_MIN`. Unset / blank / non-numeric / <1 / >1440
84
+ * (24h) fall back to the 10-minute default — a mistyped value can never
85
+ * disable the timeout or make it absurdly long. Valid values below the
86
+ * 3-minute bus-lag floor clamp UP to it (1→3, 2→3; see
87
+ * `MIN_ASK_TIMEOUT_MIN` for the measured ~120s replied-event lag).
88
+ */
89
+ export declare function resolveAskTimeoutMs(env?: Record<string, string | undefined>): number;
90
+ type TimerHandle = unknown;
91
+ export interface GateTimers {
92
+ setTimeoutFn: (cb: () => void, ms: number) => TimerHandle;
93
+ clearTimeoutFn: (handle: TimerHandle) => void;
94
+ }
95
+ export interface ApprovalGateDeps {
96
+ client: unknown;
97
+ timeoutMs: number;
98
+ /** Injectable timers (default: real global timers) for deterministic tests. */
99
+ timers?: GateTimers;
100
+ /** Injectable clock (default: Date.now) for deterministic window tests. */
101
+ now?: () => number;
102
+ }
103
+ export interface ApprovalGate {
104
+ /** Handle one host event (permission.asked/updated / permission.replied). */
105
+ handleEvent(event: PermissionEvent): void;
106
+ /** Start the poll fallback (no-op on v1 where no list endpoint exists). */
107
+ start(): void;
108
+ /** Stop the poll + every pending timer. */
109
+ dispose(): void;
110
+ /** Global gate health: armed, not degraded, and the client can reply. */
111
+ isArmed(): boolean;
112
+ /**
113
+ * The R6 hook's deferral query: TRUE only when this exact session is
114
+ * registered as carrying our injected ask set AND the gate is healthy.
115
+ * Replaces the old global `isArmed()` deferral (which let stock build/plan
116
+ * sessions pass env reads silently with no dialog in front of them).
117
+ */
118
+ canDefer(sessionID?: string): boolean;
119
+ /** True once a session registered (exec-role prompt seen on
120
+ * message.updated / chat.message by index.ts, or an R6-env-classified
121
+ * asked event). Introspection for tests/diagnostics. */
122
+ hasLiveAsk(sessionID?: string): boolean;
123
+ /** Mark a session as carrying the injected ask set (index.ts calls this
124
+ * when a user prompt routes to an agent we injected the escalated bash
125
+ * ask object into — proven live: `message.updated` UserMessage carries
126
+ * { sessionID, role:"user", agent } and fires BEFORE the session's first
127
+ * tool call, while permission.asked fires AFTER tool.execute.before). */
128
+ registerExecSession(sessionID?: string): void;
129
+ /** True after the user picked "always" on an env-related ask in this session
130
+ * — subsequent env reads pass silently (the approval event itself is audited). */
131
+ isEnvApproved(sessionID?: string): boolean;
132
+ /** Number of pending requests currently being timed (tests / introspection). */
133
+ pendingSize(): number;
134
+ }
135
+ /** True when the client exposes a permission-reply path at all. index.ts
136
+ * arms the gate ONLY for such a client — fail-closed otherwise (no armed
137
+ * auto-reject ⇒ the R6 hook keeps hard-throwing every env read). */
138
+ export declare function hasPermissionReplyCapability(client: unknown): boolean;
139
+ /**
140
+ * Create the approval gate. It is intentionally pure about timing +
141
+ * replying + session registration; the caller (index.ts) decides whether to
142
+ * arm it (mode != off AND the client is reply-capable), feeds events in, and
143
+ * wires `canDefer(sessionID)` into the R6 hook.
144
+ */
145
+ export declare function createApprovalGate(deps: ApprovalGateDeps): ApprovalGate;
146
+ export {};
147
+ //# sourceMappingURL=approval-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-gate.d.ts","sourceRoot":"","sources":["../src/approval-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAGjD,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAiBpC;0DAC0D;AAC1D,MAAM,MAAM,eAAe,GACvB,KAAK,GACL,cAAc,GACd,gBAAgB,GAChB,UAAU,GACV,kBAAkB,GAClB,UAAU,CAAA;AAEd;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAAG,MAAM,CAQjG;AAED,KAAK,WAAW,GAAG,OAAO,CAAA;AAE1B,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,WAAW,CAAA;IACzD,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAA;CAC9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,WAAW,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAA;IACzC,2EAA2E;IAC3E,KAAK,IAAI,IAAI,CAAA;IACb,2CAA2C;IAC3C,OAAO,IAAI,IAAI,CAAA;IACf,yEAAyE;IACzE,OAAO,IAAI,OAAO,CAAA;IAClB;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACrC;;8DAE0D;IAC1D,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvC;;;;8EAI0E;IAC1E,mBAAmB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7C;uFACmF;IACnF,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC1C,gFAAgF;IAChF,WAAW,IAAI,MAAM,CAAA;CACtB;AA6GD;;qEAEqE;AACrE,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAErE;AAgBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,YAAY,CA6OvE"}
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Unified approval gate — Layer 2 (timeout auto-reject) + Layer 3 (SDK
3
+ * fallback) of the R6 + R2 approval flow.
4
+ *
5
+ * The host only shows its official confirmation dialog for command patterns
6
+ * declared `ask` in an agent's permission config (Layer 1, envprotect.ts).
7
+ * When a dialog opens it SUSPENDS the tool call and waits for a human; on the
8
+ * OpenCode DESKTOP that human is present, but a request can also be left
9
+ * unanswered. This module owns ONE timer that watches the host permission
10
+ * events and closes abandoned requests on the safe side.
11
+ *
12
+ * REAL host contract (1.18.29, probed live via SSE + the plugin event hook —
13
+ * see gate-test report-p5):
14
+ * permission.asked → properties = { id, sessionID, permission: "bash",
15
+ * patterns: [<concrete command segment>, …], metadata:
16
+ * { command }, always: [<host's "always" generalization
17
+ * proposal>], tool: { messageID, callID } } — the props
18
+ * carry NO `type` field, and the tool name sits in
19
+ * `permission`. A props matching one of OUR injected
20
+ * bash ask sets means the dialog is ours: audit "ask",
21
+ * arm the timeout, and — for the R6 env face — mark
22
+ * the session deferrable (canDefer, see below).
23
+ * permission.replied → properties = { sessionID, requestID, reply } on the
24
+ * wire, but the plugin-side observer has also seen it
25
+ * with ONLY { sessionID }. The gate therefore cancels
26
+ * EVERY pending timer of that session on a reply: an
27
+ * already-approved command left on a live timer would
28
+ * get a second reject after the timeout, the SDK call
29
+ * would hit a dead id (4xx) and flip the gate
30
+ * permanently degraded.
31
+ *
32
+ * Older/typed spellings (`permission.updated`, `pattern`, `permissionID`,
33
+ * `response`) keep being accepted — the shipped SDK d.ts still names the
34
+ * events that way, and some builds omit the reply id entirely (a short
35
+ * ghost-suppression window per session then absorbs late/duplicate `asked`
36
+ * replays instead of re-arming a ghost timer).
37
+ *
38
+ * Hard rules (HUMAN-approved):
39
+ * - the plugin NEVER self-allows: `reply` is fixed to "reject". The only
40
+ * way a gated command runs is a human approving the dialog;
41
+ * - on `TM_ENV_PROTECT=off` the gate is not armed (index.ts decides);
42
+ * - deferral is SESSION-SCOPED (canDefer): only sessions registered as
43
+ * carrying our injected ask set may have env reads passed through —
44
+ * registration comes from an exec-role user prompt (the live-proven
45
+ * `message.updated` UserMessage{role,agent,sessionID} signal, plus the
46
+ * documented `chat.message` hook) or from an R6-env-classified asked
47
+ * event. Stock build/plan sessions never register, so the global R6
48
+ * hook keeps hard-throwing there — closing the R6 bypass the global
49
+ * `isArmed()` deferral allowed;
50
+ * - if the SDK reply FAILS (network/host — including the v1
51
+ * `throwOnError:false` envelope resolving with `{ error }`), we cannot
52
+ * close the dialog, so the gate flips itself permanently "degraded" —
53
+ * the R6 hook stops deferring env reads and hard-throws them again
54
+ * (fail-closed), and the event is audited "degraded".
55
+ *
56
+ * SDK surface (probed live on 1.18.x): the plugin's `input.client` is the v1
57
+ * `OpencodeClient`, which has NO `permission` namespace and NO list/create
58
+ * endpoint; the reply path is the top-level
59
+ * `postSessionIdPermissionsPermissionId({ path: { id: sessionID,
60
+ * permissionID }, body: { response } })`. `client.permission.reply` /
61
+ * `client.permission.list` are still tried first so a richer host build is
62
+ * supported, but on v1 they are simply absent and the poll fallback is a
63
+ * no-op (events are the sole pending signal there).
64
+ */
65
+ import { ENV_PROTECT_SERVICE, categorizePermission } from "./envprotect.js";
66
+ /** Default timeout, in minutes, before an unanswered dialog is auto-rejected. */
67
+ export const DEFAULT_ASK_TIMEOUT_MIN = 10;
68
+ /**
69
+ * Hard floor (minutes) for the auto-reject timeout. Measured live on the
70
+ * OpenCode Desktop: a `permission.replied` (human answered "once") reaches
71
+ * the plugin through the host event bus ~120s LATE — a 1-minute timer
72
+ * therefore fires BEFORE the cancel arrives and auto-rejects the just-
73
+ * approved request on a dead id (4xx → permanent degraded, the D4
74
+ * double-reject race). 3 min = observed ~120s lag + margin; in-range
75
+ * values below the floor clamp UP to it (a longer timeout is always the
76
+ * safe direction — the dialog just stays open a bit longer).
77
+ */
78
+ export const MIN_ASK_TIMEOUT_MIN = 3;
79
+ /** Poll cadence (ms) that re-scans pending permissions in case an event was
80
+ * missed — only meaningful when the client exposes a list capability. */
81
+ const POLL_INTERVAL_MS = 60 * 1000;
82
+ /** Suppression window (ms) for late/duplicate `permission.asked` replays
83
+ * after a `permission.replied` that carried NO permission id. Ghost timers
84
+ * are the degraded-flip poison (dead id → 4xx → permanent degraded); the
85
+ * window only sacrifices the timeout for brand-new asks of the same session
86
+ * for this short period, never a self-allow. */
87
+ const GHOST_ASK_WINDOW_MS = 60 * 1000;
88
+ /** Cap on the closed-permission-id tombstone set (ids are unique, so the
89
+ * set is purely an anti-ghost cache — pruning oldest is sufficient). */
90
+ const MAX_TOMBSTONES = 500;
91
+ /**
92
+ * Resolve `TM_ASK_TIMEOUT_MIN`. Unset / blank / non-numeric / <1 / >1440
93
+ * (24h) fall back to the 10-minute default — a mistyped value can never
94
+ * disable the timeout or make it absurdly long. Valid values below the
95
+ * 3-minute bus-lag floor clamp UP to it (1→3, 2→3; see
96
+ * `MIN_ASK_TIMEOUT_MIN` for the measured ~120s replied-event lag).
97
+ */
98
+ export function resolveAskTimeoutMs(env = process.env) {
99
+ const raw = env.TM_ASK_TIMEOUT_MIN;
100
+ if (typeof raw !== "string" || raw.trim() === "")
101
+ return DEFAULT_ASK_TIMEOUT_MIN * 60 * 1000;
102
+ const n = Number(raw.trim());
103
+ if (!Number.isFinite(n))
104
+ return DEFAULT_ASK_TIMEOUT_MIN * 60 * 1000;
105
+ const min = Math.trunc(n);
106
+ if (min < 1 || min > 1440)
107
+ return DEFAULT_ASK_TIMEOUT_MIN * 60 * 1000;
108
+ return Math.max(min, MIN_ASK_TIMEOUT_MIN) * 60 * 1000;
109
+ }
110
+ /** Best-effort structured audit; a failing log endpoint never breaks the gate. */
111
+ function auditAsk(client, tool, category, verdict) {
112
+ try {
113
+ // `app.log({...})` is a METHOD call on `app` — casting the fetched
114
+ // function and invoking it plain (`(app.log as Fn)({...})`) drops the
115
+ // SDK `this` exactly like the historic R6 audit bug did, silently
116
+ // killing every gate audit line (pinned by the this-bound §7 mocks).
117
+ const app = client?.app;
118
+ if (!app || typeof app.log !== "function")
119
+ return;
120
+ void app.log({
121
+ body: {
122
+ level: "warn",
123
+ service: ENV_PROTECT_SERVICE,
124
+ message: `${ENV_PROTECT_SERVICE} :: ${tool} :: ${category} :: ${verdict}`,
125
+ },
126
+ });
127
+ }
128
+ catch {
129
+ /* audit is best-effort */
130
+ }
131
+ }
132
+ /**
133
+ * v1 SDK default is `throwOnError:false` (types/sdk.gen contract): an HTTP
134
+ * failure — e.g. a 4xx on an already-closed permission id — RESOLVES with an
135
+ * `{ error }` envelope instead of rejecting. Unwrapping it into a throw is
136
+ * what makes the Layer-3 degraded flip actually fire on the real host.
137
+ */
138
+ function unwrapSdkEnvelope(res) {
139
+ if (res && typeof res === "object" && "error" in res) {
140
+ const err = res.error;
141
+ if (err)
142
+ throw err;
143
+ }
144
+ return res;
145
+ }
146
+ /**
147
+ * Non-privacy diagnostic appended to a `degraded` audit verdict: the error
148
+ * CLASS name and host status code only — never message text, request params
149
+ * or paths (privacy red line). Handles both thrown Errors and v1 envelope
150
+ * bodies (`{ error: { name, status | status_code } }`); the name passes an
151
+ * identifier whitelist so hostile/echoed text can never ride into the log.
152
+ */
153
+ function errorDiagnostic(err) {
154
+ const e = err;
155
+ const name = typeof e?.name === "string" && /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(e.name)
156
+ ? e.name
157
+ : "Error";
158
+ const rawStatus = [e?.status, e?.statusCode, e?.status_code].find((v) => typeof v === "number" && Number.isFinite(v));
159
+ return ` err=${name}${rawStatus !== undefined ? ` status=${rawStatus}` : ""}`;
160
+ }
161
+ /**
162
+ * Build the reject closure. CRITICAL: every SDK endpoint is invoked as a
163
+ * METHOD (`perm.reply({...})` / `c.post...({...})` — property-access call
164
+ * sites keep the receiver binding). Fetching the function first
165
+ * (`const post = c.post...` then `await post({...})`) drops the SDK `this`,
166
+ * so EVERY outbound reply died synchronously on the real host — the timer
167
+ * fired at ask+timeout yet the dialog stayed open and the gate flipped
168
+ * `degraded` (same regression class as the historic app.log audit bug; the
169
+ * §7 test mocks now require the binding so it can never come back).
170
+ */
171
+ function replyCapableFn(client) {
172
+ const c = client;
173
+ if (!c)
174
+ return null;
175
+ const perm = c.permission;
176
+ if (perm && typeof perm.reply === "function") {
177
+ return async (sid, pid) => {
178
+ // richest host first (permission.reply), reject-only, both param shapes
179
+ return unwrapSdkEnvelope(
180
+ // `!` + property-access call site: binding survives, compiler stays happy
181
+ await perm.reply({ path: { sessionID: sid, id: pid }, body: { response: "reject" } }));
182
+ };
183
+ }
184
+ if (typeof c.postSessionIdPermissionsPermissionId === "function") {
185
+ return async (sid, pid) => unwrapSdkEnvelope(await c.postSessionIdPermissionsPermissionId({
186
+ path: { id: sid, permissionID: pid },
187
+ body: { response: "reject" },
188
+ }));
189
+ }
190
+ return null;
191
+ }
192
+ function listCapableFn(client) {
193
+ const c = client;
194
+ const perm = c?.permission;
195
+ if (perm && typeof perm.list === "function") {
196
+ // method call on `perm` — `this` must survive (see replyCapableFn note)
197
+ return async () => await perm.list({});
198
+ }
199
+ return null;
200
+ }
201
+ /** True when the client exposes a permission-reply path at all. index.ts
202
+ * arms the gate ONLY for such a client — fail-closed otherwise (no armed
203
+ * auto-reject ⇒ the R6 hook keeps hard-throwing every env read). */
204
+ export function hasPermissionReplyCapability(client) {
205
+ return replyCapableFn(client) !== null;
206
+ }
207
+ /** First string-ish value among the host's several id/response spellings. */
208
+ function str(...values) {
209
+ for (const v of values) {
210
+ if (typeof v === "string" && v.trim() !== "")
211
+ return v.trim();
212
+ }
213
+ return undefined;
214
+ }
215
+ /**
216
+ * Create the approval gate. It is intentionally pure about timing +
217
+ * replying + session registration; the caller (index.ts) decides whether to
218
+ * arm it (mode != off AND the client is reply-capable), feeds events in, and
219
+ * wires `canDefer(sessionID)` into the R6 hook.
220
+ */
221
+ export function createApprovalGate(deps) {
222
+ const { client, timeoutMs } = deps;
223
+ const timers = deps.timers ?? {
224
+ // unref() so a pending timeout never, on its own, keeps the host process
225
+ // alive (same philosophy as the blackboard sweeper's unref'd interval)
226
+ setTimeoutFn: (cb, ms) => {
227
+ const t = setTimeout(cb, ms);
228
+ if (typeof t?.unref === "function")
229
+ t.unref();
230
+ return t;
231
+ },
232
+ clearTimeoutFn: (h) => clearTimeout(h),
233
+ };
234
+ const nowMs = deps.now ?? (() => Date.now());
235
+ const reply = replyCapableFn(client);
236
+ const list = listCapableFn(client);
237
+ const pending = new Map();
238
+ /** Sessions known to carry our injected ask set (deferral whitelist). */
239
+ const liveAsk = new Set();
240
+ /** Permission ids already closed (replied / timed out): a late or
241
+ * duplicated `asked` replay for one must never re-arm a ghost timer. */
242
+ const closedIds = new Set();
243
+ /** Sessions whose reply carried NO permission id → suppress fresh asks of
244
+ * that session briefly (map sid → expiry ms). */
245
+ const repliedNoId = new Map();
246
+ /** Sessions blanket-approved for env reads ("always" on an env ask). */
247
+ const envApprovedSessions = new Set();
248
+ let armed = true;
249
+ let degraded = false;
250
+ let pollHandle = null;
251
+ function remove(id) {
252
+ const rec = pending.get(id);
253
+ if (rec) {
254
+ timers.clearTimeoutFn(rec.timer);
255
+ pending.delete(id);
256
+ }
257
+ return rec;
258
+ }
259
+ function tombstone(id) {
260
+ closedIds.add(id);
261
+ if (closedIds.size > MAX_TOMBSTONES) {
262
+ const oldest = closedIds.values().next().value;
263
+ if (oldest !== undefined)
264
+ closedIds.delete(oldest);
265
+ }
266
+ }
267
+ /** True while sid is inside the no-id-reply ghost window. */
268
+ function ghostSuppressed(sid) {
269
+ const expiry = repliedNoId.get(sid);
270
+ if (expiry === undefined)
271
+ return false;
272
+ if (expiry <= nowMs()) {
273
+ repliedNoId.delete(sid);
274
+ return false;
275
+ }
276
+ return true;
277
+ }
278
+ async function fireReject(id, cat) {
279
+ const rec = pending.get(id);
280
+ if (!rec)
281
+ return;
282
+ pending.delete(id);
283
+ tombstone(id);
284
+ let ok = false;
285
+ let errDiag = "";
286
+ try {
287
+ if (reply) {
288
+ // unwrapSdkEnvelope inside `reply` turns the v1 {error} envelope into
289
+ // a throw; a rejecting transport lands in the same catch.
290
+ await reply(rec.sid, id);
291
+ ok = true;
292
+ }
293
+ }
294
+ catch (err) {
295
+ ok = false;
296
+ errDiag = errorDiagnostic(err);
297
+ }
298
+ if (!ok) {
299
+ // Layer 3 fallback: we could not close the dialog ⇒ stop trusting the
300
+ // popup path; the hook reverts to hard-throwing env reads.
301
+ degraded = true;
302
+ }
303
+ // degraded carries the non-privacy error class/status tail (see
304
+ // errorDiagnostic) so a live-host failure is diagnosable from the log
305
+ auditAsk(client, "bash", cat, (ok ? "timeout-rejected" : "degraded") + errDiag);
306
+ }
307
+ function arm(id, sid, cat) {
308
+ if (pending.has(id))
309
+ return;
310
+ const timer = timers.setTimeoutFn(() => void fireReject(id, cat), timeoutMs);
311
+ pending.set(id, { sid, cat, timer });
312
+ }
313
+ /** Single registration funnel for both event and poll discovery. */
314
+ function registerAsk(id, sid, cat) {
315
+ if (!id || !sid)
316
+ return;
317
+ if (closedIds.has(id))
318
+ return; // ghost replay of a closed request
319
+ if (ghostSuppressed(sid))
320
+ return; // replied-without-id; assume a replay
321
+ if (pending.has(id))
322
+ return;
323
+ // The R6 ENV face is the deferral proof: only sessions whose dialogs
324
+ // actually carry our injected env-ask patterns (or an exec-role
325
+ // chat.message, see registerExecSession) may defer env reads. Danger
326
+ // faces never register — stock hosts that ask on `rm` by default must
327
+ // not gain env deferral.
328
+ if (cat === "env")
329
+ liveAsk.add(sid);
330
+ auditAsk(client, "bash", cat, "ask");
331
+ arm(id, sid, cat);
332
+ }
333
+ async function pollOnce() {
334
+ if (!list || !armed)
335
+ return;
336
+ try {
337
+ const res = await list();
338
+ const data = Array.isArray(res) ? res : res?.data;
339
+ if (Array.isArray(data)) {
340
+ for (const item of data) {
341
+ const props = item?.properties ?? item;
342
+ const cat = categorizePermission(props);
343
+ if (!cat)
344
+ continue;
345
+ const p = props;
346
+ registerAsk(str(p.id, p.permissionID, p.requestID), str(p.sessionID), cat);
347
+ }
348
+ }
349
+ }
350
+ catch {
351
+ /* poll is best-effort */
352
+ }
353
+ }
354
+ function schedulePoll() {
355
+ pollHandle = timers.setTimeoutFn(() => {
356
+ void pollOnce().finally(() => {
357
+ // dispose() during an in-flight poll must not re-arm a zombie chain
358
+ if (armed)
359
+ schedulePoll();
360
+ });
361
+ }, POLL_INTERVAL_MS);
362
+ }
363
+ function onAsked(props) {
364
+ if (degraded)
365
+ return; // auto-reject is broken — do not open new timers
366
+ const cat = categorizePermission(props);
367
+ if (!cat)
368
+ return; // not one of our injected bash asks → leave it to the human
369
+ registerAsk(str(props.id, props.permissionID, props.requestID), str(props.sessionID), cat);
370
+ }
371
+ function onReplied(props) {
372
+ const sid = str(props.sessionID);
373
+ const id = str(props.requestID, props.permissionID, props.id);
374
+ const resp = str(props.reply, props.response);
375
+ if (id)
376
+ tombstone(id);
377
+ // Real 1.18.29 replied props identify the request via `requestID` — and
378
+ // the plugin-side observer has seen builds carrying ONLY { sessionID }.
379
+ // Cancel EVERY pending timer of the session either way: an approved
380
+ // command left timed would fire a reject on a dead id (4xx) and flip the
381
+ // gate permanently degraded.
382
+ const affected = [];
383
+ if (id) {
384
+ const rec = remove(id);
385
+ if (rec)
386
+ affected.push(rec);
387
+ }
388
+ for (const [pid, rec] of [...pending]) {
389
+ if (sid && rec.sid === sid) {
390
+ pending.delete(pid);
391
+ timers.clearTimeoutFn(rec.timer);
392
+ tombstone(pid);
393
+ affected.push(rec);
394
+ }
395
+ }
396
+ if (!id && sid)
397
+ repliedNoId.set(sid, nowMs() + GHOST_ASK_WINDOW_MS);
398
+ for (const rec of affected) {
399
+ if (!resp)
400
+ continue; // outcome unknown: cancel silently, never invent a verdict
401
+ const lower = resp.toLowerCase();
402
+ const verdict = lower === "reject" ? "rejected"
403
+ : lower === "always" ? "allowed-always"
404
+ : lower === "once" ? "allowed-once"
405
+ : "degraded"; // out-of-vocabulary word: record it honestly, not as "rejected"
406
+ auditAsk(client, "bash", rec.cat, verdict);
407
+ // "always" on an env-related ask → blanket-approve all env reads for
408
+ // this session (the host's pattern generalization is too broad, so we
409
+ // interpret "always" as a session-scoped env approval).
410
+ if (verdict === "allowed-always" && sid && /^(bash-)?env/.test(rec.cat)) {
411
+ envApprovedSessions.add(sid);
412
+ }
413
+ }
414
+ }
415
+ /** Global gate health: armed, not degraded, and the client can reply. */
416
+ function healthy() {
417
+ return armed && !degraded && reply !== null;
418
+ }
419
+ return {
420
+ handleEvent(event) {
421
+ if (!armed)
422
+ return; // disposed
423
+ const type = String(event?.type ?? "");
424
+ const props = event?.properties;
425
+ if (!props)
426
+ return;
427
+ // live host 1.18.29 emits `permission.asked`; the shipped d.ts spells
428
+ // the same signal `permission.updated` — accept both open spellings
429
+ if (type === "permission.asked" || type === "permission.updated")
430
+ onAsked(props);
431
+ else if (type === "permission.replied")
432
+ onReplied(props);
433
+ },
434
+ start() {
435
+ // v1 has no list endpoint (`list` is null) — events are the only
436
+ // pending signal there, so start() is a deliberate no-op on v1.
437
+ if (!list || !armed)
438
+ return;
439
+ schedulePoll();
440
+ },
441
+ dispose() {
442
+ armed = false;
443
+ if (pollHandle != null)
444
+ timers.clearTimeoutFn(pollHandle);
445
+ pollHandle = null;
446
+ for (const id of [...pending.keys()])
447
+ remove(id);
448
+ },
449
+ isArmed() {
450
+ return healthy();
451
+ },
452
+ canDefer(sessionID) {
453
+ // bound-method-safe (no `this`): destructure-able by the R6 hook
454
+ return healthy() && !!sessionID && liveAsk.has(sessionID);
455
+ },
456
+ hasLiveAsk(sessionID) {
457
+ return sessionID ? liveAsk.has(sessionID) : false;
458
+ },
459
+ registerExecSession(sessionID) {
460
+ if (sessionID)
461
+ liveAsk.add(sessionID);
462
+ },
463
+ isEnvApproved(sessionID) {
464
+ return !!sessionID && envApprovedSessions.has(sessionID);
465
+ },
466
+ pendingSize() {
467
+ return pending.size;
468
+ },
469
+ };
470
+ }
471
+ //# sourceMappingURL=approval-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-gate.js","sourceRoot":"","sources":["../src/approval-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAGH,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3E,iFAAiF;AACjF,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAA;AAEzC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAA;AAEpC;0EAC0E;AAC1E,MAAM,gBAAgB,GAAG,EAAE,GAAG,IAAI,CAAA;AAElC;;;;iDAIiD;AACjD,MAAM,mBAAmB,GAAG,EAAE,GAAG,IAAI,CAAA;AAErC;yEACyE;AACzE,MAAM,cAAc,GAAG,GAAG,CAAA;AAY1B;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAA0C,OAAO,CAAC,GAAG;IACvF,MAAM,GAAG,GAAG,GAAG,CAAC,kBAAkB,CAAA;IAClC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,uBAAuB,GAAG,EAAE,GAAG,IAAI,CAAA;IAC5F,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAA;IAC5B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,uBAAuB,GAAG,EAAE,GAAG,IAAI,CAAA;IACnE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACzB,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,IAAI;QAAE,OAAO,uBAAuB,GAAG,EAAE,GAAG,IAAI,CAAA;IACrE,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,mBAAmB,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;AACvD,CAAC;AAmDD,kFAAkF;AAClF,SAAS,QAAQ,CAAC,MAAe,EAAE,IAAY,EAAE,QAAgB,EAAE,OAAe;IAChF,IAAI,CAAC;QACH,mEAAmE;QACnE,sEAAsE;QACtE,kEAAkE;QAClE,qEAAqE;QACrE,MAAM,GAAG,GAAI,MAA2E,EAAE,GAAG,CAAA;QAC7F,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,GAAG,KAAK,UAAU;YAAE,OAAM;QACjD,KAAK,GAAG,CAAC,GAAG,CAAC;YACX,IAAI,EAAE;gBACJ,KAAK,EAAE,MAAM;gBACb,OAAO,EAAE,mBAAmB;gBAC5B,OAAO,EAAE,GAAG,mBAAmB,OAAO,IAAI,OAAO,QAAQ,OAAO,OAAO,EAAE;aAC1E;SACF,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,0BAA0B;IAC5B,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,GAAY;IACrC,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,IAAI,GAAG,EAAE,CAAC;QACrD,MAAM,GAAG,GAAI,GAA2B,CAAC,KAAK,CAAA;QAC9C,IAAI,GAAG;YAAE,MAAM,GAAG,CAAA;IACpB,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,GAAY;IACnC,MAAM,CAAC,GAAG,GAKU,CAAA;IACpB,MAAM,IAAI,GACR,OAAO,CAAC,EAAE,IAAI,KAAK,QAAQ,IAAI,gCAAgC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1E,CAAC,CAAC,CAAC,CAAC,IAAI;QACR,CAAC,CAAC,OAAO,CAAA;IACb,MAAM,SAAS,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,IAAI,CAC/D,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CACnD,CAAA;IACD,OAAO,QAAQ,IAAI,GAAG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;AAC/E,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,cAAc,CAAC,MAAe;IACrC,MAAM,CAAC,GAAG,MAGU,CAAA;IACpB,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IACnB,MAAM,IAAI,GAAG,CAAC,CAAC,UAAU,CAAA;IACzB,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;QAC7C,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;YACxB,wEAAwE;YACxE,OAAO,iBAAiB;YACtB,0EAA0E;YAC1E,MAAM,IAAI,CAAC,KAAM,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,CACvF,CAAA;QACH,CAAC,CAAA;IACH,CAAC;IACD,IAAI,OAAO,CAAC,CAAC,oCAAoC,KAAK,UAAU,EAAE,CAAC;QACjE,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CACxB,iBAAiB,CACf,MAAM,CAAC,CAAC,oCAAqC,CAAC;YAC5C,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE;YACpC,IAAI,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;SAC7B,CAAC,CACH,CAAA;IACL,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,aAAa,CAAC,MAAe;IACpC,MAAM,CAAC,GAAG,MAAoF,CAAA;IAC9F,MAAM,IAAI,GAAG,CAAC,EAAE,UAAU,CAAA;IAC1B,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC5C,wEAAwE;QACxE,OAAO,KAAK,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,IAAK,CAAC,EAAE,CAAC,CAAA;IACzC,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;qEAEqE;AACrE,MAAM,UAAU,4BAA4B,CAAC,MAAe;IAC1D,OAAO,cAAc,CAAC,MAAM,CAAC,KAAK,IAAI,CAAA;AACxC,CAAC;AAQD,6EAA6E;AAC7E,SAAS,GAAG,CAAC,GAAG,MAAiB;IAC/B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAA;IAC/D,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAsB;IACvD,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;IAClC,MAAM,MAAM,GAAe,IAAI,CAAC,MAAM,IAAI;QACxC,yEAAyE;QACzE,uEAAuE;QACvE,YAAY,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;YACvB,MAAM,CAAC,GAAG,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;YAC5B,IAAI,OAAO,CAAC,EAAE,KAAK,KAAK,UAAU;gBAAE,CAAC,CAAC,KAAK,EAAE,CAAA;YAC7C,OAAO,CAAC,CAAA;QACV,CAAC;QACD,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAmB,CAAC;KACzD,CAAA;IACD,MAAM,KAAK,GAAiB,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;IAC1D,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,CAAA;IACpC,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;IAClC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAA;IAC/C,yEAAyE;IACzE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAA;IACjC;6EACyE;IACzE,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAA;IACnC;sDACkD;IAClD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC7C,wEAAwE;IACxE,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAU,CAAA;IAC7C,IAAI,KAAK,GAAG,IAAI,CAAA;IAChB,IAAI,QAAQ,GAAG,KAAK,CAAA;IACpB,IAAI,UAAU,GAAgB,IAAI,CAAA;IAElC,SAAS,MAAM,CAAC,EAAU;QACxB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC3B,IAAI,GAAG,EAAE,CAAC;YACR,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YAChC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACpB,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,SAAS,SAAS,CAAC,EAAU;QAC3B,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACjB,IAAI,SAAS,CAAC,IAAI,GAAG,cAAc,EAAE,CAAC;YACpC,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAA2B,CAAA;YACpE,IAAI,MAAM,KAAK,SAAS;gBAAE,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACpD,CAAC;IACH,CAAC;IAED,6DAA6D;IAC7D,SAAS,eAAe,CAAC,GAAW;QAClC,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,KAAK,CAAA;QACtC,IAAI,MAAM,IAAI,KAAK,EAAE,EAAE,CAAC;YACtB,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACvB,OAAO,KAAK,CAAA;QACd,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,UAAU,UAAU,CAAC,EAAU,EAAE,GAAqB;QACzD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC3B,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAClB,SAAS,CAAC,EAAE,CAAC,CAAA;QACb,IAAI,EAAE,GAAG,KAAK,CAAA;QACd,IAAI,OAAO,GAAG,EAAE,CAAA;QAChB,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,CAAC;gBACV,sEAAsE;gBACtE,0DAA0D;gBAC1D,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;gBACxB,EAAE,GAAG,IAAI,CAAA;YACX,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,EAAE,GAAG,KAAK,CAAA;YACV,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,CAAA;QAChC,CAAC;QACD,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,sEAAsE;YACtE,2DAA2D;YAC3D,QAAQ,GAAG,IAAI,CAAA;QACjB,CAAC;QACD,gEAAgE;QAChE,sEAAsE;QACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,CAAA;IACjF,CAAC;IAED,SAAS,GAAG,CAAC,EAAU,EAAE,GAAW,EAAE,GAAqB;QACzD,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAM;QAC3B,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,KAAK,UAAU,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,SAAS,CAAC,CAAA;QAC5E,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAA;IACtC,CAAC;IAED,oEAAoE;IACpE,SAAS,WAAW,CAClB,EAAsB,EACtB,GAAuB,EACvB,GAAqB;QAErB,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG;YAAE,OAAM;QACvB,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAM,CAAC,mCAAmC;QACjE,IAAI,eAAe,CAAC,GAAG,CAAC;YAAE,OAAM,CAAC,sCAAsC;QACvE,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAM;QAC3B,qEAAqE;QACrE,gEAAgE;QAChE,sEAAsE;QACtE,sEAAsE;QACtE,yBAAyB;QACzB,IAAI,GAAG,KAAK,KAAK;YAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;QACpC,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;IACnB,CAAC;IAED,KAAK,UAAU,QAAQ;QACrB,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK;YAAE,OAAM;QAC3B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,IAAI,EAAE,CAAA;YACxB,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAE,GAA0B,EAAE,IAAI,CAAA;YACzE,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxB,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;oBACxB,MAAM,KAAK,GAAI,IAAiC,EAAE,UAAU,IAAI,IAAI,CAAA;oBACpE,MAAM,GAAG,GAAG,oBAAoB,CAAC,KAAmD,CAAC,CAAA;oBACrF,IAAI,CAAC,GAAG;wBAAE,SAAQ;oBAClB,MAAM,CAAC,GAAG,KAAuF,CAAA;oBACjG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,SAAS,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,GAAG,CAAC,CAAA;gBAC5E,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;IACH,CAAC;IAED,SAAS,YAAY;QACnB,UAAU,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE;YACpC,KAAK,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;gBAC3B,oEAAoE;gBACpE,IAAI,KAAK;oBAAE,YAAY,EAAE,CAAA;YAC3B,CAAC,CAAC,CAAA;QACJ,CAAC,EAAE,gBAAgB,CAAC,CAAA;IACtB,CAAC;IAED,SAAS,OAAO,CAAC,KAAiD;QAChE,IAAI,QAAQ;YAAE,OAAM,CAAC,iDAAiD;QACtE,MAAM,GAAG,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAA;QACvC,IAAI,CAAC,GAAG;YAAE,OAAM,CAAC,4DAA4D;QAC7E,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,GAAG,CAAC,CAAA;IAC5F,CAAC;IAED,SAAS,SAAS,CAAC,KAAiD;QAClE,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;QAChC,MAAM,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,CAAA;QAC7D,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;QAC7C,IAAI,EAAE;YAAE,SAAS,CAAC,EAAE,CAAC,CAAA;QACrB,wEAAwE;QACxE,wEAAwE;QACxE,oEAAoE;QACpE,yEAAyE;QACzE,6BAA6B;QAC7B,MAAM,QAAQ,GAAwB,EAAE,CAAA;QACxC,IAAI,EAAE,EAAE,CAAC;YACP,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;YACtB,IAAI,GAAG;gBAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC7B,CAAC;QACD,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC;YACtC,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;gBAC3B,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;gBACnB,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;gBAChC,SAAS,CAAC,GAAG,CAAC,CAAA;gBACd,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACpB,CAAC;QACH,CAAC;QACD,IAAI,CAAC,EAAE,IAAI,GAAG;YAAE,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,mBAAmB,CAAC,CAAA;QACnE,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI;gBAAE,SAAQ,CAAC,2DAA2D;YAC/E,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;YAChC,MAAM,OAAO,GACX,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU;gBAC/B,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,gBAAgB;oBACvC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,cAAc;wBACnC,CAAC,CAAC,UAAU,CAAA,CAAC,gEAAgE;YAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;YAC1C,qEAAqE;YACrE,sEAAsE;YACtE,wDAAwD;YACxD,IAAI,OAAO,KAAK,gBAAgB,IAAI,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBACxE,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YAC9B,CAAC;QACH,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,SAAS,OAAO;QACd,OAAO,KAAK,IAAI,CAAC,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAA;IAC7C,CAAC;IAED,OAAO;QACL,WAAW,CAAC,KAAsB;YAChC,IAAI,CAAC,KAAK;gBAAE,OAAM,CAAC,WAAW;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,IAAI,EAAE,CAAC,CAAA;YACtC,MAAM,KAAK,GAAG,KAAK,EAAE,UAAU,CAAA;YAC/B,IAAI,CAAC,KAAK;gBAAE,OAAM;YAClB,sEAAsE;YACtE,oEAAoE;YACpE,IAAI,IAAI,KAAK,kBAAkB,IAAI,IAAI,KAAK,oBAAoB;gBAAE,OAAO,CAAC,KAAK,CAAC,CAAA;iBAC3E,IAAI,IAAI,KAAK,oBAAoB;gBAAE,SAAS,CAAC,KAAK,CAAC,CAAA;QAC1D,CAAC;QACD,KAAK;YACH,iEAAiE;YACjE,gEAAgE;YAChE,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK;gBAAE,OAAM;YAC3B,YAAY,EAAE,CAAA;QAChB,CAAC;QACD,OAAO;YACL,KAAK,GAAG,KAAK,CAAA;YACb,IAAI,UAAU,IAAI,IAAI;gBAAE,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,CAAA;YACzD,UAAU,GAAG,IAAI,CAAA;YACjB,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;gBAAE,MAAM,CAAC,EAAE,CAAC,CAAA;QAClD,CAAC;QACD,OAAO;YACL,OAAO,OAAO,EAAE,CAAA;QAClB,CAAC;QACD,QAAQ,CAAC,SAAkB;YACzB,iEAAiE;YACjE,OAAO,OAAO,EAAE,IAAI,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC3D,CAAC;QACD,UAAU,CAAC,SAAkB;YAC3B,OAAO,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;QACnD,CAAC;QACD,mBAAmB,CAAC,SAAkB;YACpC,IAAI,SAAS;gBAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACvC,CAAC;QACD,aAAa,CAAC,SAAkB;YAC9B,OAAO,CAAC,CAAC,SAAS,IAAI,mBAAmB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC1D,CAAC;QACD,WAAW;YACT,OAAO,OAAO,CAAC,IAAI,CAAA;QACrB,CAAC;KACF,CAAA;AACH,CAAC"}
package/dist/commands.js CHANGED
@@ -85,7 +85,7 @@ If no specific scope is given, identify the most recently modified source files
85
85
  };
86
86
  /* ------------------------------------------------------------------ */
87
87
  const teamResearch = {
88
- description: "Research a topic — libraries, APIs, best practices, or documentation.",
88
+ description: "Research a topic — grounded in the local repository (code, configs, installed packages, shipped docs).",
89
89
  agent: "researcher",
90
90
  template: `Research the following topic and provide actionable findings.
91
91
 
@@ -96,9 +96,9 @@ $ARGUMENTS
96
96
  1. **Summary** — Key findings in 2-3 sentences.
97
97
  2. **Details** — Structured findings with sources.
98
98
  3. **Recommendation** — What the team should do, with trade-offs.
99
- 4. **Sources** — Links or file paths consulted.
99
+ 4. **Sources** — Local file paths consulted (file:line).
100
100
 
101
- Cite your sources. Do not fabricate URLs or API details.`,
101
+ Cite your sources with file:line paths. Do not fabricate APIs or citations.`,
102
102
  };
103
103
  /* ------------------------------------------------------------------ */
104
104
  const teamRun = {