@sema-agent/core 7.44.0 → 7.46.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 (159) hide show
  1. package/CHANGELOG.md +418 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +65 -38
  6. package/dist/agents/tool-filter.d.ts +82 -5
  7. package/dist/agents/tool-filter.js +134 -4
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/agent-definition.d.ts +2 -2
  11. package/dist/core/allow-entry.d.ts +30 -4
  12. package/dist/core/allow-entry.js +32 -5
  13. package/dist/core/ask-origin.d.ts +23 -4
  14. package/dist/core/ask-origin.js +11 -5
  15. package/dist/core/checkpoint-store.d.ts +41 -4
  16. package/dist/core/decision-receipt.js +1 -1
  17. package/dist/core/effective-path-target.d.ts +143 -10
  18. package/dist/core/effective-path-target.js +113 -9
  19. package/dist/core/engine-notice.d.ts +25 -3
  20. package/dist/core/engine-notice.js +12 -6
  21. package/dist/core/gate-fold.js +3 -0
  22. package/dist/core/gate-lanes.js +11 -3
  23. package/dist/core/gate-outcome.d.ts +5 -0
  24. package/dist/core/gate-outcome.js +2 -0
  25. package/dist/core/governance-codes.d.ts +3 -1
  26. package/dist/core/governance-codes.js +3 -0
  27. package/dist/core/hooks.d.ts +5 -0
  28. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  29. package/dist/core/memory-engine/content-origin.js +9 -4
  30. package/dist/core/memory-engine/layout.d.ts +5 -1
  31. package/dist/core/memory-engine/layout.js +1 -0
  32. package/dist/core/path-rule-binding.d.ts +65 -0
  33. package/dist/core/path-rule-binding.js +99 -0
  34. package/dist/core/permission-rule-model.d.ts +47 -18
  35. package/dist/core/permission-rule-model.js +38 -42
  36. package/dist/core/permission-rule-provider.d.ts +7 -0
  37. package/dist/core/permission-rule-provider.js +10 -3
  38. package/dist/core/permission-rule-store.d.ts +8 -0
  39. package/dist/core/permission-rule-store.js +6 -2
  40. package/dist/core/permission-rule-sync.js +2 -2
  41. package/dist/core/permission-rules.d.ts +22 -36
  42. package/dist/core/permission-rules.js +50 -40
  43. package/dist/core/real-approval.d.ts +9 -6
  44. package/dist/core/real-approval.js +5 -5
  45. package/dist/core/runner/child-tree.d.ts +110 -0
  46. package/dist/core/runner/child-tree.js +107 -0
  47. package/dist/core/runner/construction-doors.d.ts +5 -0
  48. package/dist/core/runner/construction-doors.js +6 -0
  49. package/dist/core/runner/contracts.d.ts +54 -2
  50. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  51. package/dist/core/runner/permission-rule-lanes.js +18 -12
  52. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  53. package/dist/core/runner/persisted-path-rows.js +31 -12
  54. package/dist/core/runner/prepare-ask-lane.d.ts +6 -0
  55. package/dist/core/runner/prepare-ask-lane.js +4 -2
  56. package/dist/core/runner/prepare-config-doors.js +3 -1
  57. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  58. package/dist/core/runner/prepare-execution-env.js +11 -3
  59. package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
  60. package/dist/core/runner/prepare-gate-stations.js +3 -1
  61. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  62. package/dist/core/runner/prepare-hands-readface.js +6 -6
  63. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  64. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  65. package/dist/core/runner/prepare-listings.d.ts +4 -4
  66. package/dist/core/runner/prepare-listings.js +2 -22
  67. package/dist/core/runner/prepare-park-ask.d.ts +5 -0
  68. package/dist/core/runner/prepare-park-ask.js +6 -4
  69. package/dist/core/runner/prepare-policy-chain.d.ts +16 -2
  70. package/dist/core/runner/prepare-policy-chain.js +14 -5
  71. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  72. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  73. package/dist/core/runner/prepare-task.js +41 -18
  74. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  75. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  76. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  77. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  78. package/dist/core/runner/resume-apply.js +17 -3
  79. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  80. package/dist/core/runner/resume-policy-outcome.js +2 -1
  81. package/dist/core/runner/run-leg.js +5 -1
  82. package/dist/core/runner/run-notification-lane.js +2 -2
  83. package/dist/core/runner/runtask.d.ts +2 -0
  84. package/dist/core/runner/runtask.js +4 -2
  85. package/dist/core/runner/stream-steer-verb.js +2 -2
  86. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  87. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  88. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  89. package/dist/core/runner/synthetic-tools.js +3 -2
  90. package/dist/core/runner/write-protection-station.d.ts +3 -2
  91. package/dist/core/runner/write-protection-station.js +1 -0
  92. package/dist/core/runner-deps.d.ts +14 -0
  93. package/dist/core/sensitive-path-policy.d.ts +8 -0
  94. package/dist/core/sensitive-path-policy.js +18 -2
  95. package/dist/core/session-policy-store.d.ts +6 -2
  96. package/dist/core/session-policy-store.js +2 -1
  97. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  98. package/dist/core/shell-wrapper-projection.js +56 -0
  99. package/dist/core/shell-wrapper-table.d.ts +144 -34
  100. package/dist/core/shell-wrapper-table.js +40 -20
  101. package/dist/core/skill-grant.d.ts +10 -2
  102. package/dist/core/skill-grant.js +20 -4
  103. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  104. package/dist/core/synthetic-read-verdict.js +3 -1
  105. package/dist/core/task-notification.d.ts +12 -1
  106. package/dist/core/task-notification.js +13 -1
  107. package/dist/core/task-spec.d.ts +5 -3
  108. package/dist/core/tool-catalog-entries.d.ts +21 -0
  109. package/dist/core/tool-catalog-entries.js +9 -0
  110. package/dist/core/tool-policy.d.ts +45 -12
  111. package/dist/core/tool-policy.js +18 -20
  112. package/dist/core/tool-spec.d.ts +23 -4
  113. package/dist/core/types.d.ts +1 -1
  114. package/dist/core/types.js +1 -1
  115. package/dist/core/write-protect.d.ts +4 -0
  116. package/dist/core/write-protect.js +30 -9
  117. package/dist/index.d.ts +10 -6
  118. package/dist/index.js +9 -5
  119. package/dist/orchestration/run-spec.js +8 -7
  120. package/dist/orchestration/workflow.js +3 -2
  121. package/dist/shell/lex.d.ts +7 -0
  122. package/dist/shell/lex.js +17 -0
  123. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  124. package/dist/tools/fs/bash-program-position.js +33 -69
  125. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  126. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  127. package/dist/tools/fs/index.d.ts +3 -2
  128. package/dist/tools/fs/program-read-model.d.ts +694 -0
  129. package/dist/tools/fs/program-read-model.js +1252 -0
  130. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  131. package/dist/tools/fs/program-read-model.rows.js +613 -0
  132. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  133. package/dist/tools/fs/read-deny-home.js +62 -24
  134. package/dist/tools/fs/read-deny.d.ts +54 -14
  135. package/dist/tools/fs/read-deny.js +104 -50
  136. package/dist/tools/fs/removal-targets.d.ts +83 -0
  137. package/dist/tools/fs/removal-targets.js +236 -0
  138. package/dist/tools/fs/safety.d.ts +3 -0
  139. package/dist/tools/fs/safety.js +10 -3
  140. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  141. package/dist/tools/fs/shell-read-argv.js +386 -0
  142. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  143. package/dist/tools/fs/shell-read-bindings.js +746 -0
  144. package/dist/tools/fs/shell-read-check.js +23 -15
  145. package/dist/tools/fs/shell-read-establish.js +79 -5
  146. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  147. package/dist/tools/fs/shell-read-judge.js +9 -1
  148. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  149. package/dist/tools/fs/shell-read-plan.js +299 -393
  150. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  151. package/dist/tools/fs/shell-read-word.js +30 -38
  152. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -37
  153. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  154. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  155. package/dist/tools/fs/shell-reentry-text.js +90 -53
  156. package/package.json +1 -1
  157. package/test/export-surface.snapshot.json +49 -3
  158. package/wire-changes/core@7.45.0.json +1084 -0
  159. package/wire-changes/core@7.46.0.json +1522 -0
