@sema-agent/core 7.46.0 → 7.47.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 (57) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/core/allow-entry.d.ts +1 -1
  3. package/dist/core/allow-entry.js +3 -4
  4. package/dist/core/ask-origin.d.ts +35 -12
  5. package/dist/core/ask-origin.js +23 -6
  6. package/dist/core/engine-notice.d.ts +7 -4
  7. package/dist/core/gate-fold.js +2 -2
  8. package/dist/core/gate-lanes.js +2 -1
  9. package/dist/core/gate-outcome.d.ts +4 -2
  10. package/dist/core/gate-outcome.js +2 -2
  11. package/dist/core/permission-rule-model.d.ts +12 -6
  12. package/dist/core/permission-rule-model.js +10 -2
  13. package/dist/core/permission-rules.d.ts +4 -5
  14. package/dist/core/permission-rules.js +28 -15
  15. package/dist/core/runner/gate-exit.d.ts +4 -1
  16. package/dist/core/runner/gate-exit.js +5 -1
  17. package/dist/core/runner/prepare-hands-readface.js +5 -5
  18. package/dist/core/runner/prepare-policy-chain.js +2 -1
  19. package/dist/core/shell-wrapper-projection.js +1 -1
  20. package/dist/core/shell-wrapper-table.d.ts +6 -6
  21. package/dist/core/shell-wrapper-table.js +2 -2
  22. package/dist/core/tool-catalog-entries.js +3 -2
  23. package/dist/core/tool-face.d.ts +10 -1
  24. package/dist/core/tool-name-readings.d.ts +8 -0
  25. package/dist/core/tool-name-readings.js +2 -0
  26. package/dist/core/tool-policy.d.ts +10 -9
  27. package/dist/core/tool-policy.js +2 -2
  28. package/dist/core/tool-registry.d.ts +9 -0
  29. package/dist/core/tool-registry.js +10 -0
  30. package/dist/core/write-protect.d.ts +10 -3
  31. package/dist/core/write-protect.js +11 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/shell/lex.d.ts +15 -0
  35. package/dist/shell/lex.js +58 -13
  36. package/dist/tools/fs/bash-program-position.d.ts +9 -6
  37. package/dist/tools/fs/bash-program-position.js +13 -10
  38. package/dist/tools/fs/program-read-model.d.ts +24 -7
  39. package/dist/tools/fs/program-read-model.js +7 -0
  40. package/dist/tools/fs/program-read-model.rows.js +18 -7
  41. package/dist/tools/fs/removal-targets.js +14 -4
  42. package/dist/tools/fs/shell-read-argv.d.ts +5 -4
  43. package/dist/tools/fs/shell-read-argv.js +5 -3
  44. package/dist/tools/fs/shell-read-bindings.d.ts +46 -19
  45. package/dist/tools/fs/shell-read-bindings.js +38 -29
  46. package/dist/tools/fs/shell-read-establish.js +3 -3
  47. package/dist/tools/fs/shell-read-plan.d.ts +2 -1
  48. package/dist/tools/fs/shell-read-plan.js +32 -36
  49. package/dist/tools/fs/shell-read-word.d.ts +38 -7
  50. package/dist/tools/fs/shell-read-word.js +26 -8
  51. package/dist/tools/fs/shell-read-write-targets.js +4 -3
  52. package/dist/tools/fs/shell-reentry-text.d.ts +98 -0
  53. package/dist/tools/fs/shell-reentry-text.js +115 -2
  54. package/dist/tools/monitor.d.ts +3 -3
  55. package/package.json +1 -1
  56. package/test/export-surface.snapshot.json +9 -3
  57. package/wire-changes/core@7.47.0.json +412 -0
@@ -1,15 +1,14 @@
1
1
  import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
2
2
  import { splitNamespacedName } from "./tool-name-aliases.js";
