dsh-perm-gate 5.4.1 → 5.4.3

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/runtime.js CHANGED
@@ -28,7 +28,7 @@ import { redactCommandText } from './redact.js';
28
28
  import { executableVariants, normalizeCommandText } from './command-normalize.js';
29
29
  import { completeViaHost, DEFAULT_HOST_MODEL } from './host-llm.js';
30
30
  import { chatCompletion } from './classifier.js';
31
- import { compileDocument, compileRulesObject, documentHash, extractPathCandidates, parsePermissionsDocument, } from './rule.js';
31
+ import { coerceRuleAction, compileDocument, compileRulesObject, documentHash, extractPathCandidates, parsePermissionsDocument, } from './rule.js';
32
32
  import { decomposeShellCommand } from './shell.js';
33
33
  import { extractAgentCandidates } from './agent-identity.js';
34
34
  import { resolveRuleChain } from './rule-chain.js';
@@ -289,10 +289,17 @@ export class PermGateRuntime {
289
289
  }
290
290
  compileInline(options) {
291
291
  const empty = {
292
- defaultAction: options.defaultAction ?? 'ask',
292
+ defaultAction: this.effectiveDefaultAction(options),
293
293
  deny: [], allow: [], ask: [],
294
294
  caseInsensitivePaths: options.caseInsensitivePaths ?? true,
295
295
  };
296
+ // Every other path yields a ruleset whose default came from a document; a
297
+ // document is validated, but the action is still coerced so one bad source
298
+ // can never hand the runtime an action it has no branch for.
299
+ const settle = (rs) => {
300
+ const action = coerceRuleAction(rs.defaultAction);
301
+ return action === rs.defaultAction ? rs : { ...rs, defaultAction: action };
302
+ };
296
303
  // Settings-first (dual-source): a configured `dsh-perm-gate-rules`
297
304
  // namespace overrides every file path — the gate then loads with zero
298
305
  // filesystem dependency. Bare defaults (nothing configured) fall through
@@ -303,20 +310,20 @@ export class PermGateRuntime {
303
310
  const hash = documentHash(JSON.stringify(settingsDoc));
304
311
  const hit = this.cache.get(hash);
305
312
  if (hit !== undefined)
306
- return hit;
313
+ return settle(hit);
307
314
  const compiled = compileRulesObject(settingsDoc, {
308
315
  maxGlobStars: 2,
309
316
  caseInsensitivePaths: options.caseInsensitivePaths ?? true,
310
317
  });
311
318
  this.cache.set(hash, compiled);
312
- return compiled;
319
+ return settle(compiled);
313
320
  }
314
321
  // Chain mode: when searchUp is enabled, use the multi-file chain resolver.
315
322
  if (options.searchUp === true) {
316
323
  const cwd = options.cwd ?? process.cwd();
317
324
  const rulesFile = options.rulesFile !== undefined ? options.rulesFile : 'rules.yml';
318
325
  try {
319
- return resolveRuleChain(cwd, {
326
+ return settle(resolveRuleChain(cwd, {
320
327
  rulesFile,
321
328
  searchUp: true,
322
329
  fallbackPath: options.fallbackPath,
@@ -325,7 +332,7 @@ export class PermGateRuntime {
325
332
  }, {
326
333
  maxGlobStars: 2,
327
334
  caseInsensitivePaths: options.caseInsensitivePaths ?? true,
328
- });
335
+ }));
329
336
  }
330
337
  catch {
331
338
  return empty;
@@ -338,14 +345,27 @@ export class PermGateRuntime {
338
345
  const hash = documentHash(text);
339
346
  const hit = this.cache.get(hash);
340
347
  if (hit !== undefined)
341
- return hit;
348
+ return settle(hit);
342
349
  const doc = parsePermissionsDocument(text);
343
350
  const compiled = compileDocument(doc, {
344
351
  maxGlobStars: 2,
345
352
  caseInsensitivePaths: options.caseInsensitivePaths ?? true,
346
353
  });
347
354
  this.cache.set(hash, compiled);
348
- return compiled;
355
+ return settle(compiled);
356
+ }
357
+ /**
358
+ * The root default action, read live (the config field is volatile) and
359
+ * validated. An unrecognised value — most often a `.volatile()` ref that
360
+ * reached the options un-unwrapped — fails **closed** to `ask`: previously it
361
+ * became the ruleset default verbatim, both the deny and ask branches missed
362
+ * it, and every unmatched call was allowed without review (issue #6).
363
+ */
364
+ effectiveDefaultAction(options) {
365
+ const raw = options.readDefaultAction !== undefined
366
+ ? options.readDefaultAction()
367
+ : options.defaultAction;
368
+ return coerceRuleAction(raw);
349
369
  }
350
370
  /** Reload rules from disk (HMR); on failure keeps the previous rules. */
351
371
  reload() {
@@ -659,6 +679,15 @@ export class PermGateRuntime {
659
679
  const sel = this.options.readHostModel?.();
660
680
  return sel !== undefined && sel.provider !== '' && sel.model !== '' ? sel : undefined;
661
681
  }
682
+ /**
683
+ * Reasoning effort for host reviews. `undefined` **omits** the field so the host
684
+ * model configuration decides; the old hardcoded `'off'` was rejected by host
685
+ * validation for models whose advertised efforts exclude it (issue #8).
686
+ */
687
+ hostEffort() {
688
+ const effort = this.options.readClassifyEffort?.();
689
+ return typeof effort === 'string' && effort !== '' ? effort : undefined;
690
+ }
662
691
  livePermissive() {
663
692
  return this.options.readPermissive !== undefined
664
693
  ? this.options.readPermissive()
@@ -1422,7 +1451,7 @@ export class PermGateRuntime {
1422
1451
  }
1423
1452
  else if (useHost) {
1424
1453
  const selection = this.hostModel() ?? DEFAULT_HOST_MODEL;
1425
- risk = await classifyRiskWith((system, user) => completeViaHost(this.options.hostLlm, selection, system, user, cfg?.timeoutMs ?? 20_000), req);
1454
+ risk = await classifyRiskWith((system, user) => completeViaHost(this.options.hostLlm, selection, system, user, cfg?.timeoutMs ?? 20_000, { effort: this.hostEffort() }), req);
1426
1455
  }
1427
1456
  else {
1428
1457
  risk = await classifyRisk(cfg ?? {}, req);
@@ -1624,7 +1653,7 @@ export class PermGateRuntime {
1624
1653
  }
1625
1654
  else if (useHost) {
1626
1655
  const selection = this.hostModel() ?? DEFAULT_HOST_MODEL;
1627
- risk = await classifyRiskWith((system, user) => completeViaHost(this.options.hostLlm, selection, system, user, cfg?.timeoutMs ?? 20_000), req);
1656
+ risk = await classifyRiskWith((system, user) => completeViaHost(this.options.hostLlm, selection, system, user, cfg?.timeoutMs ?? 20_000, { effort: this.hostEffort() }), req);
1628
1657
  }
1629
1658
  else {
1630
1659
  risk = await classifyRisk(cfg ?? {}, req);
@@ -2010,7 +2039,7 @@ export class PermGateRuntime {
2010
2039
  if (this.options.hostLlm === undefined)
2011
2040
  return done(false, 'host llm service unavailable');
2012
2041
  const selection = this.hostModel() ?? DEFAULT_HOST_MODEL;
2013
- const r = await completeViaHost(this.options.hostLlm, selection, 'Reply with exactly: OK', 'ping', cfg?.timeoutMs ?? 20_000);
2042
+ const r = await completeViaHost(this.options.hostLlm, selection, 'Reply with exactly: OK', 'ping', cfg?.timeoutMs ?? 20_000, { effort: this.hostEffort() });
2014
2043
  return r.ok ? done(true, `${selection.provider}/${selection.model} → ${r.content.trim().slice(0, 60) || 'ok'}`) : done(false, r.error ?? 'host llm call failed');
2015
2044
  }
2016
2045
  if (cfg?.endpoint === undefined || cfg.endpoint === '' || cfg?.model === undefined || cfg.model === '') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-perm-gate",
3
- "version": "5.4.1",
3
+ "version": "5.4.3",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "tag": "latest"