@@ -6,6 +6,7 @@ import { inlineUntrusted } from "./untrusted-text.js";
6
6
  import { pathTargetOf, protectivePathTargetOf } from "./tool-registry.js";
7
7
  import { heldValues } from "./tool-name-readings.js";
8
8
  import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
9
+ import { bindPathRules, pathRowsReach, rowsWhereTheyLead } from "./path-rule-binding.js";
9
10
  import { readShellCommand } from "./shell-lexer.js";
10
11
  import { readOnlyLineObjection, readOnlyShellVerdict } from "./read-only-shell.js";
11
12
  export const RULE_BEHAVIORS = Object.freeze(["deny", "ask", "allow"]);
@@ -68,7 +69,7 @@ export const BARE_INTERPRETER_NAMES = new Set([
68
69
  "bash", "sh", "zsh", "ksh", "csh", "tcsh", "dash", "fish", "cmd", "powershell", "pwsh", "busybox",
69
70
  "source", ".", "trap", "eval", "exec", "command", "builtin",
70
71
  "enable", "compgen", "complete", "bind", "mapfile", "readarray",
71
- "env", "noglob", "xargs",
72
+ "env", "noglob", "nocorrect", "xargs",
72
73
  "sudo", "doas", "pkexec", "su", "runuser", "ssh", "chroot", "setpriv",
73
74
  "nohup", "nice", "ionice", "chrt", "taskset", "stdbuf", "timeout", "time", "watch",
74
75
  "setsid", "flock", "unshare", "nsenter", "script", "numactl", "prlimit", "systemd-run", "parallel",
@@ -440,8 +441,8 @@ function baseValueOf(base, bases) {
440
441
  }
441
442
  }
442
443
  }
