@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
@@ -1,12 +1,136 @@
1
+ import { parsePermissionRule } from "../core/permission-rule-syntax.js";
2
+ import { HAND_TOOLS } from "../core/tool-catalog-entries.js";
3
+ export function readDeniedEntry(entry) {
4
+ if (typeof entry !== "string")
5
+ return { kind: "not_a_string", type: valueTypeLabel(entry) };
6
+ if (entry !== entry.trim())
7
+ return { kind: "name", name: entry, content: false };
8
+ const parsed = parsePermissionRule(entry);
9
+ return { kind: "name", name: parsed.toolName, content: parsed.ruleContent !== undefined };
10
+ }
11
+ export function valueTypeLabel(value) {
12
+ if (value === null)
13
+ return "null";
14
+ switch (typeof value) {
15
+ case "string":
16
+ return "a string";
17
+ case "undefined":
18
+ return "undefined";
19
+ case "number":
20
+ return "a number";
21
+ case "boolean":
22
+ return "a boolean";
23
+ case "bigint":
24
+ return "a bigint";
25
+ case "symbol":
26
+ return "a symbol";
27
+ case "function":
28
+ return "a function";
29
+ default:
30
+ try {
31
+ return Array.isArray(value) ? "an array" : "an object";
32
+ }
33
+ catch {
34
+ return "an object";
35
+ }
36
+ }
37
+ }
38
+ const isList = (value) => Array.isArray(value);
39
+ const UNREADABLE_LIST = Object.freeze({});
40
+ export function readToolNameList(value) {
41
+ if (value === UNREADABLE_LIST)
42
+ return { kind: "unreadable" };
43
+ let list;
44
+ try {
45
+ list = isList(value) ? value : undefined;
46
+ }
47
+ catch {
48
+ return { kind: "not_a_list", type: "an object" };
49
+ }
50
+ if (list === undefined)
51
+ return { kind: "not_a_list", type: valueTypeLabel(value) };
52
+ try {
53
+ const entries = [];
54
+ for (let i = 0; i < list.length; i++)
55
+ entries.push(list[i]);
56
+ return { kind: "entries", entries };
57
+ }
58
+ catch {
59
+ return { kind: "unreadable" };
60
+ }
61
+ }
62
+ export function snapshotToolNameList(value) {
63
+ const read = readToolNameList(value);
64
+ return read.kind === "entries" ? read.entries : read.kind === "unreadable" ? UNREADABLE_LIST : value;
65
+ }
66
+ const LIST_NOUN = { allowed: "allowed tools", denied: "denied tools" };
67
+ const LIST_NAME = { allowed: "an allowed-tools list", denied: "a denied-tools list" };
68
+ export function notAStringSentence(where, side, index, type) {
69
+ return `${where} declares in its ${LIST_NOUN[side]}, as entry ${index + 1}, a value that is ${type}, not a tool name — ${LIST_NAME[side]} holds tool names, each a string. Fix the entry.`;
70
+ }
71
+ export function notAListSentence(where, side, list) {
72
+ return list.kind === "not_a_list"
73
+ ? `${where} declares in its ${LIST_NOUN[side]} a value that is ${list.type}, not a list of tool names. Fix the list.`
74
+ : `${where} declares in its ${LIST_NOUN[side]} a list whose entries cannot be read (an accessor or a proxy trap threw). Fix the list.`;
75
+ }
76
+ export function toolNameListFlaw(where, side, value) {
77
+ if (value === undefined || value === null)
78
+ return undefined;
79
+ const list = readToolNameList(value);
80
+ if (list.kind !== "entries")
81
+ return notAListSentence(where, side, list);
82
+ for (let i = 0; i < list.entries.length; i++) {
83
+ const entry = list.entries[i];
84
+ if (typeof entry !== "string")
85
+ return notAStringSentence(where, side, i, valueTypeLabel(entry));
86
+ }
87
+ return undefined;
88
+ }
89
+ export function snapshotDefinitionLists(def) {
90
+ const copy = { ...def };
91
+ if (copy.allowTools !== undefined)
92
+ copy.allowTools = snapshotToolNameList(copy.allowTools);
93
+ if (copy.denyTools !== undefined)
94
+ copy.denyTools = snapshotToolNameList(copy.denyTools);
95
+ return copy;
96
+ }
97
+ export function allowedToolNames(allowTools) {
98
+ if (allowTools === undefined || allowTools === null)
99
+ return undefined;
100
+ const list = readToolNameList(allowTools);
101
+ if (list.kind !== "entries")
102
+ return new Set();
103
+ if (list.entries.includes("*"))
104
+ return undefined;
105
+ const names = new Set();
106
+ for (const entry of list.entries)
107
+ if (typeof entry === "string")
108
+ names.add(entry);
109
+ return names;
110
+ }
111
+ function deniedNames(denyTools) {
112
+ const names = new Set();
113
+ const list = readToolNameList(denyTools);
114
+ if (list.kind !== "entries")
115
+ return names;
116
+ for (const entry of list.entries) {
117
+ const read = readDeniedEntry(entry);
118
+ if (read.kind === "name")
119
+ names.add(read.name);
120
+ }
121
+ return names;
122
+ }
1
123
  export function toolNameAllowed(name, allowTools, denyTools) {
2
- const allow = allowTools !== undefined && !allowTools.includes("*") ? new Set(allowTools) : undefined;
3
- const deny = denyTools && denyTools.length > 0 ? new Set(denyTools) : undefined;
124
+ const allow = allowedToolNames(allowTools);
125
+ const denied = deniedNames(denyTools);
126
+ const deny = denied.size > 0 ? denied : undefined;
4
127
  const n = name;
5
128
  return (!allow || allow.has(n)) && (!deny || !deny.has(n));
6
129
  }
