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.
Files changed (77) hide show
  1. package/CHANGELOG.ja.md +82 -0
  2. package/CHANGELOG.ko.md +80 -0
  3. package/CHANGELOG.md +321 -0
  4. package/INSTALL.ja.md +70 -8
  5. package/INSTALL.ko.md +68 -9
  6. package/INSTALL.md +68 -8
  7. package/INSTALL.zh.md +59 -7
  8. package/README.ja.md +103 -16
  9. package/README.ko.md +101 -17
  10. package/README.md +152 -25
  11. package/README.zh.md +99 -12
  12. package/cordis.patch.yml +34 -3
  13. package/docs/baseline-guard.md +56 -0
  14. package/docs/plan-rules-to-settings.md +217 -0
  15. package/docs/rules-format.en.md +294 -0
  16. package/docs/rules-format.md +279 -0
  17. package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
  18. package/dsh.plugin.json +22 -0
  19. package/examples/permissions.example.yaml +16 -1
  20. package/lib/agent-identity.d.ts +43 -0
  21. package/lib/agent-identity.js +95 -0
  22. package/lib/allowlist.d.ts +27 -0
  23. package/lib/allowlist.js +103 -1
  24. package/lib/cli.js +11 -14
  25. package/lib/client.js +598 -6
  26. package/lib/client.js.map +1 -1
  27. package/lib/command-dispatcher.d.ts +22 -0
  28. package/lib/command-dispatcher.js +97 -0
  29. package/lib/command-semantics.d.ts +62 -0
  30. package/lib/command-semantics.js +6 -0
  31. package/lib/compiler.d.ts +50 -0
  32. package/lib/compiler.js +122 -0
  33. package/lib/config.d.ts +161 -7
  34. package/lib/config.js +78 -11
  35. package/lib/dry-run.d.ts +87 -0
  36. package/lib/dry-run.js +110 -0
  37. package/lib/engine.js +61 -4
  38. package/lib/evaluate.d.ts +9 -0
  39. package/lib/evaluate.js +264 -1
  40. package/lib/events.d.ts +69 -2
  41. package/lib/events.js +123 -0
  42. package/lib/index.d.ts +1 -1
  43. package/lib/index.js +320 -11
  44. package/lib/network-lifecycle.d.ts +107 -0
  45. package/lib/network-lifecycle.js +180 -0
  46. package/lib/network.d.ts +109 -0
  47. package/lib/network.js +196 -0
  48. package/lib/parsers/git.d.ts +43 -0
  49. package/lib/parsers/git.js +404 -0
  50. package/lib/parsers/shell-cmds.d.ts +32 -0
  51. package/lib/parsers/shell-cmds.js +293 -0
  52. package/lib/preset.d.ts +13 -0
  53. package/lib/preset.js +17 -0
  54. package/lib/proxy.d.ts +160 -0
  55. package/lib/proxy.js +529 -0
  56. package/lib/risk.d.ts +17 -3
  57. package/lib/risk.js +19 -6
  58. package/lib/rule-chain.d.ts +39 -0
  59. package/lib/rule-chain.js +213 -0
  60. package/lib/rule-dims.d.ts +174 -0
  61. package/lib/rule-dims.js +230 -0
  62. package/lib/rule.d.ts +60 -0
  63. package/lib/rule.js +69 -7
  64. package/lib/rules-view.d.ts +52 -0
  65. package/lib/rules-view.js +126 -0
  66. package/lib/runtime.d.ts +208 -7
  67. package/lib/runtime.js +433 -27
  68. package/lib/session-sweep.d.ts +45 -0
  69. package/lib/session-sweep.js +153 -0
  70. package/lib/shadow.d.ts +34 -0
  71. package/lib/shadow.js +143 -0
  72. package/lib/shell.d.ts +14 -0
  73. package/lib/shell.js +17 -0
  74. package/lib/watch.d.ts +42 -0
  75. package/lib/watch.js +170 -0
  76. package/package.json +15 -2
  77. 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 file's
401
- * `allow` whitelist and reload. Returns the reload result.
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
- /** Read-only view of the current allow-list command patterns (whitelist). */
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
- * Returns the reload result (false when no rulesFile or the write failed).
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
  }