443
- function resolvePathPattern(pattern, bases, behavior) {
444
- const family = pathFamilyOf(bases);
444
+ function resolvePathPattern(pattern, bases, behavior, treeFamily = pathFamilyOf(bases)) {
445
+ const family = treeFamily;
445
446
  const tree = bases.root ?? bases.cwd;
446
447
  if (pattern.startsWith("//")) {
447
448
  const abs = pathIdentityOf("/" + pattern.slice(2), family, tree);
@@ -503,15 +504,15 @@ function globSegmentsReach(globs, target) {
503
504
  };
504
505
  return walk(0, 0);
505
506
  }
506
- export function pathRuleReachOf(rule, target, bases) {
507
+ export function pathRuleReachOf(rule, target, bases, treeFamily = pathFamilyOf(bases)) {
507
508
  if (rule.match === "subpath")
508
- return directoryRuleAdmits(rule, target, bases) ? REACHED : NOT_REACHED;
509
+ return directoryRuleAdmits(rule, target, bases, treeFamily) ? REACHED : NOT_REACHED;
509
510
  if (rule.match !== "path")
510
511
  return NOT_REACHED;
511
- const family = pathFamilyOf(bases);
512
+ const family = treeFamily;
512
513
  if (!isPathIdentity(target, family))
513
514
  return NOT_REACHED;
514
- const r = resolvePathPattern(rule.command, bases, rule.behavior);
515
+ const r = resolvePathPattern(rule.command, bases, rule.behavior, family);
515
516
  if ("missingBase" in r) {
516
517
  return { reach: "unreadable", reason: `the rule is relative to ${PATH_RULE_BASE_LABEL[r.missingBase]} and this call supplies no absolute \`${r.missingBase}\` base of this tree's family to resolve it against` };
517
518
  }
@@ -706,10 +707,10 @@ function isLexicalNormalAbsoluteDir(path) {
706
707
  const segments = path.split("/").slice(1);
707
708
  return segments.length > 0 && segments.every((s) => s !== "" && s !== "." && s !== "..");
708
709
  }
709
- export function directoryRuleAdmits(rule, path, bases) {
710
+ export function directoryRuleAdmits(rule, path, bases, treeFamily = pathFamilyOf(bases)) {
710
711
  if (rule.match !== "subpath")
711
712
  return false;
712
- const family = pathFamilyOf(bases);
713
+ const family = treeFamily;
713
714
  if (carriesDotSegment(rule.command, family))
714
715
  return false;
715
716
  const dir = pathIdentityOf(rule.command, family, bases.root ?? bases.cwd);
@@ -820,21 +821,29 @@ function resolveCdSegmentDirectories(segments, cwd) {
820
821
  return dir === undefined || isShellRootedSpellingUnmapped(pathFamilyOf(bases), dir) ? undefined : directoryIdentityOf(dir, bases);
821
822
  });
822
823
  }
823
- export function scopeCoversCwd(scope, cwd, sessionId) {
824
+ export function scopeCoversCwd(scope, cwd, sessionId, identity) {
824
825
  if (scope.kind === "global")
825
826
  return true;
826
827
  if (scope.kind === "session")
827
828
  return sessionId !== undefined && sessionId === scope.sessionId;
828
- return cwd !== undefined && pathWithinRoot(cwd, scope.root);
829
+ if (cwd === undefined)
830
+ return false;
831
+ if (pathWithinRoot(cwd, scope.root))
832
+ return true;
833
+ return identity !== undefined && pathWithinRoot(identity.canonicalRoot, identity.canonicalOf(scope.root) ?? scope.root);
829
834
  }
830
835
  export function isRuleLive(rule) {
831
836
  return rule.adds.length > 0;
832
837
  }
833
838
  export function eligibleContext(rule, call) {
834
- return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId);
839
+ return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId, call.scopeIdentity);
835
840
  }
836
841
  export function eligiblePersisted(rule, call, behavior) {
837
- return rule.behavior === behavior && isRuleLive(rule) && eligibleContext(rule, call);
842
+ return rule.behavior === behavior && isRuleLive(rule) && eligibleContext(rule, behavior === "allow" ? spelledScope(call) : call);
843
+ }
844
+ export function spelledScope(call) {
845
+ const { scopeIdentity: _identity, ...spelled } = call;
846
+ return spelled;
838
847
  }
839
848
  function firstEligibleAdmittingRule(rules, commandText, call) {
840
849
  for (const rule of rules) {
@@ -911,41 +920,28 @@ export function adjudicatePersistedRules(rules, call) {
911
920
  }
912
921
  return undefined;
913
922
  }
914
- export function adjudicatePersistedPathRules(rules, call, targets, bases) {
923
+ export function adjudicatePersistedPathRules(rules, call, views, anchorsOf, leads) {
915
924
  const speaks = (rule, behavior) => eligiblePersisted(rule, call, behavior) ||
916
925
  (behavior === "ask" && call.face !== undefined && PATH_RULE_FACES[rule.tool]?.ask?.includes(call.face) === true && eligiblePersisted(rule, { ...call, tool: rule.tool }, behavior));
926
+ const bind = (rows) => bindPathRules(rows.map((rule) => ({ parsed: rule, ruleText: rule.rule, rule })), (s) => anchorsOf(s.rule));
917
927
  for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
918
928
  if (behavior === "allow") {
919
- if (targets.allow?.kind !== "path" || call.tool !== READ_RULE_TOOL)
929
+ if (views.allow === undefined || call.tool !== READ_RULE_TOOL)
920
930
  continue;
921
- for (const rule of rules) {
922
- if (!eligiblePersisted(rule, call, behavior))
923
- continue;
924
- if (directoryRuleAdmits(rule, targets.allow.identity, bases))
925
- return { behavior, rules: [rule] };
926
- }
931
+ const hit = pathRowsReach(bind(rules.filter((rule) => rule.match === "subpath" && eligiblePersisted(rule, call, behavior))), views.allow, "allow");
932
+ if (hit !== undefined)
933
+ return { behavior, rules: [hit.row.spec.rule] };
927
934
  continue;
928
935
  }
929
- const tighten = targets.tighten;
930
- if (tighten === undefined || tighten.kind === "refused")
936
+ if (views.tighten === undefined)
931
937
  continue;
932
- let unreadable;
933
- for (const rule of rules) {
934
- if (!speaks(rule, behavior))
935
- continue;
936
- if (tighten.kind === "unreadable") {
937
- if (rule.match === "path" || rule.match === "subpath")
938
- unreadable ??= { behavior: "ask", rules: [rule], unreadable: tighten.reason };
939
- continue;
940
- }
941
- const outcome = pathRuleReachOf(rule, tighten.identity, bases);
942
- if (outcome.reach === "reached")
943
- return { behavior, rules: [rule] };
944
- if (outcome.reach === "unreadable")
945
- unreadable ??= { behavior: "ask", rules: [rule], unreadable: outcome.reason };
946
- }
947
- if (unreadable !== undefined)
948
- return unreadable;
938
+ const rows = rules.filter((rule) => (rule.match === "path" || rule.match === "subpath") && speaks(rule, behavior));
939
+ if (rows.length === 0)
940
+ continue;
941
+ const hit = pathRowsReach(rowsWhereTheyLead(bind(rows), leads), views.tighten, "tighten");
942
+ if (hit === undefined)
943
+ continue;
944
+ return hit.unreadable !== undefined ? { behavior: "ask", rules: [hit.row.spec.rule], unreadable: hit.unreadable } : { behavior, rules: [hit.row.spec.rule] };
949
945
  }
950
946
  return undefined;
951
947
  }
@@ -987,9 +983,9 @@ export function segmentCoverageOf(command, rules, call) {
987
983
  (dir !== undefined && firstEligibleDirectoryRule(rules.persisted, dir, call) !== undefined) ||
988
984
  proposed.some((p) => p.rule.tool === "Read"
989
985
  ? dir !== undefined &&
990
- eligibleContext({ tool: p.rule.tool, scope: p.scope }, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }) &&
986
+ eligibleContext({ tool: p.rule.tool, scope: p.scope }, { ...spelledScope(call), tool: "Read" }) &&
991
987
  directoryRuleAdmits(p.rule, dir, basesOfCwd(call.execCwd ?? call.cwd))
992
- : eligibleContext({ tool: p.rule.tool, scope: p.scope }, call) && ruleAdmitsCommand(p.rule, segment)),
988
+ : eligibleContext({ tool: p.rule.tool, scope: p.scope }, spelledScope(call)) && ruleAdmitsCommand(p.rule, segment)),
993
989
  };
994
990
  });
