dsh-perm-gate 5.1.2 → 5.3.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 (47) hide show
  1. package/CHANGELOG.ja.md +54 -1
  2. package/CHANGELOG.ko.md +54 -1
  3. package/CHANGELOG.md +134 -4
  4. package/README.ja.md +43 -0
  5. package/README.ko.md +43 -1
  6. package/README.md +73 -0
  7. package/README.zh.md +41 -0
  8. package/THIRD_PARTY_NOTICES.md +44 -0
  9. package/cordis.patch.yml +16 -1
  10. package/dsh.plugin.json +1 -1
  11. package/lib/ask-context.d.ts +49 -1
  12. package/lib/ask-context.js +89 -5
  13. package/lib/client.js +714 -86
  14. package/lib/client.js.map +1 -1
  15. package/lib/config.d.ts +243 -83
  16. package/lib/config.js +84 -41
  17. package/lib/credential-paths.d.ts +35 -0
  18. package/lib/credential-paths.js +119 -0
  19. package/lib/engine.d.ts +4 -6
  20. package/lib/engine.js +53 -42
  21. package/lib/evaluate.d.ts +20 -0
  22. package/lib/evaluate.js +41 -13
  23. package/lib/feedback.d.ts +42 -0
  24. package/lib/feedback.js +85 -0
  25. package/lib/index.d.ts +2 -2
  26. package/lib/index.js +143 -17
  27. package/lib/irreversible.js +109 -1
  28. package/lib/path.d.ts +31 -0
  29. package/lib/path.js +35 -0
  30. package/lib/preset-rules.d.ts +70 -0
  31. package/lib/preset-rules.js +388 -0
  32. package/lib/preset.d.ts +8 -5
  33. package/lib/preset.js +15 -6
  34. package/lib/risk.d.ts +29 -1
  35. package/lib/risk.js +36 -2
  36. package/lib/runtime.d.ts +164 -0
  37. package/lib/runtime.js +429 -55
  38. package/lib/scan-text.d.ts +69 -0
  39. package/lib/scan-text.js +162 -0
  40. package/lib/script-review.d.ts +50 -0
  41. package/lib/script-review.js +202 -0
  42. package/lib/shell.d.ts +18 -0
  43. package/lib/shell.js +103 -1
  44. package/lib/unattended.d.ts +61 -0
  45. package/lib/unattended.js +77 -0
  46. package/package.json +4 -3
  47. package/scripts/patch-permission-glyph.mjs +27 -13
