dsh-perm-gate 5.4.3 → 5.4.4

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/lib/config.d.ts CHANGED
@@ -94,6 +94,11 @@ export interface PermGateConfig {
94
94
  * the model never answered instead of letting the tier quietly become a different one.
95
95
  */
96
96
  readonly yolo?: boolean;
97
+ /**
98
+ * YOLO only: relax the "target unpinnable" opaque signal. Never relaxes invisible
99
+ * content, deletes, remote writes, P0, credentials or irreversible acts. Default false.
100
+ */
101
+ readonly yoloWorkspaceExempt?: boolean;
97
102
  /**
98
103
  * `safenet-pass` — the switch behind the `permissive-mech` tier: the deterministic
99
104
  * mechanical screen plus the preset opinions take over the ask. Named danger is blocked
@@ -512,6 +517,13 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
512
517
  grantMaxUses: z<number, number, "defined">;
513
518
  permissive: z<boolean, boolean, "defined">;
514
519
  yolo: z<boolean, boolean, "defined">;
520
+ /**
521
+ * YOLO only: stop treating "the target could not be pinned down" as a danger signal
522
+ * (no-argument tool calls, a script outside the workspace whose body was not read).
523
+ * Invisible *content* — encoded payloads, unreadable bodies, prose — is still blocked,
524
+ * and delete/remote-write never qualify. Default false keeps the 5.4.3 behaviour.
525
+ */
526
+ yoloWorkspaceExempt: z<boolean, boolean, "defined">;
515
527
  rulesFallback: z<boolean, boolean, "defined">;
516
528
  /**
517
529
  * Retired (2026-10-17): the per-preset mode table that briefly mapped
@@ -598,6 +610,13 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
598
610
  grantMaxUses: z<number, number, "defined">;
599
611
  permissive: z<boolean, boolean, "defined">;
600
612
  yolo: z<boolean, boolean, "defined">;
613
+ /**
614
+ * YOLO only: stop treating "the target could not be pinned down" as a danger signal
615
+ * (no-argument tool calls, a script outside the workspace whose body was not read).
616
+ * Invisible *content* — encoded payloads, unreadable bodies, prose — is still blocked,
617
+ * and delete/remote-write never qualify. Default false keeps the 5.4.3 behaviour.
618
+ */
619
+ yoloWorkspaceExempt: z<boolean, boolean, "defined">;
601
620
  rulesFallback: z<boolean, boolean, "defined">;
602
621
  /**
603
622
  * Retired (2026-10-17): the per-preset mode table that briefly mapped
@@ -666,7 +685,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
666
685
  /** Live reference the 0.1.7 loader hands `apply` for `.volatile()` config fields. */
667
686
  export type VolatileRef<T> = Volatile<T>;
668
687
  /** The config fields marked `.volatile()` — live refs inside `apply`'s config. */
669
- export declare const VOLATILE_CONFIG_KEYS: readonly ["defaultAction", "caseInsensitivePaths", "classifierEnabled", "classifierEndpoint", "classifierModel", "classifierEffort", "classifierApiKey", "riskTimeoutMs", "riskLearning", "riskSediment", "classifierSource", "classifierProvider", "riskThreshold", "grantTtlMs", "grantMaxUses", "permissive", "yolo", "rulesFallback", "unattendedTiers", "safenetPass", "safenetAsk", "allowProtectedBranchPush", "credWriteExemptFamilies", "presetRulesVersion", "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"];
688
+ export declare const VOLATILE_CONFIG_KEYS: readonly ["defaultAction", "caseInsensitivePaths", "classifierEnabled", "classifierEndpoint", "classifierModel", "classifierEffort", "classifierApiKey", "riskTimeoutMs", "riskLearning", "riskSediment", "classifierSource", "classifierProvider", "riskThreshold", "grantTtlMs", "grantMaxUses", "permissive", "yolo", "yoloWorkspaceExempt", "rulesFallback", "unattendedTiers", "safenetPass", "safenetAsk", "allowProtectedBranchPush", "credWriteExemptFamilies", "presetRulesVersion", "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"];
670
689
  /** Resolve one possibly-volatile field: a live ref on 0.1.7+, a plain value otherwise. */
671
690
  export declare function readVolatileValue<T>(value: T | Volatile<T> | undefined): VolatileSnapshot<T> | undefined;
672
691
  /** Shallow-resolve every volatile field into a plain snapshot (one per read). */
package/lib/config.js CHANGED
@@ -195,6 +195,13 @@ export const Config = z.object({
195
195
  grantMaxUses: vol(z.number().min(1).default(1)),
196
196
  permissive: vol(z.boolean().default(false)),
197
197
  yolo: vol(z.boolean().default(false)),
198
+ /**
199
+ * YOLO only: stop treating "the target could not be pinned down" as a danger signal
200
+ * (no-argument tool calls, a script outside the workspace whose body was not read).
201
+ * Invisible *content* — encoded payloads, unreadable bodies, prose — is still blocked,
202
+ * and delete/remote-write never qualify. Default false keeps the 5.4.3 behaviour.
203
+ */
204
+ yoloWorkspaceExempt: vol(z.boolean().default(false)),
198
205
  rulesFallback: vol(z.boolean().default(false)),
199
206
  /**
200
207
  * Retired (2026-10-17): the per-preset mode table that briefly mapped
@@ -267,7 +274,7 @@ export const VOLATILE_CONFIG_KEYS = [
267
274
  'defaultAction', 'caseInsensitivePaths', 'classifierEnabled', 'classifierEndpoint',
268
275
  'classifierModel', 'classifierEffort', 'classifierApiKey', 'riskTimeoutMs', 'riskLearning', 'riskSediment',
269
276
  'classifierSource', 'classifierProvider', 'riskThreshold', 'grantTtlMs', 'grantMaxUses',
270
- 'permissive', 'yolo', 'rulesFallback', 'unattendedTiers', 'safenetPass', 'safenetAsk', 'allowProtectedBranchPush', 'credWriteExemptFamilies', 'presetRulesVersion', 'scriptReview', 'permissiveStrategies', 'allowlist', 'denyKeywords', 'denyKeywordsExtra', 'credentialPaths', 'extraContentArgKeys', 'extraOperationArgKeys', 'presetRulesSeeded', 'autoAllowTools',
277
+ 'permissive', 'yolo', 'yoloWorkspaceExempt', 'rulesFallback', 'unattendedTiers', 'safenetPass', 'safenetAsk', 'allowProtectedBranchPush', 'credWriteExemptFamilies', 'presetRulesVersion', 'scriptReview', 'permissiveStrategies', 'allowlist', 'denyKeywords', 'denyKeywordsExtra', 'credentialPaths', 'extraContentArgKeys', 'extraOperationArgKeys', 'presetRulesSeeded', 'autoAllowTools',
271
278
  'sessionSweep', 'searchUp', 'badFilePolicy', 'maxChainLength',
272
279
  'networkEnabled', 'networkMode', 'networkUnlisted', 'networkUnattributed', 'networkLoopback',
273
280
  'networkBind', 'networkPort', 'networkNoProxy', 'networkInjectEnv', 'networkAskTimeoutMs',
package/lib/index.js CHANGED
@@ -564,6 +564,9 @@ export function apply(ctx, config = {}) {
564
564
  // call. It is the switch behind the `permissive-yolo` tier only — never a flow added to
565
565
  // sessions that did not select it.
566
566
  readYolo: () => current().yolo ?? false,
567
+ // YOLO-only narrowing: stop treating "the target could not be pinned down" as danger.
568
+ // Invisible content, deletes, remote writes and every deterministic floor stay armed.
569
+ readYoloWorkspaceExempt: () => current().yoloWorkspaceExempt === true,
567
570
  // The two `safenet` switches, read live: each controls whether one implementation
568
571
  // exists, and each implementation owns one preset, so presets never cross.
569
572
  readSafenetPass: () => current().safenetPass ?? true,
package/lib/runtime.d.ts CHANGED
@@ -151,6 +151,12 @@ export interface PermGateRuntimeOptions extends PermGateConfig {
151
151
  readonly readPermissive?: () => PermissiveState;
152
152
  /** Optional live source of the YOLO tier switch (never human-confirm; block-on-danger). */
153
153
  readonly readYolo?: () => boolean;
154
+ /**
155
+ * Optional live source of the YOLO workspace exemption: stop treating "the target could
156
+ * not be pinned down" as a danger signal. Only `permissive-yolo` reads it, and only a
157
+ * *location*-kind opacity qualifies — invisible content, deletes and remote writes never do.
158
+ */
159
+ readonly readYoloWorkspaceExempt?: () => boolean;
154
160
  /**
155
161
  * Optional live source of the degraded-review switch: with no reviewer able to grade
156
162
  * an ask, decide on the deterministic nets instead of parking it on a human.
@@ -491,6 +497,18 @@ export declare class PermGateRuntime {
491
497
  private livePermissive;
492
498
  /** Whether YOLO mode is live (never human-confirm). */
493
499
  isYolo(): boolean;
500
+ /**
501
+ * Whether the YOLO tier should stop blocking on "target unpinnable" alone.
502
+ * Default false: 5.4.3 behaviour, one switch away.
503
+ */
504
+ private liveYoloWorkspaceExempt;
505
+ /**
506
+ * The one narrowing the exemption is allowed to make: the call is ordinary and visible,
507
+ * only its *location* was unpinnable, and everything it names is inside the workspace.
508
+ * Delete, remote write, invisible content, P0/irreversible and unlearnable acts are all
509
+ * refused here — this method can only *skip* the opaque signal, never clear a danger.
510
+ */
511
+ private yoloExemptsLocation;
494
512
  /** Whether degraded review is live (no reviewer ⇒ rule-only gate). */
495
513
  isRulesFallback(): boolean;
496
514
  /** Whether script-body review is live (default on). */
package/lib/runtime.js CHANGED
@@ -24,6 +24,7 @@ import { ArtifactRegistry } from './path.js';
24
24
  import { classifyRisk, classifyRiskWith, graderNote, RISK_SYSTEM_PROMPT_VERSION } from './risk.js';
25
25
  import { askDoubtKinds, buildAskContext, humanBrief, humanCardLine, humanCommand, toEventFields, toGraderPayload } from './ask-context.js';
26
26
  import { readScriptContents, scriptRefs, scriptsFullyCovered } from './script-review.js';
27
+ import { pathsRealInsideWorkspace } from './workspace-bound.js';
27
28
  import { redactCommandText } from './redact.js';
28
29
  import { executableVariants, normalizeCommandText } from './command-normalize.js';
29
30
  import { completeViaHost, DEFAULT_HOST_MODEL } from './host-llm.js';
@@ -697,6 +698,34 @@ export class PermGateRuntime {
697
698
  isYolo() {
698
699
  return this.options.readYolo !== undefined ? this.options.readYolo() : (this.options.yolo ?? false);
699
700
  }
701
+ /**
702
+ * Whether the YOLO tier should stop blocking on "target unpinnable" alone.
703
+ * Default false: 5.4.3 behaviour, one switch away.
704
+ */
705
+ liveYoloWorkspaceExempt() {
706
+ return this.options.readYoloWorkspaceExempt !== undefined
707
+ ? this.options.readYoloWorkspaceExempt()
708
+ : (this.options.yoloWorkspaceExempt ?? false);
709
+ }
710
+ /**
711
+ * The one narrowing the exemption is allowed to make: the call is ordinary and visible,
712
+ * only its *location* was unpinnable, and everything it names is inside the workspace.
713
+ * Delete, remote write, invisible content, P0/irreversible and unlearnable acts are all
714
+ * refused here — this method can only *skip* the opaque signal, never clear a danger.
715
+ */
716
+ yoloExemptsLocation(ctx) {
717
+ return this.liveYoloWorkspaceExempt()
718
+ && ctx.opaqueKind === 'location'
719
+ && ctx.targetKind !== 'delete'
720
+ && ctx.targetKind !== 'remote-write'
721
+ && !ctx.irreversible
722
+ && !ctx.unlearnable
723
+ && ctx.workspaceBound === true
724
+ // `workspaceBound` is lexical (the shared module cannot touch the filesystem); the
725
+ // relaxation also has to survive link resolution, so a link inside the workspace
726
+ // that points outside is still refused here.
727
+ && pathsRealInsideWorkspace(ctx.workspaceRoot, ctx.files ?? []);
728
+ }
700
729
  /** Whether degraded review is live (no reviewer ⇒ rule-only gate). */
701
730
  isRulesFallback() {
702
731
  return this.options.readRulesFallback !== undefined
@@ -1569,7 +1598,11 @@ export class PermGateRuntime {
1569
1598
  async resolveYolo(exec, decision) {
1570
1599
  const callId = randomId();
1571
1600
  const askCtx = this.unattendedContextFor(exec, decision);
1572
- const judged = this.mechanicalJudge(askCtx);
1601
+ // The workspace exemption narrows exactly one signal: "the target could not be pinned
1602
+ // down". It is decided before the screen so an exempted call reaches the reviewer (or
1603
+ // the mechanical conclusion) as an ordinary one; every other danger signal still fires.
1604
+ const exempt = this.yoloExemptsLocation(askCtx);
1605
+ const judged = exempt ? null : this.mechanicalJudge(askCtx);
1573
1606
  let riskCategory;
1574
1607
  // Whether the reviewer actually spoke. YOLO advertises "the model answers in the
1575
1608
  // person's place"; when there is no grader, or it gave up, the tier still refuses to ask
@@ -1594,11 +1627,15 @@ export class PermGateRuntime {
1594
1627
  }
1595
1628
  const danger = judged !== null || riskCategory !== undefined;
1596
1629
  if (!danger) {
1597
- const reason = reviewerAnswered
1630
+ const exemptTag = exempt ? ' [exempt:opaque-location]' : '';
1631
+ const reason = (reviewerAnswered
1598
1632
  ? `yolo: allow (no danger signal) — ${decision.reason}`
1599
- : `yolo: allow (no reviewer answered; mechanical screen found nothing) — ${decision.reason}`;
1633
+ : `yolo: allow (no reviewer answered; mechanical screen found nothing) — ${decision.reason}`) + exemptTag;
1634
+ const verdict = exempt
1635
+ ? (reviewerAnswered ? 'yolo-allow-opaque-location-exempt' : 'yolo-allow-opaque-location-exempt-no-reviewer')
1636
+ : (reviewerAnswered ? 'yolo-allow' : 'yolo-allow-no-reviewer');
1600
1637
  this.audit.append(makeEntry({ callId, tool: exec.name, outcome: 'allow', source: 'classifier', reason, at: Date.now() }));
1601
- this.recordEvent(exec, 'auto', reason, { verdict: reviewerAnswered ? 'yolo-allow' : 'yolo-allow-no-reviewer', risk: 'safe' });
1638
+ this.recordEvent(exec, 'auto', reason, { verdict, risk: 'safe' });
1602
1639
  this.untrackAsk(exec);
1603
1640
  return undefined;
1604
1641
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Whether every named path resolves inside `root`, after following links.
3
+ *
4
+ * An empty list is `true` (the call named nothing), a missing root is `false`, and any
5
+ * path that cannot be resolved is `false`.
6
+ */
7
+ export declare function pathsRealInsideWorkspace(root: string | undefined, paths: readonly string[]): boolean;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Node-side containment check for the YOLO workspace exemption.
3
+ *
4
+ * `ask-context.ts` stays dependency-free (the browser half imports its types), so its
5
+ * `workspaceBound` is a **lexical** prefix test. That is the right answer for the fact
6
+ * layer but not sufficient to *relax* a safety screen: a link inside the workspace can
7
+ * point anywhere. This module resolves the paths the call actually names and answers the
8
+ * same question through the filesystem, and it is deliberately pessimistic — a path that
9
+ * cannot be resolved (missing, unreadable, permission denied) answers `false`, because
10
+ * "I could not prove it stays inside" must never read as "it stays inside".
11
+ *
12
+ * Node half only: never imported by the client bundle.
13
+ */
14
+ import { realpathSync } from 'node:fs';
15
+ import { isAbsolute, resolve, sep } from 'node:path';
16
+ /** Windows paths are case-insensitive; the other supported lines are not. */
17
+ function fold(path) {
18
+ return process.platform === 'win32' ? path.toLowerCase() : path;
19
+ }
20
+ /**
21
+ * Whether every named path resolves inside `root`, after following links.
22
+ *
23
+ * An empty list is `true` (the call named nothing), a missing root is `false`, and any
24
+ * path that cannot be resolved is `false`.
25
+ */
26
+ export function pathsRealInsideWorkspace(root, paths) {
27
+ if (root === undefined || root === '')
28
+ return false;
29
+ if (paths.length === 0)
30
+ return true;
31
+ let realRoot;
32
+ try {
33
+ realRoot = fold(realpathSync.native(root));
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ return paths.every((candidate) => {
39
+ try {
40
+ const absolute = isAbsolute(candidate) ? candidate : resolve(root, candidate);
41
+ const real = fold(realpathSync.native(absolute));
42
+ return real === realRoot || real.startsWith(`${realRoot}${sep}`);
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ });
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-perm-gate",
3
- "version": "5.4.3",
3
+ "version": "5.4.4",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "tag": "latest"