995
991
  }
@@ -86,6 +86,13 @@ export interface EffectivePermissionRules {
86
86
  * served that are not canonical session rows of this session). The org partition's lines ride
87
87
  * `org.disclosures`. */
88
88
  disclosures: string[];
89
+ /** The durable rows left out because their project scope root is not a complete absolute path (a foreign writer's or a
90
+ * hand edit's row): each row's text and its scope root as served (a non-string root as its type). Absent when none. The
91
+ * reader tells the person (`permission.rule_row_dropped`). */
92
+ droppedRows?: ReadonlyArray<{
93
+ readonly rule: string;
94
+ readonly scopeRoot: string;
95
+ }>;
89
96
  }
90
97
  /**
91
98
  * The host-visible store, already anchored to one owner. One read verb, which never throws: each
@@ -1,5 +1,5 @@
1
1
  import { escapeForDisclosure } from "./permission-rule-model.js";
2
- import { PERMISSION_RULE_WRITER, errText, normalizePersistedRule, sameRuleIdentity, writerOf, } from "./permission-rule-store.js";
2
+ import { PERMISSION_RULE_WRITER, errText, isValidDurableScope, normalizePersistedRule, sameRuleIdentity, writerOf, } from "./permission-rule-store.js";
3
3
  import { UNGOVERNED_ORG_RESOLUTION, compileOrgShadowPredicate, createOrgRulePartition } from "./permission-rule-org.js";
4
4
  export function ruleSourceOf(scope) {
5
5
  return scope.kind === "global" ? "user" : scope.kind;
@@ -80,7 +80,11 @@ class UnifiedPermissionRuleStore {
80
80
  const sessionRead = "unreadable" in sessionRows ? [] : sessionRows;
81
81
  const shadowed = compileOrgShadowPredicate(orgResolution.rules);
82
82
  const rules = [];
83
- for (const r of [...sessionRead, ...durableRead.rules]) {
83
+ const illegal = durableRead.rules.filter((r) => !isValidDurableScope(r.scope));
84
+ const droppedRows = illegal.map((r) => ({ rule: String(r.rule), scopeRoot: scopeRootAsServed(r.scope.root) }));
85
+ if (illegal.length > 0)
86
+ disclosures.push(`the persisted rule partition served ${illegal.length} row(s) whose project scope root is not a complete absolute path — dropped, not adjudicated: ${illegal.slice(0, 3).map((r) => `${escapeForDisclosure(r.rule)} (${escapeForDisclosure(r.scope.root)})`).join(", ")}`);
87
+ for (const r of [...sessionRead, ...durableRead.rules.filter((x) => !illegal.includes(x))]) {
84
88
  rules.push({ ...r, source: ruleSourceOf(r.scope), status: r.behavior === "allow" && shadowed(r) ? "shadowed-by-org" : "live" });
85
89
  }
86
90
  const removed = [];
@@ -93,7 +97,7 @@ class UnifiedPermissionRuleStore {
93
97
  continue;
94
98
  removed.push({ behavior: t.behavior, rule: t.rule, scope: t.scope, source: ruleSourceOf(t.scope), status: "removed" });
95
99
  }
96
- return { rules, removed, org: orgResolution, rev: durableRead.rev, disclosures, ...failed };
100
+ return { rules, removed, org: orgResolution, rev: durableRead.rev, disclosures, ...failed, ...(droppedRows.length > 0 ? { droppedRows } : {}) };
97
101
  }
98
102
  async readSessionRows(sessionId, disclosures) {
99
103
  if (this.session === undefined || sessionId === undefined)
@@ -116,6 +120,9 @@ class UnifiedPermissionRuleStore {
116
120
  }
117
121
  }
118
122
  }
123
+ function scopeRootAsServed(root) {
124
+ return typeof root === "string" ? root : `(${root === null ? "null" : typeof root})`;
125
+ }
119
126
  function routingWriter(durable, session) {
120
127
  return {
121
128
  nextDot: () => durable.nextDot(),
@@ -457,6 +457,14 @@ export declare function isValidConsentScope(scope: RuleScope): boolean;
457
457
  * duty (design/382 §10): every such entrance, present or future, carries this face.
458
458
  */
459
459
  export declare function isValidDurableScope(scope: RuleScope): boolean;
460
+ /**
461
+ * A PROJECT scope's root is a complete absolute path — `/…`, a drive-rooted `X:\…` / `X:/…`, a UNC `\host\…` — the one
462
+ * predicate every scope face reads (the consent face, the durable face, the sync wire's pick). It is what a project row's
463
+ * path patterns anchor at (its `/x` and relative words), so a root that is not one (`C:`, a relative spelling, empty)
464
+ * would let the row apply (containment is a prefix test) while its anchor reads nothing: such a row is refused at every
465
+ * write, dropped at every read, and disclosed.
466
+ */
467
+ export declare function isProjectScopeRoot(root: unknown): root is string;
460
468
  /** Is `dot` at or below the frontier? Absent actor ⇒ not below (a frontier never covers what it never saw). */