package/lib/config.d.ts CHANGED
@@ -66,6 +66,75 @@ export interface PermGateConfig {
66
66
  * widens decision before the human/LLM seam.
67
67
  */
68
68
  readonly permissive?: boolean;
69
+ /**
70
+ * YOLO — the switch behind the `permissive-yolo` tier, the model-backed human-free
71
+ * implementation. It never raises a human confirmation: what the mechanical screen names
72
+ * (irreversible, unlearnable, unreadable payload) is blocked and fed back to the main LLM
73
+ * as a `工具审核意见` user message, and the residual goes to the review model, which
74
+ * answers in the person's place — risky blocks, anything else runs.
75
+ *
76
+ * This is a per-tier switch, not a session-wide flow: it governs the `permissive-yolo`
77
+ * preset and nothing else, so turning it off changes no other tier's behaviour. The tier
78
+ * is the model-backed one by definition; with no reviewer in seat it still refuses to ask
79
+ * a person (that is the promise), settles on the mechanical screen alone, and records that
80
+ * the model never answered instead of letting the tier quietly become a different one.
81
+ */
82
+ readonly yolo?: boolean;
83
+ /**
84
+ * `safenet-pass` — the switch behind the `permissive-mech` tier: the deterministic
85
+ * mechanical screen plus the preset opinions take over the ask. Named danger is blocked
86
+ * and fed back to the main LLM, everything the nets cannot condemn runs. No human prompt
87
+ * and **no model call at all** — the reviewed form of `approval: never`, and the tier for
88
+ * a run with no reviewer LLM or where the tokens are not worth spending.
89
+ */
90
+ readonly safenetPass?: boolean;
91
+ /**
92
+ * `safenet-ask` — the switch behind the `permissive-snet` tier: the mechanical screen
93
+ * still filters and lists its doubts, but the ask stays with a **person** and no grader is
94
+ * consulted. The shape the gate takes when no reviewer LLM is connected and you would
95
+ * rather answer than auto-run. Uncommon by design.
96
+ */
97
+ readonly safenetAsk?: boolean;
98
+ /**
99
+ * Degraded review — fall back to a **rule-only gate when there is no reviewer**.
100
+ *
101
+ * With `permissiveStrategies.llmAssist` on, an `ask` is normally graded by the
102
+ * configured model, and every path that cannot grade it (no endpoint/model set up,
103
+ * host `llm` absent, transport failure → `unresolved`) is fail-closed to a *human*
104
+ * confirmation. In a headless or unattended run that is a stall: nobody answers.
105
+ * This switch degrades those two no-reviewer paths to the deterministic nets only —
106
+ * a cc-safety-net-class gate: danger the rules can name (irreversible / unlearnable /
107
+ * opaque payload) is **blocked** and the reason is fed back to the main LLM, anything
108
+ * else runs. It changes nothing while a reviewer *is* answering, and never softens a
109
+ * deterministic deny. It applies to the ordinary 自动审查 tiers (`permissive`,
110
+ * `permissive-full`); the three tiers that own an implementation (`yolo`, `safenetPass`,
111
+ * `safenetAsk`) already say who answers and are not degraded by this switch.
112
+ */
113
+ readonly rulesFallback?: boolean;
114
+ /**
115
+ * **Retired** (2026-10-17) — the per-preset mode table (`<preset id>=<mode>`) that briefly
116
+ * decided who answers an ask. Each human-free implementation now owns exactly one preset
117
+ * and exactly one switch (`yolo` → `permissive-yolo`, `safenetPass` → `permissive-mech`,
118
+ * `safenetAsk` → `permissive-snet`), which is what the table could not guarantee: entries
119
+ * could name a preset outside the gate's scope, and a stored list could claim to steer
120
+ * presets it never actually reached. The key stays in the schema **only** so a
121
+ * profile written by those builds still parses; nothing reads it, and the settings card no
122
+ * longer writes it. The replacement semantics live in `src/unattended.ts`.
123
+ */
124
+ readonly unattendedTiers?: string[];
125
+ /**
126
+ * Script review (default **on**) — when a call runs a script file (`bash x.sh`,
127
+ * `pwsh -File x.ps1`, `python x.py`, `./deploy.sh`), read the file and attach its
128
+ * body to the review: the risk grader and the human both judge the code that will
129
+ * execute instead of a filename, and the deterministic nets grade the body too (a
130
+ * script that wipes `$HOME` is irreversible whatever the command line says).
131
+ *
132
+ * Bounded (a few files, a fixed per-file budget) and redacted on the way out like
133
+ * every other egress text. Turn it off to keep file contents off the configured
134
+ * endpoint: the cost is that out-of-workspace and unreadable script runs go back to
135
+ * being graded as unknowable (the `U` tier).
136
+ */
137
+ readonly scriptReview?: boolean;
69
138
  /** Backend approval strategies; combinable. Front-end exposes only `permissive` itself. */
70
139
  readonly permissiveStrategies?: Partial<PermissiveStrategies>;
71
140
  /**
@@ -95,6 +164,57 @@ export interface PermGateConfig {
95
164
  * (possibly empty) replaces it. Editable from the settings card as a list.
96
165
  */
97
166
  readonly denyKeywords?: string[];
167
+ /**
168
+ * Keywords **appended** to whichever blacklist is live (`denyKeywords` when set, the
169
+ * preset otherwise) instead of replacing it. This is what to use to protect a
170
+ * long-tail secret of your own — a company token prefix, a vault CLI subcommand —
171
+ * without having to copy fifty preset entries into your profile and keep them current.
172
+ */
173
+ readonly denyKeywordsExtra?: string[];
174
+ /**
175
+ * Credential paths beyond the built-in store list (`.ssh`, `.aws`, `.npmrc`,
176
+ * `credentials.yaml`, …), which P0 refuses to read from outside the workspace and
177
+ * refuses to mutate through a file tool.
178
+ *
179
+ * Entries are path globs matched on the normalized path: `**` crosses directories, `*`
180
+ * and `?` stay inside one; a plain entry is a path fragment (`vault/prod.env`,
181
+ * `.myco_token`), and a `~/` prefix expands to the real home. Relative patterns also
182
+ * match at any depth, so `secrets/*.env` covers `a/b/secrets/prod.env`.
183
+ *
184
+ * Why a setting and not a rule in `rules.yml`: a `read`/`grep` call is auto-allowed a
185
+ * stage before the rule chain runs (the only layer that sees it first is P0), and the
186
+ * tier of an entry must not be the author's choice — a declared secret gets what
187
+ * `.ssh` gets, never a confirmation a model can talk its way out of.
188
+ */
189
+ readonly credentialPaths?: string[];
190
+ /**
191
+ * Argument keys to treat as **document bodies** in addition to the built-in list
192
+ * (`content`, `text`, `plan`…): their values never enter the flattened text the
193
+ * deny-keyword blacklist and the P0 credential pre-screen read.
194
+ *
195
+ * The point is the long tail of host tools whose prose keys nobody shipped a rule for
196
+ * (`steps`, `notes`, `rationale`) — the shape of GitHub issue #2, where an unlisted
197
+ * document key made a plan get refused for describing `rm -rf`. Additive only: the
198
+ * built-in list cannot be replaced, because an omission would re-create that bug.
199
+ */
200
+ readonly extraContentArgKeys?: string[];
201
+ /**
202
+ * Argument keys to treat as **operations** even when the tool class says otherwise: the
203
+ * field's text is read by the blacklist and the credential pre-screen, and this list
204
+ * wins over `extraContentArgKeys` when both name the same key.
205
+ *
206
+ * For a workflow/skill-style tool whose body *is* executed code. Cost, stated: naming a
207
+ * genuine document key here re-exposes the issue #2 failure — a plan or note that merely
208
+ * mentions a destructive command will be refused for mentioning it.
209
+ */
210
+ readonly extraOperationArgKeys?: string[];
211
+ /**
212
+ * One-shot marker for the preset deny-table seed (`preset-rules.ts`, ported
213
+ * from cc-safety-net): set once the `[deny-rule:*]` entries have been merged
214
+ * into the live rules source. A full revocation (deleting every preset entry)
215
+ * persists — the seed never re-adds them.
216
+ */
217
+ readonly presetRulesSeeded?: boolean;
98
218
  /**
99
219
  * Extra tool names classified as read-only/internal and therefore auto-allowed.
100
220
  *
@@ -332,126 +452,166 @@ export declare function readRulesFromSettings(scope: {
332
452
  export declare const Config: z<Schemastery.ObjectS<NoInfer<{
333
453
  rulesFile: z<string, string, "plain">;
334
454
  dshHome: z<string, string, "plain">;
335
- defaultAction: z<"allow" | "ask" | "deny", "allow" | "ask" | "deny", "volatile-defined">;
336
- caseInsensitivePaths: z<boolean, boolean, "volatile-defined">;
337
- classifierEnabled: z<boolean, boolean, "volatile-defined">;
338
- classifierEndpoint: z<string, string, "volatile">;
339
- classifierModel: z<string, string, "volatile-defined">;
340
- classifierApiKey: z<string, string, "volatile">;
341
- riskTimeoutMs: z<number, number, "volatile-defined">;
342
- riskLearning: z<boolean, boolean, "volatile-defined">;
343
- riskSediment: z<boolean, boolean, "volatile-defined">;
344
- classifierSource: z<"host" | "custom", "host" | "custom", "volatile-defined">;
345
- classifierProvider: z<string, string, "volatile">;
346
- riskThreshold: z<number, number, "volatile-defined">;
455
+ defaultAction: z<"allow" | "ask" | "deny", "allow" | "ask" | "deny", "defined">;
456
+ caseInsensitivePaths: z<boolean, boolean, "defined">;
457
+ classifierEnabled: z<boolean, boolean, "defined">;
458
+ classifierEndpoint: z<string, string, "plain">;
459
+ classifierModel: z<string, string, "defined">;
460
+ classifierApiKey: z<string, string, "plain">;
461
+ riskTimeoutMs: z<number, number, "defined">;
462
+ riskLearning: z<boolean, boolean, "defined">;
463
+ riskSediment: z<boolean, boolean, "defined">;
464
+ classifierSource: z<"host" | "custom", "host" | "custom", "defined">;
465
+ classifierProvider: z<string, string, "plain">;
466
+ riskThreshold: z<number, number, "defined">;
347
467
  learningFile: z<string, string, "plain">;
348
468
  eventsFile: z<string, string, "plain">;
349
- grantTtlMs: z<number, number, "volatile-defined">;
350
- grantMaxUses: z<number, number, "volatile-defined">;
351
- permissive: z<boolean, boolean, "volatile-defined">;
469
+ grantTtlMs: z<number, number, "defined">;
470
+ grantMaxUses: z<number, number, "defined">;
471
+ permissive: z<boolean, boolean, "defined">;
472
+ yolo: z<boolean, boolean, "defined">;
473
+ rulesFallback: z<boolean, boolean, "defined">;
474
+ /**
475
+ * Retired (2026-10-17): the per-preset mode table that briefly mapped
476
+ * `<preset id>=<mode>`. Each human-free implementation now owns exactly one preset and
477
+ * exactly one switch (`yolo` / `safenetPass` / `safenetAsk`), so presets never cross and a
478
+ * switch means what it says — whether that implementation exists. The key is kept in the
479
+ * schema only so an older profile that stored it still parses.
480
+ */
481
+ unattendedTiers: z<string[], string[], "defined">;
482
+ /** Switch: the `permissive-mech` implementation exists (mechanical takeover of asks). */
483
+ safenetPass: z<boolean, boolean, "defined">;
484
+ /** Switch: the `permissive-snet` implementation exists (mechanical screen, still asks). */
485
+ safenetAsk: z<boolean, boolean, "defined">;
486
+ scriptReview: z<boolean, boolean, "defined">;
352
487
  gatePresets: z<string[], string[], "plain">;
353
- permissiveStrategies: z<NoInfer<Schemastery.ObjectS<NoInfer<{
488
+ permissiveStrategies: z<Schemastery.ObjectS<NoInfer<{
354
489
  trustAutoAllow: z<boolean, boolean, "defined">;
355
490
  alwaysConfirm: z<boolean, boolean, "defined">;
356
491
  llmAssist: z<boolean, boolean, "defined">;
357
492
  trustEscalation: z<boolean, boolean, "defined">;
358
- }>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
493
+ }>>, Schemastery.ObjectT<NoInfer<{
359
494
  trustAutoAllow: z<boolean, boolean, "defined">;
360
495
  alwaysConfirm: z<boolean, boolean, "defined">;
361
496
  llmAssist: z<boolean, boolean, "defined">;
362
497
  trustEscalation: z<boolean, boolean, "defined">;
363
- }>>>, "volatile">;
364
- allowlist: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
365
- denyKeywords: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
366
- autoAllowTools: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
367
- sessionSweep: z<boolean, boolean, "volatile-defined">;
498
+ }>>, "plain">;
499
+ allowlist: z<string[], string[], "plain">;
500
+ denyKeywords: z<string[], string[], "plain">;
501
+ denyKeywordsExtra: z<string[], string[], "defined">;
502
+ credentialPaths: z<string[], string[], "defined">;
503
+ extraContentArgKeys: z<string[], string[], "defined">;
504
+ extraOperationArgKeys: z<string[], string[], "defined">;
505
+ presetRulesSeeded: z<boolean, boolean, "defined">;
506
+ autoAllowTools: z<string[], string[], "plain">;
507
+ sessionSweep: z<boolean, boolean, "defined">;
368
508
  workspaceStoreFile: z<string, string, "plain">;