7
130
  export function resolveToolSubset(pool, allowTools, denyTools) {
8
- const allow = allowTools !== undefined && !allowTools.includes("*") ? new Set(allowTools) : undefined;
9
- const deny = denyTools && denyTools.length > 0 ? new Set(denyTools) : undefined;
131
+ const allow = allowedToolNames(allowTools);
132
+ const denied = deniedNames(denyTools);
133
+ const deny = denied.size > 0 ? denied : undefined;
10
134
  if (!allow && !deny)
11
135
  return [...pool];
12
136
  return pool.filter((t) => {
@@ -14,3 +138,9 @@ export function resolveToolSubset(pool, allowTools, denyTools) {
14
138
  return (!allow || matches(allow)) && (!deny || !matches(deny));
15
139
  });
16
140
  }
141
+ export function resolveAgentToolSubset(pool, allowTools, denyTools) {
142
+ const kept = new Set(resolveToolSubset([...pool, ...HAND_TOOLS], allowTools, denyTools));
143
+ const handed = pool.filter((t) => kept.has(t));
144
+ const mountedUnder = new Set(handed.map((t) => t.name));
145
+ return { handed, withheldHands: HAND_TOOLS.filter((t) => !kept.has(t) && !mountedUnder.has(t.name)).map((t) => t.name) };
146
+ }
@@ -0,0 +1,20 @@
1
+ /** The configuration directory's name below a home: the one segment the deny tables spell the agent's own settings under. */
2
+ export declare const AGENT_CONFIG_DIR_SEGMENT = ".sema";
3
+ /** The code a declared root that names no directory is refused with, wherever it is declared. */
4
+ export declare const AGENT_CONFIG_DIR_INVALID_CODE = "config.agent_config_dir_invalid";
5
+ /** A run (the `/`-separated segments of a deny row, home word already removed) read below the configuration directory:
6
+ * `.sema/settings.json` ⇒ `settings.json`, `.sema` ⇒ `""`, and `undefined` for a run that does not open with the
7
+ * directory's name. The first segment is compared as the row compares it (ASCII-case-insensitive unless the row opts out).
8
+ *
9
+ * @contract agent_config_dir.one_reading — which rows a declared configuration root relocates is decided HERE and nowhere
10
+ * else: a row that opens with the `.sema` segment names its tail below the root, in the read placement, in the always-on
11
+ * write-protection table and in the opt-in deny set alike; the root adds a place and never moves the default one, a row that
12
+ * does not open with that segment keeps its place, and a face with a list of its own of "the rows that relocate" is a fork
13
+ * of this sentence, not a variant of it. */
14
+ export declare function agentConfigDirTail(run: string, caseSensitive?: boolean): string | undefined;
15
+ /** The segments of a declared root in lexical normal form — what a path's leading segments are compared with. */
16
+ export declare function agentConfigDirSegments(dir: string): string[];
17
+ /** The declared configuration root as given (`undefined` = none declared, the default location), or a refusal naming the
18
+ * seat and the shape of the value: not a string, empty, not absolute, no such directory, not a directory, not readable.
19
+ * The directory is looked at on the host's own file system — the host that declares the root is the host that keeps it. */
20
+ export declare function checkedAgentConfigDir(value: string | undefined, seat: string): string | undefined;
@@ -0,0 +1,49 @@
1
+ import { statSync } from "node:fs";
2
+ import { isAbsolutePathForm, normalizeAbsPathLexically } from "../tools/fs/safety.js";
3
+ export const AGENT_CONFIG_DIR_SEGMENT = ".sema";
4
+ export const AGENT_CONFIG_DIR_INVALID_CODE = "config.agent_config_dir_invalid";
5
+ export function agentConfigDirTail(run, caseSensitive = false) {
6
+ const [first, ...rest] = run.split("/").filter((segment) => segment.length > 0);
7
+ if (first === undefined)
8
+ return undefined;
9
+ const folded = caseSensitive ? first : first.replace(/[A-Z]/g, (c) => c.toLowerCase());
10
+ return folded === AGENT_CONFIG_DIR_SEGMENT ? rest.join("/") : undefined;
11
+ }
12
+ export function agentConfigDirSegments(dir) {
13
+ return normalizeAbsPathLexically(dir).split(/[\\/]/).filter((segment) => segment.length > 0);
14
+ }
15
+ function shown(value) {
16
+ try {
17
+ return JSON.stringify(value) ?? String(value);
18
+ }
19
+ catch {
20
+ return typeof value;
21
+ }
22
+ }
23
+ function invalidAgentConfigDir(seat, value, why) {
24
+ const e = new Error(`${seat} ${shown(value)} ${why} — it must be the absolute path of an existing directory (the host's own configuration root); ` +
25
+ `a value that names no directory would guard nothing, so it is refused rather than read as the default \`~/${AGENT_CONFIG_DIR_SEGMENT}\` location.`);
26
+ e.code = AGENT_CONFIG_DIR_INVALID_CODE;
27
+ return e;
28
+ }
29
+ export function checkedAgentConfigDir(value, seat) {
30
+ if (value === undefined)
31
+ return undefined;
32
+ if (typeof value !== "string")
33
+ throw invalidAgentConfigDir(seat, value, `is a ${value === null ? "null" : typeof value}, not a string`);
34
+ if (value.length === 0)
35
+ throw invalidAgentConfigDir(seat, value, "is an empty string");
36
+ if (!isAbsolutePathForm(value))
37
+ throw invalidAgentConfigDir(seat, value, "is not an absolute path");
38
+ let isDirectory;
39
+ try {
40
+ isDirectory = statSync(value).isDirectory();
41
+ }
42
+ catch (e) {
43
+ const code = typeof e === "object" && e !== null && "code" in e && typeof e.code === "string" ? e.code : "unreadable";
44
+ throw invalidAgentConfigDir(seat, value, code === "ENOENT" || code === "ENOTDIR" ? "names no existing directory" : `could not be examined (${code})`);
45
+ }
46
+ if (!isDirectory)
47
+ throw invalidAgentConfigDir(seat, value, "names a file, not a directory");
48
+ return value;
49
+ }
@@ -33,7 +33,7 @@ export interface AgentDefinition {
33
33
  * the delegation tool's name cannot re-delegate (CC Explore/Plan `disallowedTools` include Agent,
34
34
  * pretty.js:409189/486948). */
35
35
  allowTools?: string[];
36
- /** Tool denylist (a deny always wins). See {@link allowTools} for the per-agent delegation-tool note. */
36
+ /** Tool denylist (a deny always wins), tool NAMES: `Tool(content)` hides the whole `Tool`. See {@link allowTools} for the per-agent delegation-tool note. */
37
37
  denyTools?: string[];
38
38
  /**
39
39
  * F7 (CC 2.1.198 parity, per-agent `skills` — 锚 pretty.js:422126 `e.skills ?? []`, GA/ungated in 198:
@@ -116,7 +116,7 @@ export interface SkillSpec {
116
116
  * either list asks once (`Execute skill: <name>`, the card listing what it releases); a skill the person invoked by
117
117
  * name ({@link import("./task-spec.js").TaskSpec.userInvokedSkills}) grants without asking. The grant holds for that
118
118
  * run (a steer it takes in does not end it) and the next runTask starts without it. An entry this grammar reads as nothing — `*` (it voids an allow list; on a deny list it is inert and the rest of the
119
- * list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`permission.allowed_tool_not_granted`).
119
+ * list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`permission.allowed_tool_not_granted`). A table whose value is not a list (a string, an object…) reads as one such entry; `null` is no table.
120
120
  */
121
121
  allowedTools?: string[];
122
122
  /** The tools refused for the rest of the run this skill is activated in — the frontmatter `disallowed-tools` list,
@@ -1,19 +1,35 @@
1
1
  import { type RuleToolNameVerdict } from "./tool-name-aliases.js";
2
2
  import { type ToolCallRequest } from "./tool-policy.js";
3
3
  import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js";
4
- import { type PersistedRule } from "./permission-rule-model.js";
4
+ import { type PathRuleBases, type PersistedRule } from "./permission-rule-model.js";
5
5
  import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
6
+ import { type ToolNameListReading } from "../agents/tool-filter.js";
6
7
  import { type EngineNotice } from "./engine-notice.js";
7
8
  /** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. `tool_name`: the
8
9
  * entry's tool name is one the name judgment reads as stale against the run's roster (`verdict` says which); `near_miss`: the
9
10
  * run mounts no tool of that exact name but mounts one that differs only by case (`currentName` is its spelling). */
10
11
  export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"];
11
12
  export type AllowEntryInertReason = (typeof ALLOW_ENTRY_INERT_REASONS)[number];
12
- /** The facts beside a `tool_name` / `near_miss` reason: the judgment's word, and the spelling to write instead when there is one. */
13
+ /** The facts beside a reason: for `tool_name` / `near_miss` the judgment's word and the spelling to write instead when there is
14
+ * one; for a list member that is not a string (`grammar`) the TYPE of the value (`a number`, `null`, `an object`…) — `entry` then
15
+ * holds {@link nonStringMemberLabel}, never the value. */
13
16
  export interface AllowEntryInertDetail {
14
17
  readonly verdict?: Exclude<RuleToolNameVerdict["kind"], "current">;
15
18
  readonly currentName?: string;
19
+ readonly valueType?: string;
16
20
  }
21
+ /**
22
+ * How a list member that is not a string is named wherever a list's entries are named (the inert record's `entry`, the Skill
23
+ * card's binding): its TYPE, in a spelling no readable entry can have — a readable entry's name holds no whitespace and a
24
+ * name-only entry holds no parenthesis, and this label holds a space and none. It can equal only another entry that reads as
25
+ * nothing, so a binding that holds it never stands for an entry that grants or refuses.
26
+ */
27
+ export declare function nonStringMemberLabel(value: unknown): string;
28
+ /** How a tool-name list's VALUE that is not a list stands as one entry (the same kind of spelling as {@link nonStringMemberLabel}: it
29
+ * reads as `grammar`): its TYPE, or that its entries could not be read — never the value. */
30
+ export declare function nonListValueLabel(read: Exclude<ToolNameListReading, {
31
+ kind: "entries";
32
+ }>): string;
17
33
  /** One entry a list reads as nothing. */
18
34
  export type AllowEntryInert = {
19
35
  readonly entry: string;
@@ -78,12 +94,14 @@ export interface AllowEntryRow {
78
94
  readonly entry: string;
79
95
  readonly compiled: CompiledAllowEntry;
80
96
  }
81
- /** The call facts a content row is judged against — the persisted lane's own bases. */
97
+ /** The facts a list's content row is judged under: the leg's root and session (which rows apply), the live working
98
+ * directory (where a command's `cd` lands), and the ANCHORS of the list's path rows — the leg the list was installed
99
+ * on (its root and environment home): a path row of a list never moves with a `cd`. */
82
100
  export interface AllowEntryJudgeContext {
83
101
  readonly root: string | undefined;
84
102
  readonly sessionId: string | undefined;
85
103
  readonly liveCwd: () => string | undefined;
86
- readonly home: string | undefined;
104
+ readonly anchors: PathRuleBases;
87
105
  }
88
106
  /**
89
107
  * The ONE list matcher, both directions — the rows that speak for the call, or undefined. ALLOW: the first name row that
@@ -189,6 +207,7 @@ declare const SHADOW_REASON: {
189
207
  readonly content_question: "the call is the engine's own question to a person";
190
208
  readonly hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person";
191
209
  readonly ancestor: "the task that delegated this one owns this question";
210
+ readonly dangerous_delete: "this removal targets a protected location (the filesystem root, a top-level directory, a home directory, or a workspace directory or one of its ancestors) — it is cleared by confirming this call, never by an allow rule";
192
211
  readonly unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule";
193
212
  readonly transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule";
194
213
  readonly org_rule: "an organization policy rule requires approval for this call";
@@ -244,6 +263,13 @@ export type AllowEntryInertNote = ({
244
263
  } & AllowEntryInert) | ({
245
264
  readonly source: AllowEntrySource;
246
265
  } & AllowEntryInert);
266
+ /**
267
+ * THE ONE WRITER of a list entry into a sentence (the start-of-run notice, a skill's refusal and its decisions, a sub-agent
268
+ * list's refusals and advisories): the entry as a JSON string — quotes, backslashes and the usual escapes — with every
269
+ * character that prints as nothing, or as a space, written as `\uXXXX` (`\u{…}` beyond the BMP) and a run of spaces kept
270
+ * space by space, so what a person reads is what the engine read. Capped on what is printed.
271
+ */
272
+ export declare function renderListEntry(entry: string): string;
247
273
  /**
248
274
  * One entry that reads as nothing, as a person reads it: the entry as written (quoted, so a surrounding space shows) and why
249
275
  * — the reason word, with the spelling to write when the reading has one. The one phrasing the start-of-run notice and a
@@ -6,15 +6,23 @@ import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
6
6
  import { ALLOW_RULE_MEMBERS } from "./allow-rule-shadow.js";
7
7
  import { COMMAND_RULE_TOOL, READ_RULE_TOOL, findAdmittingRule, laneToolNamesOf, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
8
8
  import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
9
+ import { callTargetReadingOf } from "./effective-path-target.js";
9
10
  import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
10
11
  import { mandateDisclosureOf } from "./ask-origin.js";
11
12
  import { inlineUntrusted } from "./untrusted-text.js";
13
+ import { valueTypeLabel } from "../agents/tool-filter.js";
12
14
  import { deliverEngineNotice } from "./engine-notice.js";
13
15
  export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"]);
16
+ export function nonStringMemberLabel(value) {
17
+ return `<not a string: ${valueTypeLabel(value)}>`;
18
+ }
19
+ export function nonListValueLabel(read) {
20
+ return read.kind === "not_a_list" ? `<not a list: ${read.type}>` : "<a list whose entries cannot be read>";
21
+ }
14
22
  function ruleRowOf(parsed) {
15
23
  return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "list", counter: 1 }, origin: "user", createdAt: "" }] };
16
24
  }
17
- const TOOL_NAME_SPELLING = /^[^\s()]+$/;
25
+ const TOOL_NAME_SPELLING = /^[^\s()\p{Cc}\p{Cf}]+$/u;
18
26
  function mountedSpellingOf(name, roster) {
19
27
  const folded = name.toLowerCase();
20
28
  for (const mounted of roster)
@@ -36,6 +44,8 @@ function toolNameReading(name, roster) {
36
44
  function readAllowEntry(raw, behavior, roster) {
37
45
  const inert = (reason) => ({ inert: reason });
38
46
  const row = (compiled) => ({ compiled });
47
+ if (typeof raw !== "string")
48
+ return { inert: "grammar", detail: { valueType: valueTypeLabel(raw) } };
39
49
  if (raw !== raw.trim())
40
50
  return inert("grammar");
41
51
  const parsed = parsePermissionRule(raw);
@@ -94,11 +104,11 @@ export function compileAllowList(entries, behavior, roster) {
94
104
  const rows = [];
95
105
  const inert = [];
96
106
  for (const entry of entries) {
97
- const read = voided ? { inert: "wildcard_all" } : readAllowEntry(entry, behavior, roster);
107
+ const read = typeof entry !== "string" || !voided ? readAllowEntry(entry, behavior, roster) : { inert: "wildcard_all" };
98
108
  if ("compiled" in read)
99
109
  rows.push({ entry, compiled: read.compiled });
100
110
  else
101
- inert.push({ entry, reason: read.inert, ...read.detail });
111
+ inert.push({ entry: typeof entry === "string" ? entry : nonStringMemberLabel(entry), reason: read.inert, ...read.detail });
102
112
  }
103
113
  return { rows, inert };
104
114
  }
@@ -153,6 +163,7 @@ export function autoStripped(compiled, call) {
153
163
  export function autoStrippedRule(rule, call) {
154
164
  return rule.behavior === "allow" && rule.tool === COMMAND_RULE_TOOL && autoStripped({ kind: "command", rule }, call);
155
165
  }
166
+ const NO_LEADS = new Map();
156
167
  function nameRowReaches(compiled, req) {
157
168
  const covers = compiled.kind === "namespace" ? (n) => namespacedRuleNameCovers(compiled.pattern, n) : compiled.kind === "tool" || compiled.kind === "shell" ? (n) => n === compiled.name : undefined;
158
169
  return covers !== undefined && answersTo(req, covers);
@@ -194,7 +205,7 @@ export function judgeAllowEntries(rows, req, behavior, ctx) {
194
205
  if (pathRows.length > 0) {
195
206
  const table = pathRows.map((r) => r.compiled.rule);
196
207
  for (const name of laneToolNamesOf(req).filter((n) => ruleToolGrammarOf(n) === "path")) {
197
- const answer = pathRuleLaneAnswer(table, { ...req, toolName: name }, { root: ctx.root, sessionId: ctx.sessionId, liveCwd, home: ctx.home });
208
+ const answer = pathRuleLaneAnswer(table, { ...req, toolName: name }, { root: ctx.root, sessionId: ctx.sessionId, anchorsOf: () => ctx.anchors, leads: callTargetReadingOf(req)?.leads ?? NO_LEADS });
198
209
  const spoke = answer !== undefined && ("unreadable" in answer ? behavior === "deny" : "behavior" in answer && answer.behavior === behavior);
199
210
  if (spoke) {
200
211
  const rule = answer !== undefined && "rules" in answer ? answer.rules[0]?.rule : undefined;
@@ -307,6 +318,7 @@ const SHADOW_REASON = {
307
318
  content_question: "the call is the engine's own question to a person",
308
319
  hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person",
309
320
  ancestor: "the task that delegated this one owns this question",
321
+ dangerous_delete: "this removal targets a protected location (the filesystem root, a top-level directory, a home directory, or a workspace directory or one of its ancestors) — it is cleared by confirming this call, never by an allow rule",
310
322
  unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule",
311
323
  transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule",
312
324
  org_rule: "an organization policy rule requires approval for this call",
@@ -366,7 +378,22 @@ export function cardDisclosureOf(decision, fallbackMessage) {
366
378
  }
367
379
  export const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
368
380
  const NOTICE_ENTRIES_SHOWN = 5;
381
+ const UNSEEN_IN_ENTRY = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Zs}]/gu;
382
+ const ENTRY_RENDER_MAX = 122;
383
+ function escapedCodePoint(ch) {
384
+ const cp = ch.codePointAt(0) ?? 0;
385
+ const hex = cp.toString(16).padStart(4, "0");
386
+ return cp > 0xffff ? `\\u{${hex}}` : `\\u${hex}`;
387
+ }
388
+ export function renderListEntry(entry) {
389
+ const quoted = JSON.stringify(entry)
390
+ .replace(/ {2,}/g, (run) => "\\u0020".repeat(run.length))
391
+ .replace(UNSEEN_IN_ENTRY, (ch) => (ch === " " ? ch : escapedCodePoint(ch)));
392
+ return inlineUntrusted(quoted, ENTRY_RENDER_MAX);
393
+ }
369
394
  export function inertEntryPhrase(e) {
395
+ if (e.valueType !== undefined)
396
+ return `a value that is ${e.valueType} (grammar: not a string)`;
370
397
  const spelling = e.currentName !== undefined ? inlineUntrusted(e.currentName, 120) : undefined;
371
398
  const why = (() => {
372
399
  if (e.reason === "near_miss")
@@ -386,7 +413,7 @@ export function inertEntryPhrase(e) {
386
413
  }
387
414
  }
388
415
  })();
389
- return `${inlineUntrusted(JSON.stringify(e.entry), 122)} (${why})`;
416
+ return `${renderListEntry(e.entry)} (${why})`;
390
417
  }
391
418
  export function allowedToolsNotGrantedNotice(inert, sessionId) {
392
419
  const where = (e) => (e.source === "skill" ? `skill ${inlineUntrusted(e.skill, 80)} ${e.list}` : e.source === "request" ? "this run's allowedTools" : "the deployment's allowedTools");
@@ -25,6 +25,7 @@
25
25
  * the ask). Both stay where they are and are consumed beside the origin, not folded into it.
26
26
  */
27
27
  import type { PermissionResult } from "./tool-policy.js";
28
+ import type { ContainmentFace } from "../tools/fs/containment-face.js";
28
29
  /**
29
30
  * The closed set, in AUTHORITY order — the order {@link askOriginOf} resolves a fold in when several
30
31
  * facts hold on one call (a hook ask folded over a policy ask, an org stamp over either). The order
@@ -448,8 +449,12 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
448
449
  * `classifier_no_ruling` is a classifier station's question to a person because the auto-mode classification did not rule
449
450
  * (`classifierNoRuling` — it could not run, or replied with no verdict): the denial-limit fallback's row (only a person
450
451
  * answers it; the classifier is never asked again).
452
+ * `dangerous_delete` is a removal whose target is a protected location (`dangerousRemovalOf`: the filesystem root, a
453
+ * top-level directory, a home directory, or a workspace directory — the task root, the working directory, an additional
454
+ * working directory — or an ancestor of one): the reference implementation's dangerous-removal breaker, which no mode,
455
+ * allow rule or classifier answers. `unverifiable_delete` is its sibling for a target the command text cannot resolve.
451
456
  */
452
- export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "classifier_no_ruling", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
457
+ export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "dangerous_delete", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "classifier_no_ruling", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
453
458
  export type AskClass = (typeof ASK_CLASSES)[number];
454
459
  /** Membership test for {@link AskClass} — the word crosses the wire on `tool_end.gate.disposition`. */
455
460
  export declare function isAskClass(v: unknown): v is AskClass;
@@ -457,7 +462,8 @@ export declare function isAskClass(v: unknown): v is AskClass;
457
462
  export interface ModeArbitrationRow {
458
463
  /** The ask still reaches a person under `bypassPermissions` (the reference implementation's `bypassImmune`). */
459
464
  readonly bypassImmune: boolean;
460
- /** The auto-mode classifier may answer it. */
465
+ /** The auto-mode classifier may answer it — as the class stands on its own: the folded row of an ask
466
+ * ({@link foldArbitrationRow}) also takes the read face the ask was raised under. */
461
467
  readonly classifierEligible: boolean;
462
468
  /** The sandbox-admission leg may admit it (containment holds the effect). */
463
469
  readonly sandboxAdmission: boolean;
@@ -473,6 +479,8 @@ export interface ModeArbitrationRow {
473
479
  * sandbox columns are the fold of the two former per-word lane tables (only `probe` moved: the classifier may
474
480
  * now answer it — the read boundary's unestablishable spelling is fed to it as a deny-table row instead), and
475
481
  * `ruleMayClear` restates {@link ruleClearanceOf}'s conclusion per class (a real-approval bit is never rule-clearable).
482
+ * `probe`'s `classifierEligible` is the OPEN read face's reading: a narrowed (`roots`) face withholds the classifier from the class,
483
+ * folded where the ask's row is ({@link foldArbitrationRow}).
476
484
  * `mode` is the permission mode's OWN question (`plan`'s two sentences, `decisionReason: "mode"`): the reference
477
485
  * implementation puts it to a person past every allow rule and never to an automatic lane, and its bypass conversion
478
486
  * releases it like any other non-immune ask. `unstated` is the reference implementation's passthrough conversion: an allow
@@ -503,6 +511,13 @@ export declare const MODE_ARBITRATION: {
503
511
  readonly ruleMayClear: false;
504
512
  readonly hookAskFloor: false;
505
513
  };
514
+ readonly dangerous_delete: {
515
+ readonly bypassImmune: true;
516
+ readonly classifierEligible: false;
517
+ readonly sandboxAdmission: false;
518
+ readonly ruleMayClear: false;
519
+ readonly hookAskFloor: true;
520
+ };
506
521
  readonly unverifiable_delete: {
507
522
  readonly bypassImmune: true;
508
523
  readonly classifierEligible: true;
@@ -683,6 +698,8 @@ export interface AskClassFacts extends AskOriginFacts {
683
698
  readonly toolMarkSources?: ReadonlySet<ToolMarkSource>;
684
699
  readonly a2aIsolatePeers?: boolean;
685
700
  readonly ancestorClasses?: ReadonlySet<AskClass>;
701
+ /** The read face the leg this ask was raised on resolved ({@link ContainmentFace}); absent ⇒ none resolved (a leg with no hands). */
702
+ readonly readFace?: ContainmentFace;
686
703
  }
687
704
  /** The classification the arbitration reads: the whole set, the disclosed word (the set's first member in
688
705
  * {@link ASK_CLASSES} order) and the row folded across the set. */
@@ -718,8 +735,10 @@ export declare function decisionAskClassesOf(d: Extract<PermissionResult, {
718
735
  }>): Set<AskClass>;
719
736
  /** Fold a class set onto one row: `bypassImmune` and `hookAskFloor` by OR (any immune member keeps the ask; any floored
720
737
  * member floors a hook's ask), the three permissive switches by AND (one refusing member withholds the lane). An empty
721
- * set is `ordinary`. */
722
- export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>): ModeArbitrationRow;
738
+ * set is `ordinary`. `readFace` is the face the ask was raised under: `roots` takes the classifier's lane from a set that
739
+ * holds a class the narrowed face withholds from it — the one place the face enters, so every reader of `classifierEligible`
740
+ * reads it already folded. */
741
+ export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>, readFace?: ContainmentFace): ModeArbitrationRow;
723
742
  /**
724
743
  * The ONE class derivation: the decision half ({@link decisionAskClassesOf}) joined with the gate's own facts —
725
744
  * the question tool, an ancestor's mark, the org layer's two arms, the persisted lane's unreadable call, the tool
@@ -117,7 +117,7 @@ const MANDATE_DISCLOSURE = {
117
117
  write_protection_unresolved: "the write-protection judge could not resolve this write's target, so whether it lands on the protected table is unknown — it is cleared by confirming this call: no standing rule and no automatic decider answers for it, though a wired approver seat can",
118
118
  };
119
119
  export function mandateDisclosureOf(mandate) {
120
- return mandate === undefined ? "an explicit ask rule matched this call (a person's ask-me-each-time outranks a standing allow rule)" : MANDATE_DISCLOSURE[mandate];
120
+ return mandate === undefined ? "an explicit ask rule matched this call (a person's or the deployment's ask-me-each-time outranks a standing allow rule)" : MANDATE_DISCLOSURE[mandate];
121
121
  }
122
122
  export function persistedRuleMandateOf(marks) {
123
123
  const word = irreversibilityMandateOf(marks);
@@ -170,6 +170,7 @@ export const ASK_CLASSES = Object.freeze([
170
170
  "content_question",
171
171
  "hook",
172
172
  "ancestor",
173
+ "dangerous_delete",
173
174
  "unverifiable_delete",
174
175
  "transcript_write",
175
176
  "org_rule",
@@ -200,6 +201,7 @@ export const MODE_ARBITRATION = {
200
201
  content_question: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
201
202
  hook: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
202
203
  ancestor: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
204
+ dangerous_delete: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
203
205
  unverifiable_delete: { bypassImmune: true, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
204
206
  transcript_write: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
205
207
  org_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
@@ -222,7 +224,8 @@ export const MODE_ARBITRATION = {
222
224
  unstated: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false },
223
225
  ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
224
226
  };
225
- export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
227
+ const CLASSIFIER_WITHHELD_ON_ROOTS = new Set(["probe"]);
228
+ export const RRA_EXPLAINERS = new Set(["dangerous_delete", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
226
229
  export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
227
230
  export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable"]);
228
231
  const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
@@ -243,6 +246,8 @@ export function screenAskClassMembers(askClass, classes) {
243
246
  }
244
247
  export function decisionAskClassesOf(d) {
245
248
  const out = new Set();
249
+ if (d.guard === "dangerous_delete")
250
+ out.add("dangerous_delete");
246
251
  if (d.guard === "unverifiable_delete")
247
252
  out.add("unverifiable_delete");
248
253
  if (d.guard === "transcript_write")
@@ -273,10 +278,11 @@ export function decisionAskClassesOf(d) {
273
278
  out.add(c);
274
279
  return out;
275
280
  }
276
- export function foldArbitrationRow(classes) {
281
+ export function foldArbitrationRow(classes, readFace) {
277
282
  if (classes.size === 0)
278
283
  return MODE_ARBITRATION.ordinary;
279
- let bypassImmune = false, classifierEligible = true, sandboxAdmission = true, ruleMayClear = true, hookAskFloor = false;
284
+ const withheld = readFace === "roots" && [...classes].some((c) => CLASSIFIER_WITHHELD_ON_ROOTS.has(c));
285
+ let bypassImmune = false, classifierEligible = !withheld, sandboxAdmission = true, ruleMayClear = true, hookAskFloor = false;
280
286
  for (const c of classes) {
281
287
  const row = MODE_ARBITRATION[c];
282
288
  bypassImmune ||= row.bypassImmune;
@@ -334,7 +340,7 @@ export function classifyAsk(d, facts) {
334
340
  if (classes.size === 0)
335
341
  classes.add("ordinary");
336
342
  const askClass = ASK_CLASSES.find((c) => classes.has(c));
337
- return { classes, askClass, row: foldArbitrationRow(classes) };
343
+ return { classes, askClass, row: foldArbitrationRow(classes, facts.readFace) };
338
344
  }
339
345
  export function headlessDenialMessage(toolName, askMessage) {
340
346
  return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (nobody can be asked to approve "${toolName}" in this run — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;