461
469
  export declare function dotAtOrBelowFrontier(dot: RuleDot, frontier: RuleSyncFrontier): boolean;
462
470
  /**
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { canonicalize } from "./canonical-json.js";
3
3
  import { escapeForDisclosure, isRuleBehavior, isRuleLive, parseRuleText } from "./permission-rule-model.js";
4
4
  import { thrownText } from "./thrown-value.js";
5
+ import { isAbsolutePathForm } from "../tools/fs/safety.js";
5
6
  export function sameRuleOwner(a, b) {
6
7
  return a.kind === "principal" ? b.kind === "principal" && a.principal === b.principal : b.kind === "local-owner";
7
8
  }
@@ -143,12 +144,15 @@ export function assertWriteDeltaScopeDurable(delta) {
143
144
  export function isValidConsentScope(scope) {
144
145
  const sc = scope;
145
146
  return (sc?.kind === "global" ||
146
- (sc?.kind === "project" && typeof sc.root === "string" && sc.root !== "") ||
147
+ (sc?.kind === "project" && isProjectScopeRoot(sc.root)) ||
147
148
  (sc?.kind === "session" && typeof sc.sessionId === "string" && sc.sessionId !== ""));
148
149
  }
149
150
  export function isValidDurableScope(scope) {
150
151
  const sc = scope;
151
- return sc?.kind === "global" || (sc?.kind === "project" && typeof sc.root === "string" && sc.root !== "");
152
+ return sc?.kind === "global" || (sc?.kind === "project" && isProjectScopeRoot(sc.root));
153
+ }
154
+ export function isProjectScopeRoot(root) {
155
+ return typeof root === "string" && isAbsolutePathForm(root);
152
156
  }
153
157
  function isValidDot(dot) {
154
158
  const d = dot;
@@ -1,5 +1,5 @@
1
1
  import { escapeForDisclosure, isRuleBehavior, parseRuleText } from "./permission-rule-model.js";
2
- import { applyTombstones, errText, joinFrontiers, joinRuleStates, normalizePersistedRule, readRuleWriteOutcome, ruleSyncVector, sameRuleIdentity, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
2
+ import { applyTombstones, errText, isProjectScopeRoot, joinFrontiers, joinRuleStates, normalizePersistedRule, readRuleWriteOutcome, ruleSyncVector, sameRuleIdentity, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
3
3
  import { emitTrace } from "./trace.js";
4
4
  export const PERMISSION_RULE_SYNC_PATH = "/v1/permission-rules/sync";
5
5
  export const LOCAL_OWNER_UNSYNCABLE_CODE = "permission_rules.local_owner_unsyncable";
@@ -262,7 +262,7 @@ function pickScopeCaptured(v) {
262
262
  return { kind, scope: { kind: "global" } };
263
263
  if (kind === "project") {
264
264
  const root = s?.root;
265
- return { kind, scope: typeof root === "string" && root !== "" ? { kind: "project", root } : undefined };
265
+ return { kind, scope: isProjectScopeRoot(root) ? { kind: "project", root } : undefined };
266
266
  }
267
267
  if (kind === "session") {
268
268
  const sessionId = s?.sessionId;
@@ -30,11 +30,13 @@
30
30
  * `Edit(~/.sema/settings.json)` / `Write(src/**)` / `Read(//etc/**)` deny or ask compiles through the same
31
31
  * validator (the model's path grammar — the `//abs-dir/**` directory form and upstream's pattern form:
32
32
  * `//` absolute, `~/` home, `/` project root, otherwise working-directory relative; `*` within a segment,
33
- * `**` across) and is judged with `pathRuleReaches` on the call's PROTECTIVE path target (design/388: the
34
- * object face's declaration, the catalog's for the name as the floor). The bases a pattern resolves
35
- * against are the deployment's (`pathBases`); a pattern whose base the policy was not given is refused at
36
- * compile time (`unsupported.path_base`) rather than left to reach nothing at run time — a deny that
37
- * silently matches nothing is the failure form this module's header calls the most dangerous one.
33
+ * `**` across) and is judged on the call's PROTECTIVE path target (design/388: the object face's declaration, the
34
+ * catalog's for the name as the floor), every view of it (`callTargetViewsOf`), by the one judgment every path rule
35
+ * meets (`pathRowsReach`). A pattern's ANCHORS are the rule's own fact, taken once where the policy is installed: the
36
+ * declared `pathBases` word by word, else the leg the Runner installs the policy on (`policyAnchors`) — never the
37
+ * call's live working directory. A declared base that is not a usable absolute path is refused at compile time
38
+ * (`unsupported.path_base`); a policy called outside a Runner leg with no declared base reads such a rule
39
+ * `unreadable` (the fail-closed ask).
38
40
  * A `Read(...)` / `Edit(...)` deny or ask reaches the calls on its behavior's FACES (`PATH_RULE_FACES`), not only
39
41
  * its own tool: a `Read` ask asks for a Grep / Glob rooted in its path, judged on that call's own target.
40
42
  *
@@ -53,8 +55,7 @@
53
55
  */
54
56
  import type { NamedToolPolicy } from "./tool-policy.js";
55
57
  import { parsePermissionRule, type ParsedPermissionRule } from "./permission-rule-syntax.js";
56
- import { type ParsedRule, type PathRuleBases } from "./permission-rule-model.js";
57
- import type { DenyFace, PathRuleDenyRow } from "../tools/fs/read-deny.js";
58
+ import { type PathRuleBases } from "./permission-rule-model.js";
58
59
  export { parsePermissionRule, type ParsedPermissionRule };