369
- searchUp: z<boolean, boolean, "volatile-defined">;
509
+ searchUp: z<boolean, boolean, "defined">;
370
510
  fallbackPath: z<string, string, "plain">;
371
- badFilePolicy: z<"fail" | "warn", "fail" | "warn", "volatile-defined">;
372
- maxChainLength: z<number, number, "volatile-defined">;
373
- networkEnabled: z<boolean, boolean, "volatile-defined">;
374
- networkMode: z<"deny-all" | "whitelist" | "allow-all", "deny-all" | "whitelist" | "allow-all", "volatile-defined">;
375
- networkUnlisted: z<"ask" | "deny", "ask" | "deny", "volatile-defined">;
376
- networkUnattributed: z<"allow" | "deny", "allow" | "deny", "volatile-defined">;
377
- networkLoopback: z<"allow" | "policy", "allow" | "policy", "volatile-defined">;
378
- networkBind: z<string, string, "volatile-defined">;
379
- networkPort: z<number, number, "volatile-defined">;
380
- networkNoProxy: z<"clear" | "preserve", "clear" | "preserve", "volatile-defined">;
381
- networkInjectEnv: z<boolean, boolean, "volatile-defined">;
382
- networkAskTimeoutMs: z<number, number, "volatile-defined">;
383
- networkGrantTtlMs: z<number, number, "volatile-defined">;
384
- sandboxEnabled: z<boolean, boolean, "volatile-defined">;
385
- sandboxMode: z<"read-only" | "workspace-write", "read-only" | "workspace-write", "volatile-defined">;
386
- sandboxProxy: z<string, string, "volatile-defined">;
387
- sandboxLoopback: z<"exempt" | "off", "exempt" | "off", "volatile-defined">;
388
- rules: z<NoInfer<RulesConfig>, NoInfer<RulesConfig>, "volatile">;
511
+ badFilePolicy: z<"fail" | "warn", "fail" | "warn", "defined">;
512
+ maxChainLength: z<number, number, "defined">;
513
+ networkEnabled: z<boolean, boolean, "defined">;
514
+ networkMode: z<"deny-all" | "whitelist" | "allow-all", "deny-all" | "whitelist" | "allow-all", "defined">;
515
+ networkUnlisted: z<"ask" | "deny", "ask" | "deny", "defined">;
516
+ networkUnattributed: z<"allow" | "deny", "allow" | "deny", "defined">;
517
+ networkLoopback: z<"allow" | "policy", "allow" | "policy", "defined">;
518
+ networkBind: z<string, string, "defined">;
519
+ networkPort: z<number, number, "defined">;
520
+ networkNoProxy: z<"clear" | "preserve", "clear" | "preserve", "defined">;
521
+ networkInjectEnv: z<boolean, boolean, "defined">;
522
+ networkAskTimeoutMs: z<number, number, "defined">;
523
+ networkGrantTtlMs: z<number, number, "defined">;
524
+ sandboxEnabled: z<boolean, boolean, "defined">;
525
+ sandboxMode: z<"read-only" | "workspace-write", "read-only" | "workspace-write", "defined">;
526
+ sandboxProxy: z<string, string, "defined">;
527
+ sandboxLoopback: z<"exempt" | "off", "exempt" | "off", "defined">;
528
+ rules: z<RulesConfig>;
389
529
  watch: z<boolean, boolean, "defined">;