3
- import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
3
+ import { catalogRuleFaceOf, isShellFaceToolName, 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, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
5
+ import { COMMAND_RULE_TOOL, NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, laneToolNamesOf, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
6
6
  import { pathFamilyOf } from "../tools/fs/safety.js";
7
7
  import { callTargetReadingOf, callTargetViewsOf } from "./effective-path-target.js";
8
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 };
12
- export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
13
12
  function describeBehavior(v) {
14
13
  switch (typeof v) {
15
14
  case "undefined": return "undefined";
@@ -110,13 +109,14 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
110
109
  return { face, judged: face !== undefined };
111
110
  };
112
111
  const byTool = new Map();
112
+ const command = { deny: [], ask: [] };
113
113
  const namespacedCovering = { deny: [], ask: [] };
114
114
  const issues = [];
115
115
  const bad = (rule, code, message) => {
116
116
  issues.push({ rule, code, message });
117
117
  };
118
118
  if (rules.length > caps.maxRules) {
119
- return { byTool, namespacedCovering, issues: [{ rule: "", code: "invalid.cap_exceeded", message: `${rules.length} rules > maxRules ${caps.maxRules}` }] };
119
+ return { byTool, command, namespacedCovering, issues: [{ rule: "", code: "invalid.cap_exceeded", message: `${rules.length} rules > maxRules ${caps.maxRules}` }] };
120
120
  }
121
121
  for (const r of rules) {
122
122
  const text = r.rule;
@@ -174,17 +174,24 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
174
174
  }
175
175
  const content = parsed.ruleContent;
176
176
  const colon = content.indexOf(":");
177
- if (toolName === "Bash") {
177
+ if (isShellFaceToolName(toolName)) {
178
178
  const paramPart = colon > 0 ? content.slice(0, colon).trim() : "";
179
- const looksLikeParamRule = colon > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(paramPart) && BASH_GENERIC_PARAMS.has(paramPart);
180
- if (!looksLikeParamRule || content.endsWith(":*")) {
179
+ const declaresParam = (tool) => colon > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(paramPart) && (catalogRuleFaceOf(tool)?.params ?? []).includes(paramPart);
180
+ const looksLikeParamRule = declaresParam(toolName);
181
+ if (toolName !== COMMAND_RULE_TOOL && !looksLikeParamRule) {
182
+ const commandWordParam = declaresParam(COMMAND_RULE_TOOL) && !content.endsWith(":*");
183
+ bad(text, "invalid.command_rule", commandWordParam
184
+ ? `"${text}" is a parameter rule on "${paramPart}", which ${toolName} does not declare (its rule parameters: ${(catalogRuleFaceOf(toolName)?.params ?? []).join(", ")}); a parameter rule reaches only the tool that declares the parameter, so ${COMMAND_RULE_TOOL}(${content}) is a rule on ${COMMAND_RULE_TOOL}'s own argument and does not reach ${toolName}. A shell command rule is written ${COMMAND_RULE_TOOL}(<command>) — it reaches ${toolName} and every other tool that runs a shell command by the command it runs.`
185
+ : `a shell command rule is written ${COMMAND_RULE_TOOL}(…) — it reaches ${toolName} and every other tool that runs a shell command by the command it runs; "${text}" names no rule upstream reads. Write ${COMMAND_RULE_TOOL}(${content}).`);
186
+ continue;
187
+ }
188
+ if (toolName === COMMAND_RULE_TOOL && (!looksLikeParamRule || content.endsWith(":*"))) {
181
189
  const parsedCommand = parseRuleText(text, verb);
182
190
  if ("reject" in parsedCommand) {
183
191
  bad(text, "invalid.command_rule", `${parsedCommand.reject.code}: ${parsedCommand.reject.message}`);
184
192
  continue;
185
193
  }
186
- entry.content[verb].push({ parsed: parsedCommand.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
187
- byTool.set(toolName, entry);
194
+ command[verb].push({ parsed: parsedCommand.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
188
195
  continue;
189
196
  }
190
197
  }
@@ -264,7 +271,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
264
271
  entry.param[verb].push({ param, pattern, behavior: verb, ruleText: text, source: r.source });
265
272
  byTool.set(toolName, entry);
266
273
  }
267
- return { byTool, namespacedCovering, issues };
274
+ return { byTool, command, namespacedCovering, issues };
268
275
  }
269
276
  export function validatePermissionRules(rules, opts) {
270
277
  const caps = { ...DEFAULT_CAPS, ...opts?.caps };
@@ -303,7 +310,7 @@ const NO_LEADS = new Map();
303
310
  export function createPermissionRulePolicy(rules, opts) {
304
311
  const caps = { ...DEFAULT_CAPS, ...opts?.caps };
305
312
  const pathBases = opts?.pathBases ?? {};
306
- const { byTool, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.ruleFaces, pathBases);
313
+ const { byTool, command, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.ruleFaces, pathBases);
307
314
  const setLevelCap = issues.some((i) => i.code === "invalid.cap_exceeded" && i.rule === "");
308
315
  if (issues.length > 0 && ((opts?.onInvalidRule ?? "throw") === "throw" || setLevelCap)) {
309
316
  const e = new Error(`createPermissionRulePolicy: ${issues.length} invalid/unsupported rule(s):\n` +
@@ -335,6 +342,9 @@ export function createPermissionRulePolicy(rules, opts) {
335
342
  if (entry.bare.ask !== undefined || entry.param.ask.length > 0 || entry.content.ask.length > 0)
336
343
  nameSets.ask.push(toolName);
337
344
  }
345
+ for (const behavior of ["deny", "ask"])
346
+ if (command[behavior].length > 0 && !nameSets[behavior].includes(COMMAND_RULE_TOOL))
347
+ nameSets[behavior].push(COMMAND_RULE_TOOL);
338
348
  for (const behavior of ["deny", "ask"]) {
339
349
  for (const r of namespacedCovering[behavior])
340
350
  nameSets[behavior].push(r.ruleName);
@@ -345,11 +355,13 @@ export function createPermissionRulePolicy(rules, opts) {
345
355
  deny: [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined)),
346
356
  ask: [...byTool.values()].flatMap((e) => e.content.ask.filter((r) => r.faces !== undefined)),
347
357
  };
348
- const contentLaneOf = (behavior, entry, callFace) => [
358
+ const contentLaneOf = (behavior, entry, shellCall, callFace) => [
349
359
  ...(entry?.content[behavior].filter((r) => r.faces === undefined) ?? []),
360
+ ...(shellCall ? command[behavior] : []),
350
361
  ...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
351
362
  ];
352
- const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
363
+ const hasCommandRules = command.deny.length > 0 || command.ask.length > 0;
364
+ const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
353
365
  const pathRuleOf = new Map();
354
366
  for (const e of byTool.values()) {
355
367
  for (const behavior of ["deny", "ask"]) {
@@ -450,7 +462,8 @@ export function createPermissionRulePolicy(rules, opts) {
450
462
  return { action: "deny", message: ruleDenial(req, coveringDeny.ruleText, coveringDeny.source) };
451
463
  }
452
464
  const callFace = faceRules.deny.length > 0 || faceRules.ask.length > 0 ? callPathFaceOf(req) : undefined;
453
- const denyLane = contentLaneOf("deny", entry, callFace);
465
+ const shellCall = hasCommandRules && laneToolNamesOf(req).includes(COMMAND_RULE_TOOL);
466
+ const denyLane = contentLaneOf("deny", entry, shellCall, callFace);
454
467
  const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
455
468
  if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
456
469
  return { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
@@ -473,7 +486,7 @@ export function createPermissionRulePolicy(rules, opts) {
473
486
  if (coveringAsk)
474
487
  return { action: "ask", message: ruleAskMessage(coveringAsk.ruleText, coveringAsk.source), matchedAskRule: coveringAsk.ruleText };
475
488
  }
476
- const askLane = contentLaneOf("ask", entry, callFace);
489
+ const askLane = contentLaneOf("ask", entry, shellCall, callFace);
477
490
  const contentAsk = askLane.length > 0 ? contentHit(askLane, req) : undefined;
478
491
  if (contentAsk !== undefined) {
479
492
  if (contentAsk.unreadable !== undefined)
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { type ClassifierDenyCause, type DeniedBy, type GateOutcome, type ModeRefusal, type ModeRelease, type Settlement } from "../gate-outcome.js";
25
25
  import type { AutoModeClassifierRound } from "../auto-mode.js";
26
- import { type Arbitration, type AskClassification, type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
26
+ import { type Arbitration, type AskClassification, type AskFace, type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
27
27
  import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
28
28
  import type { SafeNotifier } from "../safe-notify.js";
29
29
  import type { HookSeatOutcome, PermissionDeniedPayload, PreToolUseResult, ToolGateInput, ToolGateResult } from "../hooks.js";
@@ -143,6 +143,9 @@ export interface PreToolUseScreening {
143
143
  /** The model-facing text for an answer the decision receipt refused (an ENGINE fail-closed, not the face's deny). */
144
144
  refusedReason(answer: PreToolUseResult): string;
145
145
  }
146
+ /** The FACE a pass's questions are put on ({@link askFaceOf} of the call's wire name) — the one reading of it the
147
+ * write-protection tighten (the fold) and every class derivation (the lanes) make. */
148
+ export declare function passAskFace(pass: Pick<GatePass, "req">): AskFace;
146
149
  /**
147
150
  * ONE gate pass — the record the station groups (`runGateFold`, `runGateLanes`) and the exits read and write,
148
151
  * built once per call by `runToolGate` before the screening pass. What used to be the closure-captured `let`
@@ -1,5 +1,6 @@
1
1
  import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
2
- import { classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
2
+ import { askFaceOf, classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
3
+ import { wireOf } from "../tool-name-readings.js";
3
4
  import { isProtectedWrite } from "../tool-registry.js";
4
5
  import { askUnresolvableCauseOf, classifierDenyCauseOf, classifierRoundOf, decisionText, engineSettlementOf } from "../tool-policy.js";
5
6
  import { askUnresolvableRemedySeat } from "../ask-unresolvable-notice.js";
@@ -97,6 +98,9 @@ function withParkFailureCause(reason, parkFailed) {
97
98
  `(${inlineUntrusted(parkFailed, PARK_FAILURE_CAUSE_MAX)}), so the refusal above is what the fallback had ` +
98
99
  `left to say, not the reason the call stopped.`);
99
100
  }
101
+ export function passAskFace(pass) {
102
+ return askFaceOf(wireOf("gate.askFace", pass.req));
103
+ }
100
104
  export async function engineFailClosedExit(pass, reason) {
101
105
  const { input, toolName, toolCallId, notifyPermissionDenied } = pass;
102
106
  const gate = directDeny("hook");
@@ -8,7 +8,7 @@ import { placeReadDenyAtHome } from "../../tools/fs/read-deny-home.js";
8
8
  import { createShellOverflowSpoolFence } from "../../tools/fs/fs-shared.js";
9
9
  import { createShellReadStation } from "../../tools/fs/shell-read-check.js";
10
10
  import { composeReadChecks, createFileReadStation } from "../../tools/fs/file-read-check.js";
11
- import { readTargetToolNames } from "../tool-registry.js";
11
+ import { isShellFaceToolName, predictMountedToolNames, readTargetToolNames } from "../tool-registry.js";
12
12
  import { normalizeFileText } from "../../tools/fs/encoding.js";
13
13
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
14
14
  import { deliverEngineNotice } from "../types.js";
@@ -203,6 +203,7 @@ export async function prepareHandsMount(input) {
203
203
  const handsIncludeShell = !(executionEnv instanceof StubExecutionEnv);
204
204
  const handsReadOnly = spec.handsReadOnly === true;
205
205
  backgroundTaskToolsActive = handsIncludeShell && !handsReadOnly && hasBackgroundShell(executionEnv);
206
+ const shellFaceHere = [...predictMountedToolNames(new Set(backgroundTaskToolsActive ? ["hands", "backgroundTasks"] : ["hands"]))].filter(isShellFaceToolName);
206
207
  wsSnapshot = resume !== undefined || !(handsIncludeShell && !handsReadOnly) ? undefined : await session.getWorkspaceState().catch(() => undefined);
207
208
  rebaseWsPath = (p) => (wsSnapshot === undefined || wsSnapshot.taskRoot === rootCanonical ? p : rebaseWorkspacePath(p, wsSnapshot.taskRoot, rootCanonical));
208
209
  if (handsIncludeShell && !handsReadOnly) {
@@ -307,7 +308,7 @@ export async function prepareHandsMount(input) {
307
308
  if (handsIncludeShell) {
308
309
  const spoolFence = handsReadOnly ? createShellOverflowSpoolFence(executionEnv) : undefined;
309
310
  const readonlyBoundary = () => spoolFence !== undefined ? { roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical], cwd: rootCanonical, ...denySeat, ...writeDenySeat, alsoInside: spoolFence } : undefined;
310
- shellReadTools = new Set(handsReadOnly ? ["Bash"] : backgroundTaskToolsActive ? ["Bash", "Monitor"] : ["Bash"]);
311
+ shellReadTools = new Set(shellFaceHere);
311
312
  shellReadStation = createShellReadStation({
312
313
  env: executionEnv,
313
314
  tools: shellReadTools,
@@ -400,9 +401,8 @@ export async function prepareHandsMount(input) {
400
401
  if (seatOwned)
401
402
  shellGatedTools.add(name);
402
403
  };
403
- installShellSeat("Bash");
404
- if (backgroundTaskToolsActive)
405
- installShellSeat("Monitor");
404
+ for (const name of shellFaceHere)
405
+ installShellSeat(name);
406
406
  }
407
407
  }
408
408
  return {
@@ -14,7 +14,7 @@ import { emitTrace } from "../trace.js";
14
14
  import { skillGrantBindingMembers } from "../skill-grant.js";
15
15
  import { SKILL_TOOL_NAME } from "./synthetic-tools.js";
16
16
  import { autoStrippedRule, cardDisclosureOf } from "../allow-entry.js";
17
- import { arbitrateByMode, askClassMembersOf, askMandateMember } from "../ask-origin.js";
17
+ import { arbitrateByMode, askClassMembersOf, askFaceOf, askMandateMember } from "../ask-origin.js";
18
18
  import { createModeGatePolicy, mountedToolDeclaresReadOnly } from "../mode-policy.js";
19
19
  const ancestorFacts = (ask) => ({ ...(ask ?? {}), ancestorResolved: true });
20
20
  import { headlessDenyAtFold, headlessDenyAtRecheck, INHERITED_STATION_FACTS, inheritedAskCarry, inheritedAskFallbackMessage, judgeInheritedClassifier, lateAskSettlementObserver, resolveInheritedAsk, settleDenialLimitFallback } from "./denial-limit-arms.js";
@@ -313,6 +313,7 @@ export async function preparePolicyChain(input) {
313
313
  ...(seat.sources.size > 0 ? { toolMarkSources: seat.sources } : {}),
314
314
  ...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}),
315
315
  ...(face !== undefined ? { readFace: face } : {}),
316
+ askFace: askFaceOf(wireOf("inherited.askFace", call)),
316
317
  };
317
318
  };
318
319
  const inheritedAskEvidence = inheritedAskRuleEvidence(deps.permissionRuleStore);
@@ -30,7 +30,7 @@ const runsCommand = (spec) => spec.effect === "executes" || spec.value?.kind ===
30
30
  function projectRow(row) {
31
31
  const sorted = (xs) => [...new Set(xs)].sort();
32
32
  const all = spellings(row);
33
- const viaShell = row.reentry?.kind === "shell" || row.launcher?.shellText === true || row.body === "first-operand" || row.body === "assignment-values" || (row.operands?.rest?.kind === "program-text" && row.operands.rest.language === "arithmetic");
33
+ const viaShell = row.reentry?.kind === "shell" || row.launcher?.shellText !== undefined || row.body === "first-operand" || row.body === "assignment-values" || (row.operands?.rest?.kind === "program-text" && row.operands.rest.language === "arithmetic");
34
34
  if (row.launcher === undefined) {
35
35
  if (viaShell)
36
36
  return { valueFlags: [], flags: [], commandString: [], viaShell: true };
@@ -276,15 +276,15 @@ export declare const SHELL_WRAPPER_TABLE: {
276
276
  readonly commandString: readonly [];
277
277
  };
278
278
  readonly su: {
279
- readonly valueFlags: readonly [];
280
- readonly flags: readonly [];
281
- readonly commandString: readonly [];
279
+ readonly valueFlags: readonly ["--group", "--shell", "--supp-group", "--whitelist-environment", "-G", "-g", "-s", "-w"];
280
+ readonly flags: readonly ["--fast", "--help", "--login", "--no-pty", "--preserve-environment", "--pty", "--version", "-P", "-T", "-V", "-f", "-h", "-l", "-m", "-p"];
281
+ readonly commandString: readonly ["--command", "--session-command", "-c"];
282
282
  readonly viaShell: true;
283
283
  };
284
284
  readonly runuser: {
285
- readonly valueFlags: readonly [];
286
- readonly flags: readonly [];
287
- readonly commandString: readonly [];
285
+ readonly valueFlags: readonly ["--group", "--shell", "--supp-group", "--user", "--whitelist-environment", "-G", "-g", "-s", "-u", "-w"];
286
+ readonly flags: readonly ["--fast", "--help", "--login", "--no-pty", "--preserve-environment", "--pty", "--version", "-P", "-T", "-V", "-f", "-h", "-l", "-m", "-p"];
287
+ readonly commandString: readonly ["--command", "--session-command", "-c"];
288
288
  readonly viaShell: true;
289
289
  };
290
290
  readonly chroot: {
@@ -43,8 +43,8 @@ export const SHELL_WRAPPER_TABLE = {
43
43
  compgen: { valueFlags: [], flags: [], commandString: [], executes: ["-C", "-F"] },
44
44
  complete: { valueFlags: [], flags: [], commandString: [], executes: ["-C", "-F"] },
45
45
  busybox: { valueFlags: [], flags: [], commandString: [] },
46
- su: { valueFlags: [], flags: [], commandString: [], viaShell: true },
47
- runuser: { valueFlags: [], flags: [], commandString: [], viaShell: true },
46
+ su: { valueFlags: ["--group", "--shell", "--supp-group", "--whitelist-environment", "-G", "-g", "-s", "-w"], flags: ["--fast", "--help", "--login", "--no-pty", "--preserve-environment", "--pty", "--version", "-P", "-T", "-V", "-f", "-h", "-l", "-m", "-p"], commandString: ["--command", "--session-command", "-c"], viaShell: true },
47
+ runuser: { valueFlags: ["--group", "--shell", "--supp-group", "--user", "--whitelist-environment", "-G", "-g", "-s", "-u", "-w"], flags: ["--fast", "--help", "--login", "--no-pty", "--preserve-environment", "--pty", "--version", "-P", "-T", "-V", "-f", "-h", "-l", "-m", "-p"], commandString: ["--command", "--session-command", "-c"], viaShell: true },
48
48
  chroot: { valueFlags: [], flags: [], commandString: [], positional: /^/ },
49
49
  setpriv: { valueFlags: [], flags: [], commandString: [] },
50
50
  numactl: { valueFlags: [], flags: [], commandString: [] },
@@ -8,7 +8,8 @@ export const TASK_LIST_TOOL_NAME = "TaskList";
8
8
  export const TODO_WRITE_TOOL_NAME = "TodoWrite";
9
9
  export const REFRESH_MCP_TOOLS_TOOL_NAME = "RefreshMcpTools";
10
10
  export const ARTIFACT_TOOL_NAME = "Artifact";
11
- const SHELL_RULE_FACE = { primaryParams: ["command"], params: ["command", "timeout", "description", "run_in_background"] };
11
+ const shellRuleFace = (params) => ({ grammar: "command", primaryParams: ["command"], params });
12
+ const SHELL_RULE_FACE = shellRuleFace(["command", "timeout", "description", "run_in_background"]);
12
13
  const c = (contractId, implementationRevision = "1") => ({ contractId, implementationRevision });
13
14
  export const TOOL_CATALOG_ENTRIES = [
14
15
  {
@@ -108,7 +109,7 @@ export const TOOL_CATALOG_ENTRIES = [
108
109
  { id: "task-stop-hands", name: "TaskStop", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"], cards: [], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
109
110
  { id: "task-output", name: "TaskOutput", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-output"], face: { family: "delegate", effect: "read", contract: TASK_OUTPUT_CONTRACT } },
110
111
  { id: "task-stop", name: "TaskStop", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-stop"], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
111
- { id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root", "read_unestablished"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
112
+ { id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root", "read_unestablished"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), ruleFace: shellRuleFace(["command", "description", "timeout_ms", "persistent"]), offloadThresholdChars: 10_000 } },
112
113
  { id: "send-message", name: "SendMessage", source: "builtin", definedIn: "src/agents/send-message-tool.ts", mountedBy: ["delegation", "hostInternals", "peerLane"], cards: ["send-message"], face: { family: "protocol", effect: "write", contract: c("core.send_message@1") } },
113
114
  { id: "agent-transcript", name: "AgentTranscript", source: "builtin", definedIn: "src/agents/agent-transcript-tool.ts", mountedBy: ["delegation"], cards: ["agent-transcript"], face: { family: "delegate", effect: "read", contract: c("core.agent_transcript@1") } },
114
115
  { id: "list-agents", name: "ListAgents", source: "builtin", definedIn: "src/agents/list-agents-tool.ts", mountedBy: ["delegation", "peerLane"], cards: ["list-agents"], face: { family: "protocol", effect: "read", aliases: ["ListPeers"], contract: c("core.list_agents@1") } },
@@ -201,7 +201,16 @@ export interface ToolFaceDeclaration {
201
201
  };
202
202
  aliases?: string[];
203
203
  pathTarget?: ToolPathTarget;
204
- ruleFace?: ToolRuleFace;
204
+ /**
205
+ * The rule face ({@link ToolRuleFace}) and, for a definition whose primary parameter is a shell command line, the grammar
206
+ * that judges it — `grammar: "command"`, the SHELL FACE. Every `Bash(<command>)` rule (the command grammar's one spelling,
207
+ * deny / ask / allow, from every source) is judged on a call that answers to the name of such a definition, against that
208
+ * parameter; a bare `Bash` rule stays a rule on the tool of that name. Only a catalog row declares it (a caller's
209
+ * `ToolSpec.ruleFace` is a {@link ToolRuleFace}): the static catalog is the one place the shell face is read from.
210
+ */
211
+ ruleFace?: ToolRuleFace & {
212
+ readonly grammar?: "command";
213
+ };
205
214
  renderHints?: ToolRenderHints;
206
215
  offload?: boolean;
207
216
  offloadThresholdChars?: number;
@@ -69,6 +69,10 @@ export declare const NAME_READINGS: {
69
69
  readonly kind: "wire";
70
70
  readonly file: string;
71
71
  };
72
+ readonly "gate.askFace": {
73
+ readonly kind: "wire";
74
+ readonly file: string;
75
+ };
72
76
  readonly "fold.readStation": {
73
77
  readonly kind: "seat";
74
78
  readonly rank: "member";
@@ -251,6 +255,10 @@ export declare const NAME_READINGS: {
251
255
  readonly kind: "wire";
252
256
  readonly file: string;
253
257
  };
258
+ readonly "inherited.askFace": {
259
+ readonly kind: "wire";
260
+ readonly file: string;
261
+ };
254
262
  readonly "park.contentKindProjection": {
255
263
  readonly kind: "wire";
256
264
  readonly file: string;
@@ -19,6 +19,7 @@ export const NAME_READINGS = {
19
19
  "gate.sandboxBoundary.egress": any(CHAIN),
20
20
  "gate.sandboxBoundary.hand": wire(CHAIN),
21
21
  "gate.preToolUse": wire("src/core/hooks.ts"),
22
+ "gate.askFace": wire("src/core/runner/gate-exit.ts"),
22
23
  "fold.readStation": seat("member", "src/tools/fs/shell-read-check.ts"),
23
24
  "lanes.contentQuestionClass": any(LANES),
24
25
  "lanes.readOnlyShellAllow": wire(LANES),
@@ -63,6 +64,7 @@ export const NAME_READINGS = {
63
64
  "activity.sink": spelling(HANDLERS),
64
65
  "reconcile.orphanEffect": spelling("src/core/session-reconcile.ts"),
65
66
  "inherited.questionFloat": wire(CHAIN),
67
+ "inherited.askFace": wire(CHAIN),
66
68
  "park.contentKindProjection": wire("src/core/checkpoint-store.ts"),
67
69
  "resume.contentAskRow": wire("src/core/runner/decide-continuation.ts"),
68
70
  "resume.editRecheckQuestionSkip": wire("src/core/runner/resume-apply.ts"),
@@ -977,13 +977,14 @@ export declare function createApprovalPolicy(opts: {
977
977
  */
978
978
  export declare function combinePolicies(...policies: ToolPolicy[]): ToolPolicy;
979
979
  /**
980
- * The shell tools whose `{ command }` arg a coarse command-name policy parses; other tools are out of scope. `Monitor`
981
- * belongs here: it runs an arbitrary shell `command` through the same env seam as Bash, so the always-on parity gates
982
- * keyed on this list reach it too (a list of `Bash` alone let the identical command run ungated as `Monitor`). EXPORTED:
983
- * a downstream governance layer keeps its own copy of this list so its per-command ask-list gates the same tools core
984
- * does — the list itself is the fact, and publishing it removes the guess.
980
+ * The shell tools whose `{ command }` arg a coarse command-name policy parses; other tools are out of scope. It is the
981
+ * catalog's shell face ({@link shellFaceToolNames} — every definition whose `command` the command grammar judges, `Monitor`
982
+ * among them: it runs an arbitrary shell `command` through the same env seam as Bash), spelled out for the wire; the
983
+ * catalog's shell face is the truth, and a lockstep pin keeps the two one (a catalog row added or dropped turns it red):
984
+ * the always-on parity gates keyed on it, the `Bash(...)` rules and the shell seat reach the same tools. EXPORTED: a
985
+ * downstream governance layer reads it so its per-command ask-list gates the same tools core does.
985
986
  */
986
- export declare const COARSE_SHELL_TOOLS: string[];
987
+ export declare const COARSE_SHELL_TOOLS: readonly ["Bash", "Monitor"];
987
988
  /**
988
989
  * design/center §10 — a COARSE allow/deny gate keyed on the LEADING command NAME (`argv[0]`) of a single
989
990
  * simple shell command. It reuses the ONE shared simple-command parser
@@ -1033,7 +1034,7 @@ export declare function createCoarseCommandNamePolicy(opts: {
1033
1034
  deny?: string[];
1034
1035
  /** What to do for a command not matched by `allow` (or un-parseable): `"ask"` (default) or `"deny"`. */
1035
1036
  defaultAction?: "ask" | "deny";
1036
- /** Override the shell tool names this gate parses (default `Bash` — bash + bash_readonly share the CC name). */
1037
+ /** Override the shell tool names this gate parses (default {@link COARSE_SHELL_TOOLS}, the shell face). */
1037
1038
  tools?: string[];
1038
1039
  }): ToolPolicy;
1039
1040
  export declare function findUnverifiableRecursiveDelete(command: string, safeVars?: ReadonlySet<string>): string | undefined;
@@ -1112,7 +1113,7 @@ export declare function createUnverifiableDeletePolicy(opts?: {
1112
1113
  * the default was `TMPDIR` alone, reading STRICTER than the gate really is; `HOME`/`PWD` are a
1113
1114
  * registered sema divergence from CC's temp-only carve-out, see the RB-340 note there). */
1114
1115
  safeVars?: readonly string[];
1115
- /** Override the shell tool names this gate parses (default `Bash`). */
1116
+ /** Override the shell tool names this gate parses (default {@link COARSE_SHELL_TOOLS}, the shell face). */
1116
1117
  tools?: string[];
1117
1118
  removalSeat?: RemovalGuardSeat;
1118
1119
  }): ToolPolicy;
@@ -1121,7 +1122,7 @@ export declare function createTranscriptIntegrityPolicy(opts?: {
1121
1122
  sessionsDirs?: readonly string[];
1122
1123
  /** argv[0] allowlist for read-only access to the transcript dir (default {@link BASH_READONLY_DEFAULT_ALLOW}). */
1123
1124
  readAllow?: readonly string[];
1124
- /** Override the shell tool names this gate parses (default `Bash`). */
1125
+ /** Override the shell tool names this gate parses (default {@link COARSE_SHELL_TOOLS}, the shell face). */
1125
1126
  tools?: string[];
1126
1127
  }): ToolPolicy;
1127
1128
  /**
@@ -10,7 +10,7 @@ export { policyCannotAnswer, rowDemandsAPerson } from "./real-approval.js";
10
10
  import { sanitizeAutoModeArmingRecipe } from "./auto-mode-arming.js";
11
11
  import { join, normalize as normalizePath, sep } from "node:path";
12
12
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
13
- import { protectivePathTargetOf } from "./tool-registry.js";
13
+ import { protectivePathTargetOf, shellFaceToolNames } from "./tool-registry.js";
14
14
  import { heldBy } from "./tool-name-readings.js";
15
15
  import { boundInputHashOf } from "./canonical-json.js";
16
16
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
@@ -642,7 +642,7 @@ export function combinePolicies(...policies) {
642
642
  },
643
643
  };
644
644
  }
645
- export const COARSE_SHELL_TOOLS = ["Bash", "Monitor"];
645
+ export const COARSE_SHELL_TOOLS = Object.freeze(["Bash", "Monitor"]);
646
646
  function canonicalToolNameSet(tools) {
647
647
  return new Set(tools ?? COARSE_SHELL_TOOLS);
648
648
  }
@@ -25,6 +25,15 @@ export declare function catalogToolNames(): ReadonlySet<string>;
25
25
  export declare function pathConfinableWriteToolNames(): ReadonlySet<string>;
26
26
  /** A-8 — every definition that names a path target at all (read or write): the file-tool glob lane. */
27
27
  export declare function pathTargetingToolNames(): ReadonlySet<string>;
28
+ /**
29
+ * The SHELL FACE: every definition whose `command` argument is a shell command line the command grammar judges (today the two
30
+ * `Bash` definitions and `Monitor`). A `Bash(<command>)` rule reaches a call that answers to any of these names; the coarse
31
+ * command-name gate's default list, the shell read station and the shell seat are this set. Derived from the rows' rule faces,
32
+ * never a list of its own.
33
+ */
34
+ export declare function shellFaceToolNames(): ReadonlySet<string>;
35
+ /** {@link shellFaceToolNames}' membership for one name — any definition under the name on the shell face. */
36
+ export declare function isShellFaceToolName(name: string): boolean;
28
37
  /** The tools whose declared path target is a READ (a file read, a search root): the file read station's membership. */
29
38
  export declare function readTargetToolNames(): ReadonlySet<string>;
30
39
  /** A-10 — the micro-compaction whitelist (`compactable: true`). */
@@ -29,6 +29,16 @@ export function pathConfinableWriteToolNames() {
29
29
  export function pathTargetingToolNames() {
30
30
  return names((e) => e.face.pathTarget !== undefined);
31
31
  }
32
+ const onShellFace = (e) => e.face.ruleFace?.grammar === "command";
33
+ export function shellFaceToolNames() {
34
+ return names(onShellFace);
35
+ }
36
+ export function isShellFaceToolName(name) {
37
+ for (const e of TOOL_CATALOG_ENTRIES)
38
+ if (e.name === name && onShellFace(e))
39
+ return true;
40
+ return false;
41
+ }
32
42
  export function readTargetToolNames() {
33
43
  return names((e) => e.face.pathTarget?.access === "read");
34
44
  }
@@ -1,5 +1,6 @@
1
1
  import type { ExecutionEnv } from "../internal/harness-types.js";
2
2
  import type { PermissionResult } from "./tool-policy.js";
3
+ import { type AskFace } from "./ask-origin.js";
3
4
  import { type ToolCallLike } from "./tool-registry.js";
4
5
  /**
5
6
  * How one table row matches the judged path:
@@ -209,11 +210,17 @@ export declare function compileWriteProtection(entries?: readonly WriteProtected
209
210
  * message (table names are deployment-validated vocabulary); the model-authored target path deliberately
210
211
  * does not — `message` is the classifier's `askMessage`, and the approval card already shows the args. The
211
212
  * spelling form's sentence is the historical one byte for byte; the target form says the target RESOLVED.
212
- * · `unresolvable` ⇒ `deny` naming the cause: an unjudged target is never vouched, and an ask would be a
213
- * question about a file nobody can name — the rule the canonicalizing deny set follows for the same reason.
213
+ * · `unresolvable` ⇒ read by the call's FACE (`face`, {@link releasableAskClassesFor}): where no permission mode releases
214
+ * `write_protection_unresolved` (the file face) — `deny` naming the cause: an unjudged target is never vouched (the rule
215
+ * the canonicalizing deny set follows for the same reason), so there a standing allow is refused while a standing ask
216
+ * carrying the record stays a person's question in the modes that ask (dontAsk and bypassPermissions refuse it); where
217
+ * a mode may release the class (the shell face) — `ask` (`safety`)
218
+ * naming the cause, which the mode arbitration answers exactly as it answers that record over a standing ask
219
+ * (bypassPermissions releases it, default puts it to a person, dontAsk refuses): on that face alone a standing allow
220
+ * reads as a standing ask.
214
221
  * · `clear` ⇒ `undefined`: the surviving decision stands.
215
222
  */
216
- export declare function writeProtectionTightenDecision(toolName: string, verdict: WriteProtectionVerdict): PermissionResult | undefined;
223
+ export declare function writeProtectionTightenDecision(toolName: string, verdict: WriteProtectionVerdict, face: AskFace): PermissionResult | undefined;
217
224
  /**
218
225
  * @contract write_protection.verdict_is_the_mandate — the judge's two non-clear verdicts are the two facts
219
226
  * a standing allow rule may not retire, and `clear` is neither: a MATCHED row is the mandate a live decider
@@ -1,3 +1,4 @@
1
+ import { releasableAskClassesFor } from "./ask-origin.js";
1
2
  import { canonicalizeTarget, isWinFormPath, writeTargetPath, isUncPath, isAbsoluteForFamily, joinForFamily, pathFamilyOf } from "../tools/fs/safety.js";
2
3
  import { protectivePathTargetOf } from "./tool-registry.js";
3
4
  import { pathTargetBaseOf } from "./effective-path-target.js";
@@ -346,7 +347,7 @@ export function compileWriteProtection(entries, opts) {
346
347
  },
347
348
  };
348
349
  }
349
- export function writeProtectionTightenDecision(toolName, verdict) {
350
+ export function writeProtectionTightenDecision(toolName, verdict, face) {
350
351
  if (verdict.kind === "hit") {
351
352
  return {
352
353
  action: "ask",
@@ -359,6 +360,15 @@ export function writeProtectionTightenDecision(toolName, verdict) {
359
360
  };
360
361
  }
361
362
  if (verdict.kind === "unresolvable") {
363
+ if (releasableAskClassesFor(face).includes("write_protection_unresolved")) {
364
+ return {
365
+ action: "ask",
366
+ message: verdict.unresolvedSymlink
367
+ ? `tool "${toolName}" writes to a symlink whose real target could not be resolved, so the write-protection judge cannot tell whether it lands on a protected path (${verdict.cause}) — explicit approval required`
368
+ : `tool "${toolName}" writes to a target that could not be resolved on this tree, so the write-protection judge cannot tell whether it lands on a protected path (${verdict.cause}) — explicit approval required`,
369
+ decisionReason: "safety",
370
+ };
371
+ }
362
372
  return {
363
373
  action: "deny",
364
374
  message: verdict.unresolvedSymlink
package/dist/index.d.ts CHANGED
@@ -197,7 +197,7 @@ export { createAllowDenyPolicy, createApprovalPolicy,
197
197
  type ApprovalSeatOutcome, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, abortedSeatDeny, receiveHookResult, isEngineRefusal, wrapToolPolicy, decisionText, resolveAsk, toolPolicyNameSets, toolPolicyPathRules, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicyProjection, type ToolPolicyProjectionComponent, type ConstraintChainEntry, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, type AskRuleEvidence, type AskEvidenceAbsence, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
198
198
  export { type ReadRootGrantScreen } from "./core/read-root-grant-screen.js";
199
199
  export { type AskOrigin, ASK_ORIGINS, isAskOrigin, ORIGIN_IMPLIES_REAL_APPROVAL, PERSISTED_RULE_MANDATES, type PersistedRuleMandate, isPersistedRuleMandate, screenAskClassMembers } from "./core/ask-origin.js";
200
- export { PERMISSION_MODES, type PermissionMode, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, type AskClass, isAskClass, MODE_ARBITRATION, type ModeArbitrationRow, RELEASABLE_ASK_CLASSES, MODE_RELEASE_REASONS, type ModeReleaseReason, isModeReleaseReason, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, type AskClassification, type AskClassFacts, type ToolMarkSource, arbitrateByMode, type Arbitration, fileCommandOptionsSentence } from "./core/ask-origin.js";
200
+ export { PERMISSION_MODES, type PermissionMode, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, type AskClass, isAskClass, MODE_ARBITRATION, type ModeArbitrationRow, ASK_FACES, type AskFace, askFaceOf, releasableAskClassesFor, MODE_RELEASE_REASONS, type ModeReleaseReason, isModeReleaseReason, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, type AskClassification, type AskClassFacts, type ToolMarkSource, arbitrateByMode, type Arbitration, fileCommandOptionsSentence } from "./core/ask-origin.js";
201
201
  export { modePolicyOf, type ModePolicy, PERMISSION_MODE_FOLD_REASONS, type PermissionModeFoldReason, type PermissionModeReceipt, ACCEPT_EDITS_FILE_COMMANDS, modeAcceptsFileCommand } from "./core/mode-policy.js";
202
202
  export type { PermissionSeatInputs, PermissionSeatReading } from "./core/mode-policy.js";
203
203
  export type { SkillGateSeats, SkillGrantHit, SkillGrantLane, SkillGrantOrigin, SkillGrantRecord } from "./core/skill-grant.js";
package/dist/index.js CHANGED
@@ -162,7 +162,7 @@ export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.
162
162
  export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, abortedSeatDeny, receiveHookResult, isEngineRefusal, wrapToolPolicy, decisionText, resolveAsk, toolPolicyNameSets, toolPolicyPathRules, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
163
163
  export {} from "./core/read-root-grant-screen.js";
164
164
  export { ASK_ORIGINS, isAskOrigin, ORIGIN_IMPLIES_REAL_APPROVAL, PERSISTED_RULE_MANDATES, isPersistedRuleMandate, screenAskClassMembers } from "./core/ask-origin.js";
165
- export { PERMISSION_MODES, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, isAskClass, MODE_ARBITRATION, RELEASABLE_ASK_CLASSES, MODE_RELEASE_REASONS, isModeReleaseReason, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, arbitrateByMode, fileCommandOptionsSentence } from "./core/ask-origin.js";
165
+ export { PERMISSION_MODES, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, isAskClass, MODE_ARBITRATION, ASK_FACES, askFaceOf, releasableAskClassesFor, MODE_RELEASE_REASONS, isModeReleaseReason, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, arbitrateByMode, fileCommandOptionsSentence } from "./core/ask-origin.js";
166
166
  export { modePolicyOf, PERMISSION_MODE_FOLD_REASONS, ACCEPT_EDITS_FILE_COMMANDS, modeAcceptsFileCommand } from "./core/mode-policy.js";
167
167
  export { SKILL_GRANT_ORIGINS } from "./core/skill-grant.js";
168
168
  export { ALLOW_ENTRY_INERT_REASONS, compileAllowEntry, compileAllowList, renderListEntry } from "./core/allow-entry.js";
@@ -103,6 +103,17 @@ export interface WordExpansion {
103
103
  readonly body?: string;
104
104
  /** The parameter name, where the expansion names one. */
105
105
  readonly name?: string;
106
+ /** A glob's pattern — absent where an expansion stands inside it (its text is not the pattern): `folded`, what the
107
+ * program receives where it matches nothing — quotes removed, the spelling {@link LexedWord.folded} carries at its place
108
+ * (`[^.]`); `written`, as written, quoting kept (`[^\.]`, `["!"a]`) — the text a shell re-reading it matches by;
109
+ * `judged`, the spelling a reader judges the names it matches by — `folded`, save a class the judge reads as any one
110
+ * character: one holding a quoted or escaped character (its quote-removed text reads another class: `["!"a]` is not
111
+ * the negation `[!a]`) or a `[:…:]` / `[=…=]` / `[.….]` unit — spelled `?`, a superset of its members. */
112
+ readonly pattern?: {
113
+ readonly folded: string;
114
+ readonly written: string;
115
+ readonly judged: string;
116
+ };
106
117
  }
107
118
  export interface QuotedSpan {
108
119
  /** Offsets into {@link LexedWord.raw}, `[start, end)`; the quote characters belong to their span. */
@@ -143,6 +154,10 @@ export interface LexedWord {
143
154
  readonly kind: "word";
144
155
  readonly raw: string;
145
156
  readonly folded: string;
157
+ /** The spelling a reader judges the word's names by, where it is not `folded`: each bracket class holding a quoted or
158
+ * escaped character or a `[:…:]` / `[=…=]` / `[.….]` unit spelled `?` (`["!"a]*`, `[[:alpha:]]*` ⇒ `?*`,
159
+ * {@link WordExpansion.pattern}). */
160
+ readonly judged?: string;
146
161
  readonly pieces: readonly WordPiece[];
147
162
  readonly quotedSpans: readonly QuotedSpan[];
148
163
  /** The pieces that carry an expansion, in order. */