59
60
  /** [K-PLATFORM-SWEEP] MINOR#11: Bash's REAL non-primary arg names, accepted as generic param rules
60
61
  * (`Bash(timeout:...)`). Exported SOLELY so a lockstep test can assert it against the live Bash tool
@@ -93,9 +94,10 @@ export interface PermissionRuleIssue {
93
94
  * is an object/array (a rule on it can never match); the tool is unknown to the resolver a leg
94
95
  * supplied (absent resolver ⇒ the catalog alone judges and an unknown tool stays permissive). */
95
96
  | "unsupported.param_unknown" | "unsupported.param_non_scalar" | "unsupported.tool_unknown"
96
- /** A path-form deny/ask whose spelling needs a base this policy was not given (`/x` needs
97
- * `pathBases.root`; a relative pattern needs `pathBases.cwd` or `root`) — refused rather than compiled
98
- * into a rule that reaches nothing. */
97
+ /** A path-form deny/ask whose spelling needs a base this policy DECLARES as something other than an absolute path
98
+ * of its tree's family (`/x` reads `pathBases.root`; a relative pattern `pathBases.cwd`, else `root`; `~/` `home`)
99
+ * — a bad declared value, refused rather than compiled into a rule that reaches nothing. An undeclared base is no
100
+ * refusal: the rule is anchored where the policy is installed. */
99
101
  | "unsupported.path_base" | "unsupported.mcp_paren"
100
102
  /** A parenthesised rule whose NAME is a covering spelling in a non-MCP protocol namespace
101
103
  * (`a2a__<peer>__*(x:y)`, `a2a__<peer>(x:y)`). The param DSL is keyed by EXACT tool name, and a
@@ -143,12 +145,17 @@ export interface PermissionRulePolicyOptions {
143
145
  */
144
146
  ruleFaces?: (toolName: string) => RuleFaceView | undefined;
145
147
  /**
146
- * The bases a path-form deny/ask pattern resolves against at match time ({@link PathRuleBases}): `root`
147
- * for `/…` (the task's project root), `cwd` for a relative pattern (the root stands in when absent — the
148
- * word's own meaning), `home` for `~/…` (the EXECUTION ENVIRONMENT's home — never the engine process's
149
- * own; #644). Facts of the DEPLOYMENT and the task, never of the rule — a pattern is stored as spelled.
150
- * A rule whose base is absent (or not an absolute path) here is refused at compile time
148
+ * The ANCHORS of this rule set's path patterns ({@link PathRuleBases}), word by word: `root` for `/…`, `cwd` for a
149
+ * relative pattern (the declared root stands in when absent — the word's own meaning), `home` for `~/…` (an
150
+ * execution environment's home — never the engine process's own; #644). A word declared here anchors the rules
151
+ * wherever the policy runs; a word left out is anchored where the Runner installs the policy — the root and the
152
+ * environment's home of the leg it enters the delegation chain on — and stays there for every child and every `cd`.
153
+ * A declared value that is not an absolute path of its tree's family is refused at compile time
151
154
  * (`unsupported.path_base`, naming the base — {@link ruleBasesNeeded} is the same classification).
155
+ * These are the RULES' anchors, never the call's: a call's target is read from the call (its target reading, else its
156
+ * own `root` / `cwd`); only a host calling the policy directly with a request that carries neither has its target
157
+ * resolved against `root` / `cwd` here. A target nothing locates is unreadable: every path rule able to reach the
158
+ * call answers it with the fail-closed ask (`ruleUnreadable`).
152
159
  */
153
160
  pathBases?: PathRuleBases;
154
161
  }
@@ -201,28 +208,7 @@ export interface RuleFaceView {
201
208
  export declare function catalogRuleFaceView(toolName: string): RuleFaceView | undefined;
202
209
  /** Exported for the lockstep guard only (the scalar vocabulary of a catalog tool) — not part of the rule DSL. */
203
210
  export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
204
- /**
205
- * The deny-table row of ONE face-keyed path deny rule, resolved against `bases` — the row's reach IS the rule's (the same
206
- * parsed rule, the same bases). Every source of such rows builds them here: a policy's compiled rules, and the person's
207
- * persisted store rows. The caller holds the bases the pattern needs (`ruleBasesNeeded`): a row whose base is missing
208
- * reads `unreadable`, which this row treats as a reach — a deny row that cannot be judged holds.
209
- */
210
- export declare function pathRuleDenyRowOf(rule: {
211
- readonly parsed: ParsedRule;
212
- readonly ruleText: string;
213
- readonly faces: readonly DenyFace[];
214
- }, bases: PathRuleBases): PathRuleDenyRow;
215
211
  /** Dry-run 校验/迁移报告:不 throw,返回全部 issues(含 `unsupported.*` 分类,
216
212
  * 供从 CC settings 迁移的部署逐条对照)。 */
217
213
  export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps" | "ruleFaces" | "pathBases">): PermissionRuleIssue[];
218
- /**
219
- * Compile declarative permission rules into ONE {@link ToolPolicy} (design/127). Evaluation order
220
- * inside the policy (CC `G1e` :595272 isomorph): bare deny → command deny → param deny → bare ask →
221
- * command ask → param ask → bare allow → `defaultAction` (the content-form command tiers are the
222
- * field-level tiers of upstream's arbiter, beside the generic param tiers). `check()` is idempotent and side-effect-free (combinePolicies 契约);
223
- * all parsing/validation happens HERE, the hot path never re-parses.
224
- *
225
- * ⚠️ This policy states no allow: a call no rule reaches gets `defaultAction`, whose `"allow"` is "no objection" (states
226
- * nothing, clears nothing). Express default-closed via `defaultAction`, never via a trailing always-ask policy.
227
- */
228
214
  export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): NamedToolPolicy;