390
530
  watchDebounceMs: z<number, number, "defined">;
391
531
  }>>, Schemastery.ObjectT<NoInfer<{
392
532
  rulesFile: z<string, string, "plain">;
393
533
  dshHome: z<string, string, "plain">;
394
- defaultAction: z<"allow" | "ask" | "deny", "allow" | "ask" | "deny", "volatile-defined">;
395
- caseInsensitivePaths: z<boolean, boolean, "volatile-defined">;
396
- classifierEnabled: z<boolean, boolean, "volatile-defined">;
397
- classifierEndpoint: z<string, string, "volatile">;
398
- classifierModel: z<string, string, "volatile-defined">;
399
- classifierApiKey: z<string, string, "volatile">;
400
- riskTimeoutMs: z<number, number, "volatile-defined">;
401
- riskLearning: z<boolean, boolean, "volatile-defined">;
402
- riskSediment: z<boolean, boolean, "volatile-defined">;
403
- classifierSource: z<"host" | "custom", "host" | "custom", "volatile-defined">;
404
- classifierProvider: z<string, string, "volatile">;
405
- riskThreshold: z<number, number, "volatile-defined">;
534
+ defaultAction: z<"allow" | "ask" | "deny", "allow" | "ask" | "deny", "defined">;
535
+ caseInsensitivePaths: z<boolean, boolean, "defined">;
536
+ classifierEnabled: z<boolean, boolean, "defined">;
537
+ classifierEndpoint: z<string, string, "plain">;
538
+ classifierModel: z<string, string, "defined">;
539
+ classifierApiKey: z<string, string, "plain">;
540
+ riskTimeoutMs: z<number, number, "defined">;
541
+ riskLearning: z<boolean, boolean, "defined">;
542
+ riskSediment: z<boolean, boolean, "defined">;
543
+ classifierSource: z<"host" | "custom", "host" | "custom", "defined">;
544
+ classifierProvider: z<string, string, "plain">;
545
+ riskThreshold: z<number, number, "defined">;
406
546
  learningFile: z<string, string, "plain">;
407
547
  eventsFile: z<string, string, "plain">;
408
- grantTtlMs: z<number, number, "volatile-defined">;
409
- grantMaxUses: z<number, number, "volatile-defined">;
410
- permissive: z<boolean, boolean, "volatile-defined">;
548
+ grantTtlMs: z<number, number, "defined">;
549
+ grantMaxUses: z<number, number, "defined">;
550
+ permissive: z<boolean, boolean, "defined">;
551
+ yolo: z<boolean, boolean, "defined">;
552
+ rulesFallback: z<boolean, boolean, "defined">;
553
+ /**
554
+ * Retired (2026-10-17): the per-preset mode table that briefly mapped
555
+ * `<preset id>=<mode>`. Each human-free implementation now owns exactly one preset and
556
+ * exactly one switch (`yolo` / `safenetPass` / `safenetAsk`), so presets never cross and a
557
+ * switch means what it says — whether that implementation exists. The key is kept in the
558
+ * schema only so an older profile that stored it still parses.
559
+ */
560
+ unattendedTiers: z<string[], string[], "defined">;
561
+ /** Switch: the `permissive-mech` implementation exists (mechanical takeover of asks). */
562
+ safenetPass: z<boolean, boolean, "defined">;
563
+ /** Switch: the `permissive-snet` implementation exists (mechanical screen, still asks). */
564
+ safenetAsk: z<boolean, boolean, "defined">;
565
+ scriptReview: z<boolean, boolean, "defined">;
411
566
  gatePresets: z<string[], string[], "plain">;
412
- permissiveStrategies: z<NoInfer<Schemastery.ObjectS<NoInfer<{
567
+ permissiveStrategies: z<Schemastery.ObjectS<NoInfer<{
413
568
  trustAutoAllow: z<boolean, boolean, "defined">;
414
569
  alwaysConfirm: z<boolean, boolean, "defined">;
415
570
  llmAssist: z<boolean, boolean, "defined">;
416
571
  trustEscalation: z<boolean, boolean, "defined">;
417
- }>>>, NoInfer<Schemastery.ObjectT<NoInfer<{
572
+ }>>, Schemastery.ObjectT<NoInfer<{
418
573
  trustAutoAllow: z<boolean, boolean, "defined">;
419
574
  alwaysConfirm: z<boolean, boolean, "defined">;
420
575
  llmAssist: z<boolean, boolean, "defined">;
421
576
  trustEscalation: z<boolean, boolean, "defined">;
422
- }>>>, "volatile">;
423
- allowlist: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
424
- denyKeywords: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
425
- autoAllowTools: z<NoInfer<string[]>, NoInfer<string[]>, "volatile">;
426
- sessionSweep: z<boolean, boolean, "volatile-defined">;
577
+ }>>, "plain">;
578
+ allowlist: z<string[], string[], "plain">;
579
+ denyKeywords: z<string[], string[], "plain">;
580
+ denyKeywordsExtra: z<string[], string[], "defined">;
581
+ credentialPaths: z<string[], string[], "defined">;
582
+ extraContentArgKeys: z<string[], string[], "defined">;
583
+ extraOperationArgKeys: z<string[], string[], "defined">;
584
+ presetRulesSeeded: z<boolean, boolean, "defined">;
585
+ autoAllowTools: z<string[], string[], "plain">;
586
+ sessionSweep: z<boolean, boolean, "defined">;
427
587
  workspaceStoreFile: z<string, string, "plain">;
428
- searchUp: z<boolean, boolean, "volatile-defined">;
588
+ searchUp: z<boolean, boolean, "defined">;
429
589
  fallbackPath: z<string, string, "plain">;
430
- badFilePolicy: z<"fail" | "warn", "fail" | "warn", "volatile-defined">;
431
- maxChainLength: z<number, number, "volatile-defined">;
432
- networkEnabled: z<boolean, boolean, "volatile-defined">;
433
- networkMode: z<"deny-all" | "whitelist" | "allow-all", "deny-all" | "whitelist" | "allow-all", "volatile-defined">;
434
- networkUnlisted: z<"ask" | "deny", "ask" | "deny", "volatile-defined">;
435
- networkUnattributed: z<"allow" | "deny", "allow" | "deny", "volatile-defined">;
436
- networkLoopback: z<"allow" | "policy", "allow" | "policy", "volatile-defined">;
437
- networkBind: z<string, string, "volatile-defined">;
438
- networkPort: z<number, number, "volatile-defined">;
439
- networkNoProxy: z<"clear" | "preserve", "clear" | "preserve", "volatile-defined">;
440
- networkInjectEnv: z<boolean, boolean, "volatile-defined">;
441
- networkAskTimeoutMs: z<number, number, "volatile-defined">;
442
- networkGrantTtlMs: z<number, number, "volatile-defined">;
443
- sandboxEnabled: z<boolean, boolean, "volatile-defined">;
444
- sandboxMode: z<"read-only" | "workspace-write", "read-only" | "workspace-write", "volatile-defined">;
445
- sandboxProxy: z<string, string, "volatile-defined">;
446
- sandboxLoopback: z<"exempt" | "off", "exempt" | "off", "volatile-defined">;
447
- rules: z<NoInfer<RulesConfig>, NoInfer<RulesConfig>, "volatile">;
590
+ badFilePolicy: z<"fail" | "warn", "fail" | "warn", "defined">;
591
+ maxChainLength: z<number, number, "defined">;
592
+ networkEnabled: z<boolean, boolean, "defined">;
593
+ networkMode: z<"deny-all" | "whitelist" | "allow-all", "deny-all" | "whitelist" | "allow-all", "defined">;
594
+ networkUnlisted: z<"ask" | "deny", "ask" | "deny", "defined">;
595
+ networkUnattributed: z<"allow" | "deny", "allow" | "deny", "defined">;
596
+ networkLoopback: z<"allow" | "policy", "allow" | "policy", "defined">;
597
+ networkBind: z<string, string, "defined">;
598
+ networkPort: z<number, number, "defined">;
599
+ networkNoProxy: z<"clear" | "preserve", "clear" | "preserve", "defined">;
600
+ networkInjectEnv: z<boolean, boolean, "defined">;
601
+ networkAskTimeoutMs: z<number, number, "defined">;
602
+ networkGrantTtlMs: z<number, number, "defined">;
603
+ sandboxEnabled: z<boolean, boolean, "defined">;
604
+ sandboxMode: z<"read-only" | "workspace-write", "read-only" | "workspace-write", "defined">;
605
+ sandboxProxy: z<string, string, "defined">;
606
+ sandboxLoopback: z<"exempt" | "off", "exempt" | "off", "defined">;
607
+ rules: z<RulesConfig>;
448
608
  watch: z<boolean, boolean, "defined">;
449
609
  watchDebounceMs: z<number, number, "defined">;
450
610
  }>>, "plain">;
