dsh-perm-gate 2.1.0 → 3.0.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/CHANGELOG.ja.md +82 -0
- package/CHANGELOG.ko.md +80 -0
- package/CHANGELOG.md +321 -0
- package/INSTALL.ja.md +70 -8
- package/INSTALL.ko.md +68 -9
- package/INSTALL.md +68 -8
- package/INSTALL.zh.md +59 -7
- package/README.ja.md +103 -16
- package/README.ko.md +101 -17
- package/README.md +152 -25
- package/README.zh.md +99 -12
- package/cordis.patch.yml +34 -3
- package/docs/baseline-guard.md +56 -0
- package/docs/plan-rules-to-settings.md +217 -0
- package/docs/rules-format.en.md +294 -0
- package/docs/rules-format.md +279 -0
- package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
- package/dsh.plugin.json +22 -0
- package/examples/permissions.example.yaml +16 -1
- package/lib/agent-identity.d.ts +43 -0
- package/lib/agent-identity.js +95 -0
- package/lib/allowlist.d.ts +27 -0
- package/lib/allowlist.js +103 -1
- package/lib/cli.js +11 -14
- package/lib/client.js +598 -6
- package/lib/client.js.map +1 -1
- package/lib/command-dispatcher.d.ts +22 -0
- package/lib/command-dispatcher.js +97 -0
- package/lib/command-semantics.d.ts +62 -0
- package/lib/command-semantics.js +6 -0
- package/lib/compiler.d.ts +50 -0
- package/lib/compiler.js +122 -0
- package/lib/config.d.ts +161 -7
- package/lib/config.js +78 -11
- package/lib/dry-run.d.ts +87 -0
- package/lib/dry-run.js +110 -0
- package/lib/engine.js +61 -4
- package/lib/evaluate.d.ts +9 -0
- package/lib/evaluate.js +264 -1
- package/lib/events.d.ts +69 -2
- package/lib/events.js +123 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +320 -11
- package/lib/network-lifecycle.d.ts +107 -0
- package/lib/network-lifecycle.js +180 -0
- package/lib/network.d.ts +109 -0
- package/lib/network.js +196 -0
- package/lib/parsers/git.d.ts +43 -0
- package/lib/parsers/git.js +404 -0
- package/lib/parsers/shell-cmds.d.ts +32 -0
- package/lib/parsers/shell-cmds.js +293 -0
- package/lib/preset.d.ts +13 -0
- package/lib/preset.js +17 -0
- package/lib/proxy.d.ts +160 -0
- package/lib/proxy.js +529 -0
- package/lib/risk.d.ts +17 -3
- package/lib/risk.js +19 -6
- package/lib/rule-chain.d.ts +39 -0
- package/lib/rule-chain.js +213 -0
- package/lib/rule-dims.d.ts +174 -0
- package/lib/rule-dims.js +230 -0
- package/lib/rule.d.ts +60 -0
- package/lib/rule.js +69 -7
- package/lib/rules-view.d.ts +52 -0
- package/lib/rules-view.js +126 -0
- package/lib/runtime.d.ts +208 -7
- package/lib/runtime.js +433 -27
- package/lib/session-sweep.d.ts +45 -0
- package/lib/session-sweep.js +153 -0
- package/lib/shadow.d.ts +34 -0
- package/lib/shadow.js +143 -0
- package/lib/shell.d.ts +14 -0
- package/lib/shell.js +17 -0
- package/lib/watch.d.ts +42 -0
- package/lib/watch.js +170 -0
- package/package.json +15 -2
- package/scripts/patch-permission-glyph.mjs +209 -0
package/lib/runtime.d.ts
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
|
-
import { type AuditEntry } from './audit.js';
|
|
2
|
-
import { type PermGateConfig, type PermissiveStrategies } from './config.js';
|
|
1
|
+
import { type AuditEntry, type DecisionSource } from './audit.js';
|
|
2
|
+
import { type PermGateConfig, type PermissiveStrategies, type RulesConfig } from './config.js';
|
|
3
3
|
import type { ClassifierConfig } from './classifier.js';
|
|
4
4
|
import { EventLog } from './events.js';
|
|
5
5
|
import { RiskLearning } from './learning.js';
|
|
6
6
|
import { type RiskRequest, type RiskVerdict } from './risk.js';
|
|
7
7
|
import { type HostLlmLike, type HostModelSelection } from './host-llm.js';
|
|
8
|
-
import { type CompiledRuleset } from './rule.js';
|
|
8
|
+
import { type CompiledRuleEntry, type CompiledRuleset, type RuleAction } from './rule.js';
|
|
9
|
+
import type { NetworkTarget } from './network.js';
|
|
9
10
|
import { type SessionEventLike } from './preset.js';
|
|
10
11
|
export interface PreToolDecisionLike {
|
|
11
12
|
kind: 'deny' | 'ask';
|
|
12
13
|
reason: string;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* The P2 rule chain's own verdict for one call, as reported by
|
|
17
|
+
* {@link PermGateRuntime.explainRules}. Distinct from the gate's effective
|
|
18
|
+
* verdict: a P0 hard-deny or a preset deny-keyword fires *before* this layer and
|
|
19
|
+
* never leaves a rule index behind, so a differing pair is information rather
|
|
20
|
+
* than a contradiction.
|
|
21
|
+
*/
|
|
22
|
+
export interface RuleExplanation {
|
|
23
|
+
readonly action: RuleAction;
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
readonly ruleIndex: number | undefined;
|
|
26
|
+
readonly rule: CompiledRuleEntry | undefined;
|
|
27
|
+
readonly defaultAction: RuleAction;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The pure policy outcome for one call, as reported by
|
|
31
|
+
* {@link PermGateRuntime.explainCall}. Same chain as the host-facing decision,
|
|
32
|
+
* minus every side effect and minus the session-state layers.
|
|
33
|
+
*/
|
|
34
|
+
export interface CallExplanation {
|
|
35
|
+
readonly action: RuleAction;
|
|
36
|
+
readonly reason: string;
|
|
37
|
+
readonly source: DecisionSource | 'deny-keyword' | 'cleanup-safe';
|
|
38
|
+
readonly ruleIndex?: number;
|
|
39
|
+
}
|
|
14
40
|
export interface ToolExecutionLike {
|
|
15
41
|
readonly name: string;
|
|
16
42
|
readonly arguments: Record<string, unknown>;
|
|
@@ -73,6 +99,8 @@ interface PendingAsk {
|
|
|
73
99
|
readonly learningKey?: string;
|
|
74
100
|
}
|
|
75
101
|
export interface PermGateRuntimeOptions extends PermGateConfig {
|
|
102
|
+
/** Workspace root for chain resolution. Defaults to process.cwd(). */
|
|
103
|
+
readonly cwd?: string;
|
|
76
104
|
readonly onRulesChanged?: (ruleset: CompiledRuleset) => void;
|
|
77
105
|
readonly now?: () => number;
|
|
78
106
|
/**
|
|
@@ -132,6 +160,42 @@ export interface PermGateRuntimeOptions extends PermGateConfig {
|
|
|
132
160
|
* review page's diff/revert data plane (events are still recorded).
|
|
133
161
|
*/
|
|
134
162
|
readonly snapshotsDir?: string;
|
|
163
|
+
/**
|
|
164
|
+
* Raise one interactive approval request on behalf of a subprocess network
|
|
165
|
+
* connection. Wired by `apply` to the DSH `approval` service
|
|
166
|
+
* (`approval.request`), which routes the prompt to the agent, appends the
|
|
167
|
+
* `approval/asked` + `approval/decided` audit pair, and applies the session
|
|
168
|
+
* policy before any answerer runs.
|
|
169
|
+
*
|
|
170
|
+
* Absent (or throwing) means the connection cannot ride the seam and the
|
|
171
|
+
* gate fails closed to `deny`. Only `'allowed-once'` is treated as an allow.
|
|
172
|
+
*/
|
|
173
|
+
readonly requestApproval?: (req: NetworkApprovalRequest) => Promise<ApprovalOutcomeLike>;
|
|
174
|
+
/**
|
|
175
|
+
* How long one approved network target stays approved for the session (ms).
|
|
176
|
+
* A single shell command routinely opens many connections to the same host;
|
|
177
|
+
* without this the human would be prompted once per connection.
|
|
178
|
+
* Default 30 minutes.
|
|
179
|
+
*/
|
|
180
|
+
readonly networkGrantTtlMs?: number;
|
|
181
|
+
/**
|
|
182
|
+
* Live settings-sourced rules document (the `dsh-perm-gate-rules` namespace),
|
|
183
|
+
* read on every (re)compile. When it carries a real configuration — entries or
|
|
184
|
+
* a non-default `defaultAction` — it takes precedence over `rulesFile` and the
|
|
185
|
+
* rule chain (settings first, file fallback); `undefined` means "namespace
|
|
186
|
+
* still at bare defaults", and the file paths run unchanged.
|
|
187
|
+
*/
|
|
188
|
+
readonly readRulesDocument?: () => RulesConfig | undefined;
|
|
189
|
+
/**
|
|
190
|
+
* Settings-backed allowlist writer. When wired (the settings service is
|
|
191
|
+
* available), {@link approveAllowEverywhere} / {@link setAllowlist} go through
|
|
192
|
+
* it instead of rewriting the rules file; a falsy verdict (no scope, rejected
|
|
193
|
+
* write) falls back to the file path.
|
|
194
|
+
*/
|
|
195
|
+
readonly allowlistWriter?: {
|
|
196
|
+
readonly append: (pattern: string, reason: string) => boolean;
|
|
197
|
+
readonly replace: (patterns: readonly string[], reason: string) => boolean;
|
|
198
|
+
};
|
|
135
199
|
}
|
|
136
200
|
export type CallDecision = 'allow' | 'deny' | 'ask';
|
|
137
201
|
/** The effective Permissive tier state read per tool call. */
|
|
@@ -152,6 +216,30 @@ export interface ApprovalRequestLike {
|
|
|
152
216
|
}
|
|
153
217
|
/** A closed approval outcome, structurally the DSH seam's `ApprovalOutcome`. */
|
|
154
218
|
export type ApprovalOutcomeLike = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
219
|
+
/**
|
|
220
|
+
* One approval ask the gate raises on behalf of a subprocess network
|
|
221
|
+
* connection. Structurally the subset of the DSH `ApprovalRequest` the gate
|
|
222
|
+
* supplies — the host service adds the borrowed session/log handling.
|
|
223
|
+
*/
|
|
224
|
+
export interface NetworkApprovalRequest {
|
|
225
|
+
/** The host agent the question is asked on behalf of (routes the prompt). */
|
|
226
|
+
readonly agent: unknown;
|
|
227
|
+
/** The tool whose subprocess opened the connection. */
|
|
228
|
+
readonly toolName: string;
|
|
229
|
+
/** The exact tool call, when the host gave one — lets the UI attach the prompt. */
|
|
230
|
+
readonly callId?: string;
|
|
231
|
+
readonly reason: string;
|
|
232
|
+
readonly signal?: AbortSignal;
|
|
233
|
+
}
|
|
234
|
+
/** The attribution the proxy layer receives for one connection. */
|
|
235
|
+
export interface ShellAttribution {
|
|
236
|
+
readonly tool: string;
|
|
237
|
+
readonly callId?: string;
|
|
238
|
+
/** The host agent, needed to route an approval request. */
|
|
239
|
+
readonly agent?: unknown;
|
|
240
|
+
readonly sessionId: string;
|
|
241
|
+
readonly signal?: AbortSignal;
|
|
242
|
+
}
|
|
155
243
|
export declare class PermGateRuntime {
|
|
156
244
|
private readonly options;
|
|
157
245
|
private ruleset;
|
|
@@ -165,6 +253,14 @@ export declare class PermGateRuntime {
|
|
|
165
253
|
private readonly gatePresets;
|
|
166
254
|
/** Extra tool names the user classified as safe (config `autoAllowTools`). */
|
|
167
255
|
private readonly autoAllowExtra;
|
|
256
|
+
/** In-flight shell executions, for proxy-layer network attribution. */
|
|
257
|
+
private readonly inFlightShells;
|
|
258
|
+
/** Stable key per execution object (the host call id is not always present). */
|
|
259
|
+
private readonly shellKeys;
|
|
260
|
+
private shellSeq;
|
|
261
|
+
/** Session-scoped network grants: grantKey -> expiry epoch ms. */
|
|
262
|
+
private readonly networkGrants;
|
|
263
|
+
private readonly networkGrantTtlMs;
|
|
168
264
|
/**
|
|
169
265
|
* Cache of the permission-preset fold. The key is the log's length plus the
|
|
170
266
|
* identity of its LAST event: a session's log only appends, so "same length
|
|
@@ -174,6 +270,12 @@ export declare class PermGateRuntime {
|
|
|
174
270
|
* frozen events, which would otherwise re-fold on every tool call.
|
|
175
271
|
*/
|
|
176
272
|
private presetCache;
|
|
273
|
+
/**
|
|
274
|
+
* Last stand-down announced per session (sessionId → the out-of-scope preset
|
|
275
|
+
* name, `''` when the session records none). Bounded by the session count and
|
|
276
|
+
* only read on the stand-down path — the in-scope path never touches it.
|
|
277
|
+
*/
|
|
278
|
+
private readonly standDownAnnounced;
|
|
177
279
|
private readonly learning;
|
|
178
280
|
private readonly events?;
|
|
179
281
|
/** Learning candidates awaiting human approval + execution (call fingerprint → candidate). */
|
|
@@ -213,6 +315,40 @@ export declare class PermGateRuntime {
|
|
|
213
315
|
/** Deep read-only view of the learning state (diagnostics/tests). */
|
|
214
316
|
learningSnapshot(): ReturnType<RiskLearning['snapshot']>;
|
|
215
317
|
private ctxFor;
|
|
318
|
+
/**
|
|
319
|
+
* Register a shell execution as in-flight so a proxy-layer block made by its
|
|
320
|
+
* child process can be attributed back to the session — and therefore ride
|
|
321
|
+
* the interactive approval seam. Called when the gate lets a shell call run;
|
|
322
|
+
* {@link endShellExecution} removes it.
|
|
323
|
+
*/
|
|
324
|
+
beginShellExecution(exec: ToolExecutionLike): void;
|
|
325
|
+
/** Remove a shell execution from the in-flight table (its tool call settled). */
|
|
326
|
+
endShellExecution(exec: ToolExecutionLike): void;
|
|
327
|
+
/** Drop every in-flight entry (dispose / session teardown). */
|
|
328
|
+
clearShellExecutions(): void;
|
|
329
|
+
/**
|
|
330
|
+
* The newest live in-flight shell execution, for proxy-layer attribution.
|
|
331
|
+
* Stale entries (a shell that never reported a result) are expired here so a
|
|
332
|
+
* long-dead call can never authorize a later connection.
|
|
333
|
+
*/
|
|
334
|
+
currentAttribution(): ShellAttribution | undefined;
|
|
335
|
+
/** A stable per-execution key: the host call id, else an object-keyed fallback. */
|
|
336
|
+
private shellKeyOf;
|
|
337
|
+
/**
|
|
338
|
+
* Ask the human to allow one network target, on behalf of the attributed
|
|
339
|
+
* shell execution. Returns `'allow'` only for an explicit `allowed-once`;
|
|
340
|
+
* every other outcome (rejection, cancellation, timeout, no answerer)
|
|
341
|
+
* fails closed to `'deny'`.
|
|
342
|
+
*
|
|
343
|
+
* The approval seam requires an open turn — the durable audit pair must be
|
|
344
|
+
* enclosed by the log's commit boundary — which holds here because the ask
|
|
345
|
+
* happens *while* the shell tool call is executing.
|
|
346
|
+
*/
|
|
347
|
+
askNetwork(target: NetworkTarget, reason: string, timeoutMs: number): Promise<'allow' | 'deny'>;
|
|
348
|
+
/** Record one network escalation outcome in the audit mirror + event feed. */
|
|
349
|
+
private auditNetworkAsk;
|
|
350
|
+
/** The compiled ruleset (read-only view for network module). */
|
|
351
|
+
get compiledRuleset(): CompiledRuleset;
|
|
216
352
|
private liveRiskLearning;
|
|
217
353
|
private liveRiskSediment;
|
|
218
354
|
private liveClassifySource;
|
|
@@ -238,8 +374,26 @@ export declare class PermGateRuntime {
|
|
|
238
374
|
* `danger-full-access` means "full access without approval prompts", where a
|
|
239
375
|
* forwarded ask can only fail (the approval seam rejects before any answerer
|
|
240
376
|
* runs) and a hard-deny would silently overrule the tier the user chose.
|
|
377
|
+
*
|
|
378
|
+
* P0 is therefore monotonic only WITHIN the gate's scope, not across every
|
|
379
|
+
* preset. {@link announceStandDown} makes that visible instead of silent.
|
|
241
380
|
*/
|
|
242
381
|
private gateActive;
|
|
382
|
+
/**
|
|
383
|
+
* Record one `stand-down` event the first time a session is observed with a
|
|
384
|
+
* preset outside {@link gatePresets}, and again whenever that preset changes.
|
|
385
|
+
*
|
|
386
|
+
* Per-call silence is what makes a stand-down dangerous: the tool call looks
|
|
387
|
+
* exactly like a call the gate inspected and allowed. One event per
|
|
388
|
+
* transition is enough to say otherwise, and keeps the feed (and the
|
|
389
|
+
* `events.jsonl` append) proportional to preset changes rather than to call
|
|
390
|
+
* volume.
|
|
391
|
+
*
|
|
392
|
+
* The event is a NOTICE, not a decision: it carries no allow/deny meaning,
|
|
393
|
+
* and the call it was observed on is still settled entirely by the selected
|
|
394
|
+
* tier. `verdict: 'stand-down'` labels it for the history view.
|
|
395
|
+
*/
|
|
396
|
+
private announceStandDown;
|
|
243
397
|
/**
|
|
244
398
|
* Whether an `ask` produced while the gate is active can actually reach a
|
|
245
399
|
* human. Under `approval: never` the DSH approval seam returns `rejected`
|
|
@@ -318,6 +472,45 @@ export declare class PermGateRuntime {
|
|
|
318
472
|
* the human seam.
|
|
319
473
|
*/
|
|
320
474
|
decideExecution(exec: ToolExecutionLike): PreToolDecisionLike | undefined;
|
|
475
|
+
/**
|
|
476
|
+
* The **pure policy** outcome for one call: the same chain
|
|
477
|
+
* {@link decideExecution} runs, with every side effect and every session-state
|
|
478
|
+
* layer removed.
|
|
479
|
+
*
|
|
480
|
+
* Side effects removed — no audit entry, no event, no ask tracking, no call
|
|
481
|
+
* clearance, no learning. A rule-test panel must be able to run on every
|
|
482
|
+
* keystroke without writing to the live decision feed.
|
|
483
|
+
*
|
|
484
|
+
* Session-state layers removed — the preset stand-down and the
|
|
485
|
+
* `approval: never` ask degradation both describe a session a session-less
|
|
486
|
+
* caller does not have. Including them is not harmless: the degradation turns
|
|
487
|
+
* every "the rules would ask a human" into a reported **allow**, which is the
|
|
488
|
+
* opposite of the truth for exactly the rules someone opens the panel to
|
|
489
|
+
* review (measured on the live host: `shell ls -la` reported `allow` while the
|
|
490
|
+
* rule layer said `ask`).
|
|
491
|
+
*
|
|
492
|
+
* `reason` is never collapsed: "allow" has causes worth naming (a rule, a
|
|
493
|
+
* grant, cleanup-safety, the permissive default).
|
|
494
|
+
*/
|
|
495
|
+
explainCall(exec: ToolExecutionLike): CallExplanation;
|
|
496
|
+
/**
|
|
497
|
+
* P0 hard-deny → P1 session grant → P2 rules → P4 ask, as a value. Shared by
|
|
498
|
+
* the host-facing decision and {@link explainCall} so the two cannot drift.
|
|
499
|
+
*/
|
|
500
|
+
private rawPolicy;
|
|
501
|
+
/**
|
|
502
|
+
* Rule-layer explanation for one would-be call: what the `permissions` chain
|
|
503
|
+
* decides on its own, before P0 hard-deny, P1 session grants, the P3
|
|
504
|
+
* classifier and the P4 ask ever get a say.
|
|
505
|
+
*
|
|
506
|
+
* This is the read-only half of a dry-run. It appends nothing to the audit
|
|
507
|
+
* mirror, records no event, mints no grant and touches no learning store, so a
|
|
508
|
+
* "rule test" panel may call it on every keystroke without leaving a trace in
|
|
509
|
+
* the decision feed. It answers a narrower question than
|
|
510
|
+
* {@link decideExecution} and says so: `ruleIndex` identifies a rule in the
|
|
511
|
+
* chain, which only this layer can attribute.
|
|
512
|
+
*/
|
|
513
|
+
explainRules(exec: ToolExecutionLike): RuleExplanation;
|
|
321
514
|
/**
|
|
322
515
|
* Apply the Permissive tier to a raw decision. Hard-deny and minted-grant
|
|
323
516
|
* outcomes are never touched (fail-closed). `alwaysConfirm` escalates a
|
|
@@ -397,15 +590,23 @@ export declare class PermGateRuntime {
|
|
|
397
590
|
approveRepeat(exec: ToolExecutionLike, maxUses?: number, ttlMs?: number): void;
|
|
398
591
|
/**
|
|
399
592
|
* Grant "allow every occurrence of this command" (the always-confirm panel's
|
|
400
|
-
* second extended allow button): persist the command into the rules
|
|
401
|
-
*
|
|
593
|
+
* second extended allow button): persist the command into the rules' `allow`
|
|
594
|
+
* whitelist and reload. Prefers the settings-backed writer (the namespace's
|
|
595
|
+
* watch triggers the reload once the write lands); falls back to the rules
|
|
596
|
+
* file when no settings scope is wired. Returns the write verdict.
|
|
402
597
|
*/
|
|
403
598
|
approveAllowEverywhere(commandWord: string, reason?: string): boolean;
|
|
404
|
-
/**
|
|
599
|
+
/**
|
|
600
|
+
* Read-only view of the current allow-list command patterns (whitelist).
|
|
601
|
+
* Reads the settings document when the namespace is configured, else the
|
|
602
|
+
* rules file.
|
|
603
|
+
*/
|
|
405
604
|
allowlist(): readonly string[];
|
|
406
605
|
/**
|
|
407
606
|
* Replace the whitelist with exactly the given command patterns and reload.
|
|
408
|
-
*
|
|
607
|
+
* Prefers the settings-backed writer; falls back to the rules file when no
|
|
608
|
+
* settings scope is wired. Returns the write verdict (false when neither
|
|
609
|
+
* sink is available or the write failed).
|
|
409
610
|
*/
|
|
410
611
|
setAllowlist(patterns: readonly string[]): boolean;
|
|
411
612
|
}
|