@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.
- package/README.md +109 -14
- package/README.zh-CN.md +92 -13
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +128 -26
- package/dist/agents.js.map +1 -1
- package/dist/approval-gate.d.ts +147 -0
- package/dist/approval-gate.d.ts.map +1 -0
- package/dist/approval-gate.js +471 -0
- package/dist/approval-gate.js.map +1 -0
- package/dist/commands.js +3 -3
- package/dist/commands.js.map +1 -1
- package/dist/envprotect.d.ts +115 -4
- package/dist/envprotect.d.ts.map +1 -1
- package/dist/envprotect.js +364 -11
- package/dist/envprotect.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +85 -2
- package/dist/index.js.map +1 -1
- package/dist/tm/config.d.ts +34 -0
- package/dist/tm/config.d.ts.map +1 -1
- package/dist/tm/config.js +21 -0
- package/dist/tm/config.js.map +1 -1
- package/dist/tm/guard.js +4 -4
- package/dist/tm/guard.js.map +1 -1
- package/dist/tm/index.d.ts +4 -2
- package/dist/tm/index.d.ts.map +1 -1
- package/dist/tm/index.js +32 -2
- package/dist/tm/index.js.map +1 -1
- package/dist/tm/ptc.d.ts +279 -0
- package/dist/tm/ptc.d.ts.map +1 -0
- package/dist/tm/ptc.js +839 -0
- package/dist/tm/ptc.js.map +1 -0
- package/dist/tm/tools.d.ts +16 -0
- package/dist/tm/tools.d.ts.map +1 -1
- package/dist/tm/tools.js +5 -1
- package/dist/tm/tools.js.map +1 -1
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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":"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"}
|
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 —
|
|
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** —
|
|
99
|
+
4. **Sources** — Local file paths consulted (file:line).
|
|
100
100
|
|
|
101
|
-
Cite your sources. Do not fabricate
|
|
101
|
+
Cite your sources with file:line paths. Do not fabricate APIs or citations.`,
|
|
102
102
|
};
|
|
103
103
|
/* ------------------------------------------------------------------ */
|
|
104
104
|
const teamRun = {
|