451
611
  /** Live reference the 0.1.7 loader hands `apply` for `.volatile()` config fields. */
452
612
  export type VolatileRef<T> = Volatile<T>;
453
613
  /** The config fields marked `.volatile()` — live refs inside `apply`'s config. */
454
- export declare const VOLATILE_CONFIG_KEYS: readonly ["defaultAction", "caseInsensitivePaths", "classifierEnabled", "classifierEndpoint", "classifierModel", "classifierApiKey", "riskTimeoutMs", "riskLearning", "riskSediment", "classifierSource", "classifierProvider", "riskThreshold", "grantTtlMs", "grantMaxUses", "permissive", "permissiveStrategies", "allowlist", "denyKeywords", "autoAllowTools", "sessionSweep", "searchUp", "badFilePolicy", "maxChainLength", "networkEnabled", "networkMode", "networkUnlisted", "networkUnattributed", "networkLoopback", "networkBind", "networkPort", "networkNoProxy", "networkInjectEnv", "networkAskTimeoutMs", "networkGrantTtlMs", "rules", "sandboxEnabled", "sandboxMode", "sandboxProxy", "sandboxLoopback"];
614
+ export declare const VOLATILE_CONFIG_KEYS: readonly ["defaultAction", "caseInsensitivePaths", "classifierEnabled", "classifierEndpoint", "classifierModel", "classifierApiKey", "riskTimeoutMs", "riskLearning", "riskSediment", "classifierSource", "classifierProvider", "riskThreshold", "grantTtlMs", "grantMaxUses", "permissive", "yolo", "rulesFallback", "unattendedTiers", "safenetPass", "safenetAsk", "scriptReview", "permissiveStrategies", "allowlist", "denyKeywords", "denyKeywordsExtra", "credentialPaths", "extraContentArgKeys", "extraOperationArgKeys", "presetRulesSeeded", "autoAllowTools", "sessionSweep", "searchUp", "badFilePolicy", "maxChainLength", "networkEnabled", "networkMode", "networkUnlisted", "networkUnattributed", "networkLoopback", "networkBind", "networkPort", "networkNoProxy", "networkInjectEnv", "networkAskTimeoutMs", "networkGrantTtlMs", "rules", "sandboxEnabled", "sandboxMode", "sandboxProxy", "sandboxLoopback"];
455
615
  /** Resolve one possibly-volatile field: a live ref on 0.1.7+, a plain value otherwise. */