@@ -2,10 +2,10 @@ import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
2
2
  import { splitNamespacedName } from "./tool-name-aliases.js";
3
3
  import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
4
4
  import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
5
- import { NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, pathRuleHeadOf, pathRuleReachOf, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
6
- import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/safety.js";
7
- import { effectivePathTargetOf } from "./effective-path-target.js";
8
- import { protectivePathTargetOf } from "./tool-registry.js";
5
+ import { NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
6
+ import { pathFamilyOf } from "../tools/fs/safety.js";
7
+ import { callTargetReadingOf, callTargetViewsOf } from "./effective-path-target.js";
8
+ import { bindPathRules, pathRowsReach, policyAnchors, rowsWhereTheyLead } from "./path-rule-binding.js";
9
9
  import { ruleDenialSentence } from "./rule-denial-sentence.js";
10
10
  import { heldValues } from "./tool-name-readings.js";
11
11
  export { parsePermissionRule };
@@ -93,22 +93,6 @@ export const fileToolParamVocabulary = (canonicalTool) => {
93
93
  const face = catalogRuleFaceOf(canonicalTool);
94
94
  return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
95
95
  };
96
- export function pathRuleDenyRowOf(rule, bases) {
97
- const family = pathFamilyOf(bases);
98
- const tree = bases.root ?? bases.cwd;
99
- const head = pathRuleHeadOf(rule.parsed, bases);
100
- return {
101
- rule: rule.ruleText,
102
- tool: rule.parsed.tool,
103
- pattern: parsePermissionRule(rule.ruleText).ruleContent ?? "",
104
- faces: rule.faces,
105
- reaches: (absolutePath) => {
106
- const id = pathIdentityOf(absolutePath, family, tree);
107
- return id.ok && pathRuleReachOf(rule.parsed, id.identity, bases).reach !== "not_reached";
108
- },
109
- ...(head !== undefined ? { head: spellingOfIdentity(head, family) } : {}),
110
- };
111
- }
112
96
  function countStars(s) {
113
97
  let n = 0;
114
98
  for (const c of s)
@@ -233,9 +217,9 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
233
217
  continue;
234
218
  }
235
219
  for (const base of ruleBasesNeeded(parsedPath.rule)) {
236
- const value = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
237
- if (!isUsablePathBase(value, pathFamilyOf(pathBases))) {
238
- bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and this policy was given no absolute \`pathBases.${base}\`${base === "cwd" ? " (or `root`)" : ""} to resolve it against — the rule could not be judged on any call`);
220
+ const declared = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
221
+ if (declared !== undefined && !isUsablePathBase(declared, pathFamilyOf(pathBases))) {
222
+ bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and the \`pathBases.${base === "cwd" && pathBases.cwd === undefined ? "root" : base}\` this policy declares is not an absolute path of its tree's family — the rule could not be judged on any call`);
239
223
  }
240
224
  }
241
225
  if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
@@ -315,6 +299,7 @@ function ruleDenial(req, ruleText, source) {
315
299
  function ruleAskMessage(ruleText, source) {
316
300
  return `flagged by permission rule "${ruleText}"${source ? ` (${source})` : ""}`;
317
301
  }
302
+ const NO_LEADS = new Map();
318
303
  export function createPermissionRulePolicy(rules, opts) {
319
304
  const caps = { ...DEFAULT_CAPS, ...opts?.caps };
320
305
  const pathBases = opts?.pathBases ?? {};
@@ -360,16 +345,55 @@ export function createPermissionRulePolicy(rules, opts) {
360
345
  deny: [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined)),
361
346
  ask: [...byTool.values()].flatMap((e) => e.content.ask.filter((r) => r.faces !== undefined)),
362
347
  };
363
- const pathDenyRules = faceRules.deny.map((r) => pathRuleDenyRowOf({ parsed: r.parsed, ruleText: r.ruleText, faces: r.faces }, pathBases));
364
348
  const contentLaneOf = (behavior, entry, callFace) => [
365
349
  ...(entry?.content[behavior].filter((r) => r.faces === undefined) ?? []),
366
350
  ...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
367
351
  ];
368
352
  const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
353
+ const pathRuleOf = new Map();
354
+ for (const e of byTool.values()) {
355
+ for (const behavior of ["deny", "ask"]) {
356
+ for (const r of e.content[behavior]) {
357
+ if (r.parsed.match !== "path" && r.parsed.match !== "subpath")
358
+ continue;
359
+ pathRuleOf.set(r, { parsed: r.parsed, ruleText: r.ruleText, ...(behavior === "deny" && r.faces !== undefined ? { faces: r.faces } : {}) });
360
+ }
361
+ }
362
+ }
363
+ const pathSpecs = [...pathRuleOf.values()];
364
+ const bound = new Map();
365
+ const boundAt = (anchors) => {
366
+ const key = JSON.stringify([anchors.root ?? null, anchors.cwd ?? null, anchors.home ?? null]);
367
+ const prior = bound.get(key);
368
+ if (prior !== undefined)
369
+ return prior;
370
+ const rows = bindPathRules(pathSpecs, anchors);
371
+ const of = new Map();
372
+ [...pathRuleOf.keys()].forEach((r, i) => of.set(r, rows[i]));
373
+ const out = { rows, of };
374
+ bound.set(key, out);
375
+ return out;
376
+ };
369
377
  const contentHit = (lane, req) => {
370
378
  if (lane.length === 0)
371
379
  return undefined;
372
- const target = effectivePathTargetOf(req, protectivePathTargetOf(req), pathBases);
380
+ const pathLane = lane.filter((r) => pathRuleOf.has(r));
381
+ if (pathLane.length > 0) {
382
+ const views = callTargetViewsOf(req, "tighten", { ...(pathBases.root !== undefined ? { root: pathBases.root } : {}), ...(pathBases.cwd !== undefined ? { cwd: pathBases.cwd } : {}) });
383
+ if (views !== undefined) {
384
+ const { of } = boundAt(policyAnchors(pathBases, req.pathRuleAnchors));
385
+ const rows = rowsWhereTheyLead(pathLane.map((r) => of.get(r)), callTargetReadingOf(req)?.leads ?? NO_LEADS);
386
+ const hit = pathRowsReach(rows, views, "tighten");
387
+ const rule = hit === undefined ? undefined : pathLane.find((r) => of.get(r).spec === hit.row.spec);
388
+ if (hit !== undefined && hit.unreadable === undefined)
389
+ return { rule: rule };
390
+ if (hit !== undefined)
391
+ return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req) ?? { rule: rule, unreadable: hit.unreadable };
392
+ }
393
+ }
394
+ return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req);
395
+ };
396
+ const judgeOthers = (lane, req) => {
373
397
  const command = isPlainObjectArgs(req.args) ? req.args[commandParam] : undefined;
374
398
  let unreadable;
375
399
  for (const r of lane) {
@@ -379,20 +403,6 @@ export function createPermissionRulePolicy(rules, opts) {
379
403
  return { rule: r };
380
404
  continue;
381
405
  }
382
- if (r.parsed.match === "subpath" || r.parsed.match === "path") {
383
- if (target === undefined || target.kind === "refused")
384
- continue;
385
- if (target.kind === "unreadable") {
386
- unreadable ??= { rule: r, unreadable: target.reason };
387
- continue;
388
- }
389
- const outcome = pathRuleReachOf(r.parsed, target.identity, pathBases);
390
- if (outcome.reach === "reached")
391
- return { rule: r };
392
- if (outcome.reach === "unreadable")
393
- unreadable ??= { rule: r, unreadable: outcome.reason };
394
- continue;
395
- }
396
406
  if (typeof command !== "string")
397
407
  continue;
398
408
  const outcome = programRunReachOf(r.parsed, command);
@@ -427,7 +437,7 @@ export function createPermissionRulePolicy(rules, opts) {
427
437
  };
428
438
  return {
429
439
  nameSets: [nameSets],
430
- ...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
440
+ ...(pathSpecs.length > 0 ? { pathRules: (anchors) => boundAt(policyAnchors(pathBases, anchors)).rows } : {}),
431
441
  check(req) {
432
442
  const entry = entryFor(req);
433
443
  const covering = hasCovering && protocolOf(req.toolName) !== undefined;
@@ -6,6 +6,7 @@
6
6
  * may still refuse it.
7
7
  */
8
8
  import type { Checkpoint } from "./checkpoint-store.js";
9
+ import type { HostDecider } from "./gate-outcome.js";
9
10
  import type { OrgGateVerdict } from "./hooks.js";
10
11
  import type { AskRequest, PermissionResult, ResolvedAsk } from "./tool-policy.js";
11
12
  /** The synchronous seat's refusal of a configuration's allow: one settlement word for both forms
@@ -16,10 +17,12 @@ export declare function unjudgedAllowRefused(req: AskRequest, by: "blanket" | "p
16
17
  /** A durable row demands a person when its gate carries the real-approval bit (the gate's own, which every resume
17
18
  * check reads; the pending action's `requiresRealApproval` is its display twin). */
18
19
  export declare function rowDemandsAPerson(cp: Checkpoint): boolean;
19
- /** A question a durable decide is held against, in each form a station holds one: the parked row, a policy's
20
- * re-check of an approver's edit, and the organization's CURRENT verdict about the call. */
21
- export type HeldQuestion = Checkpoint | PermissionResult | OrgGateVerdict;
22
- /** Whether a durable decide made by `decidedBy` cannot answer the held question: a question demanding a person is not
20
+ /** A question a decide is held against, in each form a station holds one: the parked row, a policy's re-check of an
21
+ * approver's edit, the organization's CURRENT verdict about the call, and the request an approver was handed (the card a
22
+ * live door answers). */
23
+ export type HeldQuestion = Checkpoint | PermissionResult | OrgGateVerdict | AskRequest;
24
+ /** Whether a decide made by `decidedBy` cannot answer the held question: a question demanding a person is not
23
25
  * the policy's to answer. One predicate for every station that holds a decide against such a question — the resume
24
- * entry (the row, an edit's re-check, the org's current verdict) and the execution belt (the last two, re-read). */
25
- export declare function policyCannotAnswer(question: HeldQuestion, decidedBy: "person" | "sla_timeout" | "policy"): boolean;
26
+ * entry (the row, an edit's re-check, the org's current verdict), the execution belt (the last two, re-read) and a live
27
+ * door answering the request an approver was handed. */
28
+ export declare function policyCannotAnswer(question: HeldQuestion, decidedBy: HostDecider): boolean;
@@ -16,11 +16,11 @@ export function rowDemandsAPerson(cp) {
16
16
  return cp.gate.kind === "irreversible_ask" && cp.gate.realApproval !== undefined;
17
17
  }
18
18
  function demandsAPerson(question) {
19
- if ("gate" in question)
20
- return rowDemandsAPerson(question);
21
- if ("action" in question)
22
- return question.action === "ask" && question.requiresRealApproval === true;
23
- return question.status === "available" && question.verdict?.behavior === "ask";
19
+ const gate = Reflect.get(question, "gate");
20
+ const verdict = Reflect.get(question, "verdict");
21
+ const row = typeof gate === "object" && gate !== null && rowDemandsAPerson(question);
22
+ const org = Reflect.get(question, "status") === "available" && typeof verdict === "object" && verdict !== null && Reflect.get(verdict, "behavior") === "ask";
23
+ return row || Reflect.get(question, "requiresRealApproval") === true || org;
24
24
  }
25
25
  export function policyCannotAnswer(question, decidedBy) {
26
26
  return decidedBy === "policy" && demandsAPerson(question);