456
616
  export declare function readVolatileValue<T>(value: T | Volatile<T> | undefined): VolatileSnapshot<T> | undefined;
457
617
  /** Shallow-resolve every volatile field into a plain snapshot (one per read). */
package/lib/config.js CHANGED
@@ -150,64 +150,107 @@ export function readRulesFromSettings(scope) {
150
150
  const value = scope?.get();
151
151
  return isRulesConfigured(value) ? value : undefined;
152
152
  }
153
+ /**
154
+ * Feature guard for `Schema…volatile()`.
155
+ *
156
+ * `.volatile()` is not guaranteed to exist on the schemastery copy this plugin
157
+ * resolves at runtime. Every node below used to chain it unguarded **inside the
158
+ * module body**, so a missing method throws while `Config` is being built: the
159
+ * plugin fails to load at all (`engines.dsh` declares support for all 15 host
160
+ * lines, so that is a hard load-failure, not a degradation).
161
+ *
162
+ * Probing per node keeps the plain schema node instead. The read side already
163
+ * tolerates that — `readVolatileValue` / `resolveVolatileConfig` treat a value
164
+ * without `get()` as the frozen snapshot — so every switch still works, and only
165
+ * hot reload of a changed value is lost (it then needs a restart).
166
+ *
167
+ * Do **not** “fix” this by adding `volatile` to schemastery's prototype: the
168
+ * profile uses a hoisted flat layout, so that single copy is shared by every
169
+ * plugin in the profile (14 of them declare schemastery themselves) — a
170
+ * prototype write here would silently change their behaviour too.
171
+ */
172
+ function vol(node) {
173
+ const maybe = node;
174
+ return typeof maybe.volatile === 'function' ? maybe.volatile() : node;
175
+ }
153
176
  export const Config = z.object({
154
177
  rulesFile: z.string(),
155
178
  dshHome: z.string(),
156
- defaultAction: z.union(['allow', 'ask', 'deny']).default('ask').volatile(),
157
- caseInsensitivePaths: z.boolean().default(true).volatile(),
158
- classifierEnabled: z.boolean().default(false).volatile(),
159
- classifierEndpoint: z.string().volatile(),
160
- classifierModel: z.string().default('deepseek-chat').volatile(),
161
- classifierApiKey: z.string().volatile(),
162
- riskTimeoutMs: z.number().min(1000).default(20_000).volatile(),
163
- riskLearning: z.boolean().default(true).volatile(),
164
- riskSediment: z.boolean().default(true).volatile(),
165
- classifierSource: z.union(['custom', 'host']).default('custom').volatile(),
166
- classifierProvider: z.string().volatile(),
167
- riskThreshold: z.number().min(1).max(10).default(1).volatile(),
179
+ defaultAction: vol(z.union(['allow', 'ask', 'deny']).default('ask')),
180
+ caseInsensitivePaths: vol(z.boolean().default(true)),
181
+ classifierEnabled: vol(z.boolean().default(false)),
182
+ classifierEndpoint: vol(z.string()),
183
+ classifierModel: vol(z.string().default('deepseek-chat')),
184
+ classifierApiKey: vol(z.string()),
185
+ riskTimeoutMs: vol(z.number().min(1000).default(20_000)),
186
+ riskLearning: vol(z.boolean().default(true)),
187
+ riskSediment: vol(z.boolean().default(true)),
188
+ classifierSource: vol(z.union(['custom', 'host']).default('custom')),
189
+ classifierProvider: vol(z.string()),
190
+ riskThreshold: vol(z.number().min(1).max(10).default(1)),
168
191
  learningFile: z.string(),
169
192
  eventsFile: z.string(),
170
- grantTtlMs: z.number().min(1).default(5 * 60_000).volatile(),
171
- grantMaxUses: z.number().min(1).default(1).volatile(),
172
- permissive: z.boolean().default(false).volatile(),
193
+ grantTtlMs: vol(z.number().min(1).default(5 * 60_000)),
194
+ grantMaxUses: vol(z.number().min(1).default(1)),
195
+ permissive: vol(z.boolean().default(false)),
196
+ yolo: vol(z.boolean().default(false)),
197
+ rulesFallback: vol(z.boolean().default(false)),
198
+ /**
199
+ * Retired (2026-10-17): the per-preset mode table that briefly mapped
200
+ * `<preset id>=<mode>`. Each human-free implementation now owns exactly one preset and
201
+ * exactly one switch (`yolo` / `safenetPass` / `safenetAsk`), so presets never cross and a
202
+ * switch means what it says — whether that implementation exists. The key is kept in the
203
+ * schema only so an older profile that stored it still parses.
204
+ */
205
+ unattendedTiers: vol(z.array(z.string()).default([])),
206
+ /** Switch: the `permissive-mech` implementation exists (mechanical takeover of asks). */
207
+ safenetPass: vol(z.boolean().default(true)),
208
+ /** Switch: the `permissive-snet` implementation exists (mechanical screen, still asks). */
209
+ safenetAsk: vol(z.boolean().default(false)),
210
+ scriptReview: vol(z.boolean().default(true)),
173
211
  gatePresets: z.array(z.string()),
174
- permissiveStrategies: z.object({
212
+ permissiveStrategies: vol(z.object({
175
213
  trustAutoAllow: z.boolean().default(true),
176
214
  alwaysConfirm: z.boolean().default(false),
177
215
  llmAssist: z.boolean().default(false),
178
216
  trustEscalation: z.boolean().default(true),
179
- }).volatile(),
180
- allowlist: z.array(z.string()).volatile(),
181
- denyKeywords: z.array(z.string()).volatile(),
182
- autoAllowTools: z.array(z.string()).volatile(),
183
- sessionSweep: z.boolean().default(true).volatile(),
217
+ })),
218
+ allowlist: vol(z.array(z.string())),
219
+ denyKeywords: vol(z.array(z.string())),
220
+ denyKeywordsExtra: vol(z.array(z.string()).default([])),
221
+ credentialPaths: vol(z.array(z.string()).default([])),
222
+ extraContentArgKeys: vol(z.array(z.string()).default([])),
223
+ extraOperationArgKeys: vol(z.array(z.string()).default([])),
224
+ presetRulesSeeded: vol(z.boolean().default(false)),
225
+ autoAllowTools: vol(z.array(z.string())),
226
+ sessionSweep: vol(z.boolean().default(true)),
184
227
  workspaceStoreFile: z.string(),
185
228
  // Rule chain (T1.9)
186
- searchUp: z.boolean().default(false).volatile(),
229
+ searchUp: vol(z.boolean().default(false)),
187
230
  fallbackPath: z.string(),
188
- badFilePolicy: z.union(['fail', 'warn']).default('fail').volatile(),
189
- maxChainLength: z.number().min(1).max(50).default(10).volatile(),
231
+ badFilePolicy: vol(z.union(['fail', 'warn']).default('fail')),
232
+ maxChainLength: vol(z.number().min(1).max(50).default(10)),
190
233
  // Network (Phase 2)
191
- networkEnabled: z.boolean().default(false).volatile(),
192
- networkMode: z.union(['deny-all', 'whitelist', 'allow-all']).default('whitelist').volatile(),
193
- networkUnlisted: z.union(['ask', 'deny']).default('ask').volatile(),
194
- networkUnattributed: z.union(['allow', 'deny']).default('allow').volatile(),
195
- networkLoopback: z.union(['allow', 'policy']).default('allow').volatile(),
196
- networkBind: z.string().default('127.0.0.1').volatile(),
197
- networkPort: z.number().min(0).max(65535).default(0).volatile(),
198
- networkNoProxy: z.union(['clear', 'preserve']).default('clear').volatile(),
199
- networkInjectEnv: z.boolean().default(true).volatile(),
200
- networkAskTimeoutMs: z.number().min(1000).max(600_000).default(120_000).volatile(),
201
- networkGrantTtlMs: z.number().min(0).max(24 * 60 * 60_000).default(30 * 60_000).volatile(),
234
+ networkEnabled: vol(z.boolean().default(false)),
235
+ networkMode: vol(z.union(['deny-all', 'whitelist', 'allow-all']).default('whitelist')),
236
+ networkUnlisted: vol(z.union(['ask', 'deny']).default('ask')),
237
+ networkUnattributed: vol(z.union(['allow', 'deny']).default('allow')),
238
+ networkLoopback: vol(z.union(['allow', 'policy']).default('allow')),
239
+ networkBind: vol(z.string().default('127.0.0.1')),
240
+ networkPort: vol(z.number().min(0).max(65535).default(0)),
241
+ networkNoProxy: vol(z.union(['clear', 'preserve']).default('clear')),
242
+ networkInjectEnv: vol(z.boolean().default(true)),
243
+ networkAskTimeoutMs: vol(z.number().min(1000).max(600_000).default(120_000)),
244
+ networkGrantTtlMs: vol(z.number().min(0).max(24 * 60 * 60_000).default(30 * 60_000)),
202
245
  // AppContainer sandbox (Windows; goal 2)
203
- sandboxEnabled: z.boolean().default(false).volatile(),
204
- sandboxMode: z.union(['read-only', 'workspace-write']).default('workspace-write').volatile(),
205
- sandboxProxy: z.string().default('').volatile(),
206
- sandboxLoopback: z.union(['exempt', 'off']).default('exempt').volatile(),
246
+ sandboxEnabled: vol(z.boolean().default(false)),
247
+ sandboxMode: vol(z.union(['read-only', 'workspace-write']).default('workspace-write')),
248
+ sandboxProxy: vol(z.string().default('')),
249
+ sandboxLoopback: vol(z.union(['exempt', 'off']).default('exempt')),
207
250
  // 0.1.7: the rules document lives on this entry as a volatile whole-object
208
251
  // field (a second settings namespace is no longer projectable). Unconfigured
209
252
  // (bare defaults) -> the rules file remains the source.
210
- rules: RulesSchema.volatile(),
253
+ rules: vol(RulesSchema),
211
254
  // Hot reload (Phase 3)
212
255
  watch: z.boolean().default(false),
213
256
  watchDebounceMs: z.number().min(50).max(5000).default(300),
@@ -217,7 +260,7 @@ export const VOLATILE_CONFIG_KEYS = [
217
260
  'defaultAction', 'caseInsensitivePaths', 'classifierEnabled', 'classifierEndpoint',
218
261
  'classifierModel', 'classifierApiKey', 'riskTimeoutMs', 'riskLearning', 'riskSediment',
219
262
  'classifierSource', 'classifierProvider', 'riskThreshold', 'grantTtlMs', 'grantMaxUses',
220
- 'permissive', 'permissiveStrategies', 'allowlist', 'denyKeywords', 'autoAllowTools',
263
+ 'permissive', 'yolo', 'rulesFallback', 'unattendedTiers', 'safenetPass', 'safenetAsk', 'scriptReview', 'permissiveStrategies', 'allowlist', 'denyKeywords', 'denyKeywordsExtra', 'credentialPaths', 'extraContentArgKeys', 'extraOperationArgKeys', 'presetRulesSeeded', 'autoAllowTools',
221
264
  'sessionSweep', 'searchUp', 'badFilePolicy', 'maxChainLength',
222
265
  'networkEnabled', 'networkMode', 'networkUnlisted', 'networkUnattributed', 'networkLoopback',
223
266
  'networkBind', 'networkPort', 'networkNoProxy', 'networkInjectEnv', 'networkAskTimeoutMs',
@@ -0,0 +1,35 @@
1
+ /**
2
+ * User-declared credential paths — the long tail the built-in list cannot know.
3
+ *
4
+ * The built-in credential set (`.ssh`, `.aws`, `.npmrc`, `credentials.yaml`, …) ships in
5
+ * `path.ts` because the layer that needs it is P0: a `read`/`grep` call is auto-allowed
6
+ * one stage before the rule chain runs, and mutating a file tool never reaches a model
7
+ * that could be talked out of it. That is also why this cannot simply be "add a rule to
8
+ * `rules.yml`" for the read side — see the note in `runtime.ts` about why the auto-allow
9
+ * does not consult the ruleset (preset entries and hand-written entries live in one
10
+ * document with no provenance, so `.dsh/**` would start blocking legitimate memory
11
+ * reads).
12
+ *
13
+ * So the *list* is what a user extends, through the `credentialPaths` setting, and the
14
+ * *tier* stays where it has to be: a listed path gets the same treatment as the
15
+ * built-ins — reading it outside the workspace is refused, writing it is refused.
16
+ *
17
+ * Matching is pure string work: no filesystem access, no resolution, no execution. A
18
+ * pattern that cannot be understood is dropped rather than guessed at — an unreadable
19
+ * entry must not silently become "protects everything".
20
+ */
21
+ /**
22
+ * Compile the configured credential paths into one predicate.
23
+ *
24
+ * Returns a matcher that answers `false` for an empty or unusable list, so callers can
25
+ * pass the raw setting straight through. `home` expands a leading `~/` the way a shell
26
+ * would, because the setting is written in home-relative form (`~/.myco/vault.token`)
27
+ * while the paths being judged are absolute. **Without `home`** the `~/` is stripped and
28
+ * the rest matched as a path fragment — a declared secret must not silently stop
29
+ * protecting because one caller did not know the user directory; the widened tail match
30
+ * only ever denies more, never less.
31
+ */
32
+ export declare function compileCredentialPaths(entries: readonly string[] | undefined, opts?: {
33
+ readonly caseInsensitive?: boolean;
34
+ readonly home?: string;
35
+ }): (path: string) => boolean;