@sema-agent/core 7.46.0 → 7.48.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 (77) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/dist/core/allow-entry.d.ts +1 -1
  3. package/dist/core/allow-entry.js +4 -5
  4. package/dist/core/ask-origin.d.ts +35 -12
  5. package/dist/core/ask-origin.js +23 -6
  6. package/dist/core/checkpoint-store.d.ts +1 -1
  7. package/dist/core/engine-notice.d.ts +7 -4
  8. package/dist/core/gate-fold.js +2 -2
  9. package/dist/core/gate-lanes.js +2 -1
  10. package/dist/core/gate-outcome.d.ts +4 -2
  11. package/dist/core/gate-outcome.js +2 -2
  12. package/dist/core/mcp-server-spec.d.ts +1 -1
  13. package/dist/core/permission-rule-model.d.ts +12 -6
  14. package/dist/core/permission-rule-model.js +10 -2
  15. package/dist/core/permission-rules.d.ts +4 -5
  16. package/dist/core/permission-rules.js +28 -15
  17. package/dist/core/runner/gate-exit.d.ts +4 -1
  18. package/dist/core/runner/gate-exit.js +5 -1
  19. package/dist/core/runner/park-commit.d.ts +1 -1
  20. package/dist/core/runner/prepare-hands-readface.js +5 -5
  21. package/dist/core/runner/prepare-policy-chain.js +2 -1
  22. package/dist/core/safety-axis-vocab.d.ts +1 -1
  23. package/dist/core/safety-merge-corpus.d.ts +1 -1
  24. package/dist/core/session-policy-store.d.ts +6 -3
  25. package/dist/core/session-policy-store.js +28 -10
  26. package/dist/core/shell-wrapper-projection.js +1 -1
  27. package/dist/core/shell-wrapper-table.d.ts +8 -8
  28. package/dist/core/shell-wrapper-table.js +3 -3
  29. package/dist/core/tool-catalog-entries.js +3 -2
  30. package/dist/core/tool-face.d.ts +10 -1
  31. package/dist/core/tool-name-aliases.d.ts +9 -8
  32. package/dist/core/tool-name-aliases.js +2 -0
  33. package/dist/core/tool-name-readings.d.ts +8 -0
  34. package/dist/core/tool-name-readings.js +2 -0
  35. package/dist/core/tool-policy.d.ts +10 -9
  36. package/dist/core/tool-policy.js +6 -7
  37. package/dist/core/tool-registry.d.ts +9 -0
  38. package/dist/core/tool-registry.js +10 -0
  39. package/dist/core/write-protect.d.ts +10 -3
  40. package/dist/core/write-protect.js +11 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/orchestration/workflow.js +2 -2
  44. package/dist/prompt-assembly/types.d.ts +1 -1
  45. package/dist/shell/lex.d.ts +15 -0
  46. package/dist/shell/lex.js +86 -15
  47. package/dist/tools/fs/bash-program-position.d.ts +16 -6
  48. package/dist/tools/fs/bash-program-position.js +18 -12
  49. package/dist/tools/fs/index.d.ts +3 -2
  50. package/dist/tools/fs/index.js +2 -2
  51. package/dist/tools/fs/program-read-model.d.ts +84 -12
  52. package/dist/tools/fs/program-read-model.js +105 -34
  53. package/dist/tools/fs/program-read-model.rows.js +26 -7
  54. package/dist/tools/fs/read-deny-home.d.ts +20 -1
  55. package/dist/tools/fs/read-deny-home.js +28 -7
  56. package/dist/tools/fs/read-deny.d.ts +13 -6
  57. package/dist/tools/fs/read-deny.js +12 -3
  58. package/dist/tools/fs/removal-targets.js +24 -5
  59. package/dist/tools/fs/shell-read-argv.d.ts +6 -8
  60. package/dist/tools/fs/shell-read-argv.js +5 -5
  61. package/dist/tools/fs/shell-read-bindings.d.ts +71 -19
  62. package/dist/tools/fs/shell-read-bindings.js +190 -75
  63. package/dist/tools/fs/shell-read-establish.js +6 -7
  64. package/dist/tools/fs/shell-read-plan.d.ts +3 -15
  65. package/dist/tools/fs/shell-read-plan.js +126 -129
  66. package/dist/tools/fs/shell-read-start.d.ts +57 -0
  67. package/dist/tools/fs/shell-read-start.js +92 -0
  68. package/dist/tools/fs/shell-read-word.d.ts +68 -8
  69. package/dist/tools/fs/shell-read-word.js +106 -12
  70. package/dist/tools/fs/shell-read-write-targets.js +4 -3
  71. package/dist/tools/fs/shell-reentry-text.d.ts +103 -3
  72. package/dist/tools/fs/shell-reentry-text.js +122 -4
  73. package/dist/tools/monitor.d.ts +3 -3
  74. package/package.json +1 -1
  75. package/test/export-surface.snapshot.json +9 -3
  76. package/wire-changes/core@7.47.0.json +412 -0
  77. package/wire-changes/core@7.48.0.json +252 -0
@@ -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
  }
@@ -725,7 +725,7 @@ function assignmentTokenSpan(tokens, i) {
725
725
  return 1;
726
726
  }
727
727
  function valueIsOpaque(value, assigned, safeVars) {
728
- const v = value.replace(/["']/g, "");
728
+ const v = value.replace(/["']/g, "").replace(/\\(.)/g, "$1");
729
729
  if (v.includes("$(") || v.includes("`"))
730
730
  return true;
731
731
  if (/\$[@*#!?0-9-]/.test(v))
@@ -734,11 +734,12 @@ function valueIsOpaque(value, assigned, safeVars) {
734
734
  return true;
735
735
  for (const vm of v.matchAll(/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g)) {
736
736
  const ref = vm[1];
737
- if (!assigned.has(ref) && !safeVars.has(ref))
737
+ if (!assigned.has(ref) && !(safeVars.has(ref) && safeRootBelow(v.slice((vm.index ?? 0) + vm[0].length))))
738
738
  return true;
739
739
  }
740
740
  return false;
741
741
  }
742
+ const safeRootBelow = (after) => /^\/[^*?[\]]+$/.test(after) && !after.split("/").includes("..");
742
743
  export function findUnverifiableRecursiveDelete(command, safeVars = new Set(UNVERIFIABLE_DELETE_SAFE_VARS)) {
743
744
  const assigned = new Set();
744
745
  const assignedEscapesRoot = new Set();
@@ -887,9 +888,7 @@ export function findUnverifiableRecursiveDelete(command, safeVars = new Set(UNVE
887
888
  return `\`rm\` recursive+force target ${raw} resolves through ${name}, whose in-window value climbs above a known-safe root (\`..\`) — the deleted path cannot be verified from this command`;
888
889
  }
889
890
  if (safeVars.has(name)) {
890
- const after = targetNorm.slice((vm.index ?? 0) + vm[0].length);
891
- const escapesRoot = after.split("/").includes("..");
892
- if (/^\/[^*?[\]]+$/.test(after) && !escapesRoot)
891
+ if (safeRootBelow(targetNorm.slice((vm.index ?? 0) + vm[0].length)))
893
892
  continue;
894
893
  }
895
894
  return `\`rm\` recursive+force target ${raw} references $${name}, which has no assignment in this command — an empty or unexpected value could make this a home-directory or filesystem-root wipe`;
@@ -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";
@@ -16,7 +16,7 @@ import { isWorkflowRunActive, closeWorkflowChannel, markWorkflowActive, publishW
16
16
  import { isDurablePause } from "../agents/suspend-guard.js";
17
17
  import { RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
18
18
  import { boundInputHashOf } from "../core/canonical-json.js";
19
- import { boundedRedactedSummary } from "../core/untrusted-egress.js";
19
+ import { boundedRedactedSummary, redactSecrets } from "../core/untrusted-egress.js";
20
20
  import { delimitUntrusted } from "../core/untrusted-text.js";
21
21
  import { OUTPUT_TOOL_NAME } from "../core/runner/synthetic-tools.js";
22
22
  import { deniedToolNamesOf } from "../core/runner/sub-agent-deny-lists.js";
@@ -2420,7 +2420,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
2420
2420
  run.endedAt = now();
2421
2421
  if (parkedLeg !== undefined && !thrownIs(err, WorkflowAgentParkedError))
2422
2422
  err = parkedError();
2423
- run.error = thrownText(err);
2423
+ run.error = redactSecrets(thrownText(err));
2424
2424
  closeAbandonedAgents(run.endedAt);
2425
2425
  {
2426
2426
  let thrownCode;
@@ -146,7 +146,7 @@ export interface PackSectionDeclaration {
146
146
  /**
147
147
  * design/147 epoch declaration axis (ruling a): `"provider"` marks a section whose text came
148
148
  * from the typed `stableBlocks` lane (incl. the provider-assembled pass-through role) — the SELECTOR
149
- * for the epoch artifactDigest's declaration input, so a config-center publish (declaration bytes
149
+ * for the epoch artifactDigest's declaration input, so a web-admin publish (declaration bytes
150
150
  * change) re-pins sessions at the next legal boundary. NOT a digest-disclosure gate
151
151
  * (that stays {@link declaredContentHash}-only): epoch digests are one-way hashes over the payload.
152
152
  */
@@ -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. */
package/dist/shell/lex.js CHANGED
@@ -357,6 +357,7 @@ class WordBuilder {
357
357
  marks = [];
358
358
  subscript;
359
359
  compound = false;
360
+ widenedClasses = [];
360
361
  begin(src) { if (!this.started)
361
362
  this.at = src; }
362
363
  push(w, f, by, src) {
@@ -402,6 +403,17 @@ class WordBuilder {
402
403
  mark(reason, rawAt) { this.marks.push({ at: rawAt, reason }); }
403
404
  inSubscript() { return this.subscript !== undefined && this.subscript.rawEnd === undefined; }
404
405
  foldedOf(r) { return r >= this.foldedAt.length ? this.folded.length : this.foldedAt[r]; }
406
+ classSpelled(from, to) {
407
+ let out = "";
408
+ let at = from;
409
+ for (const [a, b] of this.widenedClasses) {
410
+ if (a < at || b > to)
411
+ continue;
412
+ out += `${this.folded.slice(at, a)}?`;
413
+ at = b;
414
+ }
415
+ return out + this.folded.slice(at, to);
416
+ }
405
417
  slice(from, to) {
406
418
  const out = [];
407
419
  for (const p of this.pieces) {
@@ -519,9 +531,37 @@ class WordBuilder {
519
531
  else if (stack.length > 0 && (d === "," || (d === "." && this.raw[j + 1] === ".")))
520
532
  stack[stack.length - 1].listed = true;
521
533
  }
522
- const nextBracketClose = new Array(n + 1).fill(-1);
523
- for (let j = n - 1; j >= 0; j--)
524
- nextBracketClose[j] = readable(j) && this.raw[j] === "]" ? j : nextBracketClose[j + 1];
534
+ const text = this.folded;
535
+ const nf = text.length;
536
+ const readableAt = (f) => f < nf && readable(this.rawOfFolded[f]);
537
+ const unitClose = (d) => {
538
+ const at = new Array(nf + 2).fill(-1);
539
+ for (let f = nf - 2; f >= 0; f--)
540
+ at[f] = readableAt(f) && readableAt(f + 1) && text[f] === d && text[f + 1] === "]" ? f : at[f + 1];
541
+ return at;
542
+ };
543
+ const unitCloses = new Map([":", "=", "."].map((d) => [d, unitClose(d)]));
544
+ const lettersEnd = new Array(nf + 1).fill(nf);
545
+ for (let f = nf - 1; f >= 0; f--) {
546
+ const lower = text.charCodeAt(f) | 0x20;
547
+ lettersEnd[f] = lower >= 0x61 && lower <= 0x7a && readableAt(f) ? lettersEnd[f + 1] : f;
548
+ }
549
+ const unitEndAt = (f) => {
550
+ if (!(readableAt(f) && text[f] === "[" && readableAt(f + 1)))
551
+ return -1;
552
+ const d = text[f + 1];
553
+ const end = unitCloses.get(d)?.[f + 2] ?? -1;
554
+ if (end < 0)
555
+ return -1;
556
+ return d === ":" ? (end > f + 2 && lettersEnd[f + 2] === end ? end : -1) : end === f + 3 ? end : -1;
557
+ };
558
+ const nextBracketClose = new Array(nf + 2).fill(-1);
559
+ const nextSlash = new Array(nf + 1).fill(nf);
560
+ for (let f = nf - 1; f >= 0; f--) {
561
+ const unitEnd = unitEndAt(f);
562
+ nextBracketClose[f] = readableAt(f) && text[f] === "]" ? f : unitEnd >= 0 ? nextBracketClose[unitEnd + 2] : nextBracketClose[f + 1];
563
+ nextSlash[f] = text[f] === "/" ? f : nextSlash[f + 1];
564
+ }
525
565
  const ranges = [];
526
566
  for (let k = 0; k < n; k++) {
527
567
  if (!readable(k))
@@ -536,19 +576,35 @@ class WordBuilder {
536
576
  continue;
537
577
  }
538
578
  if (c === "*" || c === "?") {
539
- ranges.push({ from: k, to: k + 1, exp: { kind: "glob", yields: "many" } });
579
+ ranges.push({ from: k, to: k + 1, exp: { kind: "glob", yields: "many", pattern: { folded: c, written: c, judged: c } } });
540
580
  continue;
541
581
  }
542
582
  if (c === "[") {
543
- let j = k + 1;
544
- if (this.raw[j] === "!" || this.raw[j] === "^")
583
+ if (this.rawOfFolded[this.foldedOf(k)] !== k)
584
+ continue;
585
+ let j = this.foldedOf(k) + 1;
586
+ if ((text[j] === "!" || text[j] === "^") && readableAt(j))
545
587
  j++;
546
- if (this.raw[j] === "]")
588
+ if (text[j] === "]" && readableAt(j))
547
589
  j++;
548
- const close = j < n ? nextBracketClose[j] : -1;
549
- if (close < 0)
590
+ const closeAt = j < nf ? nextBracketClose[j] : -1;
591
+ if (closeAt < 0 || nextSlash[j] < closeAt)
550
592
  continue;
551
- ranges.push({ from: k, to: close + 1, exp: { kind: "glob", yields: "many", body: this.raw.slice(k + 1, close) } });
593
+ const close = this.rawOfFolded[closeAt];
594
+ let expands = false;
595
+ for (let m = k + 1; m < close && !expands; m++)
596
+ expands = inExp[m] || braceClose.get(m)?.listed === true;
597
+ let widened = false;
598
+ for (let m = k + 1; m < close && !widened; m++)
599
+ widened = this.rawQuoted[m] !== undefined;
600
+ for (let f = j; f < closeAt && !widened; f++)
601
+ widened = readableAt(f) && text[f] === "[" && readableAt(f + 1) && (text[f + 1] === ":" || text[f + 1] === "=" || text[f + 1] === ".");
602
+ const from = this.foldedOf(k);
603
+ const to = this.foldedOf(close + 1);
604
+ if (!expands && widened)
605
+ this.widenedClasses.push([from, to]);
606
+ const pattern = expands ? {} : { pattern: { folded: this.folded.slice(from, to), written: this.raw.slice(k, close + 1), judged: widened ? "?" : this.folded.slice(from, to) } };
607
+ ranges.push({ from: k, to: close + 1, exp: { kind: "glob", yields: "many", body: this.raw.slice(k + 1, close), ...pattern } });
552
608
  k = close;
553
609
  }
554
610
  }
@@ -647,18 +703,20 @@ class WordBuilder {
647
703
  }
648
704
  finish(atCommandPosition, tildeSpelling = true) {
649
705
  const rawText = this.raw;
650
- let foldedText = this.folded;
706
+ let literalTilde = false;
651
707
  let assignment;
652
708
  const tildeEnd = this.tildePrefixEnd(0, false);
653
709
  if (tildeEnd !== undefined)
654
710
  this.carveTilde(0, tildeEnd);
655
711
  else {
656
- if (tildeSpelling)
657
- foldedText = literalTildeSpelling(foldedText);
712
+ literalTilde = tildeSpelling;
658
713
  assignment = this.readAssignment();
659
714
  }
660
715
  if (!(assignment !== undefined && assignment.bare && atCommandPosition))
661
716
  this.carveBraceAndGlob();
717
+ const spelled = (text) => (literalTilde ? literalTildeSpelling(text) : text);
718
+ const foldedText = spelled(this.folded);
719
+ const judged = this.widenedClasses.length > 0 ? spelled(this.classSpelled(0, this.folded.length)) : undefined;
662
720
  if (assignment !== undefined && this.subscript?.text)
663
721
  this.mark("subscript_text", this.subscript.rawStart);
664
722
  const marks = this.marks.filter((m) => m.reason !== "subscript_text" || assignment?.subscript !== undefined).sort((a, b) => a.at - b.at);
@@ -679,6 +737,7 @@ class WordBuilder {
679
737
  kind: "word",
680
738
  raw: rawText,
681
739
  folded: foldedText,
740
+ ...(judged !== undefined ? { judged } : {}),
682
741
  pieces,
683
742
  quotedSpans: spans,
684
743
  expansions: pieces.filter((p) => p.expansion !== undefined),
@@ -703,6 +762,7 @@ function scan(source, connectorsAreText) {
703
762
  let acceptable = true;
704
763
  let timeGrammar;
705
764
  let definitionName = false;
765
+ let coprocWord;
706
766
  let afterLeadingRedirect = false;
707
767
  let assignmentSeen = false;
708
768
  let subscriptsClosed = false;
@@ -716,7 +776,7 @@ function scan(source, connectorsAreText) {
716
776
  w.opaque(source.slice(from, to), SUBSTITUTION_PLACEHOLDER, by, from, to, { kind, yields: kind === "command" && by === undefined ? "many" : "one", body });
717
777
  current().substitutions.push(body);
718
778
  };
719
- const resetPosition = () => { acceptable = true; commandWord = undefined; afterLeadingRedirect = false; assignmentSeen = false; subscriptsClosed = false; timeGrammar = undefined; definitionName = false; };
779
+ const resetPosition = () => { acceptable = true; commandWord = undefined; afterLeadingRedirect = false; assignmentSeen = false; subscriptsClosed = false; timeGrammar = undefined; definitionName = false; coprocWord = undefined; };
720
780
  const endWord = () => {
721
781
  if (!w.started)
722
782
  return undefined;
@@ -771,6 +831,11 @@ function scan(source, connectorsAreText) {
771
831
  }
772
832
  if (acceptable && bare && (SHELL_CONTROL_PREFIXES.has(word.folded) || GROUP_WORDS.has(word.folded))) {
773
833
  timeGrammar = word.folded === "time" ? "opt" : undefined;
834
+ coprocWord = undefined;
835
+ return undefined;
836
+ }
837
+ if (acceptable && bare && word.folded === "coproc") {
838
+ coprocWord = "head";
774
839
  return undefined;
775
840
  }
776
841
  if (acceptable && bare && timeGrammar === "opt" && word.folded === "-p") {
@@ -784,10 +849,16 @@ function scan(source, connectorsAreText) {
784
849
  timeGrammar = undefined;
785
850
  if (word.assignment !== undefined && word.assignment.bare && acceptable) {
786
851
  assignmentSeen = true;
852
+ coprocWord = undefined;
787
853
  return undefined;
788
854
  }
789
- if (acceptable)
855
+ if (acceptable && coprocWord !== "name")
790
856
  commandWord = bare ? word.folded : undefined;
857
+ if (coprocWord === "head") {
858
+ coprocWord = "name";
859
+ return undefined;
860
+ }
861
+ coprocWord = undefined;
791
862
  acceptable = false;
792
863
  return undefined;
793
864
  };
@@ -34,6 +34,13 @@ export { SHELL_CONTROL_PREFIXES };
34
34
  /** A control word is a keyword only when bash sees it BARE: `'if'` / `"do"` / `\\!` are command names (a program
35
35
  * looked up on PATH, whose failure changes nothing), so a quoted or escaped spelling never counts as one. */
36
36
  export declare function isBareControlWord(words: readonly LexedWord[], i: number, set: ReadonlySet<string>): boolean;
37
+ /** Does the shell read this word as syntax — a reserved word, a control word, `{`, `coproc` — of `set`? Only a word
38
+ * written BARE where bash reads a command's first word (the lexer's {@link LexedWord.atCommandPosition}): a quoted or
39
+ * escaped spelling is a command name, and a word an expansion produced (a value, a split field) is never syntax —
40
+ * bash parses the line before it expands it. The one reading every syntax reader of a command's words asks. */
41
+ export declare function isShellSyntaxWord(w: LexedWord | undefined, set: ReadonlySet<string>): boolean;
42
+ /** The words that head a loop over a list (`for NAME in …`, `select NAME in …`). */
43
+ export declare const LOOP_LIST_HEADS: ReadonlySet<string>;
37
44
  /**
38
45
  * A word bash strips as an ASSIGNMENT before it chooses the program: the lexer's assignment fact, written BARE
39
46
  * (the name, the brackets and the operator unquoted — a quoted VALUE still assigns, a quoted operator is a
@@ -53,12 +60,15 @@ export type ProgramPositionRefusal = {
53
60
  readonly kind: "control_syntax_at_program_position";
54
61
  readonly owner: string;
55
62
  readonly word: string;
56
- /** Not a DECLINE ({@link programPositionRunsShell}): the row WAS read and a SHELL stands at the program
57
- * position. Absent ⇒ genuine control syntax, which IS one. */
58
- readonly runsShell?: true;
59
- /** The launcher hands the shell its operands as text (`watch "S"`); absent beside `runsShell`, the shell is handed
60
- * nothing (`sudo -s`, `chroot DIR`) and runs its standard input. */
61
- readonly handsText?: true;
63
+ /** Not a DECLINE ({@link programPositionRunsShell}): the row WAS read and a SHELL — or a program the launcher
64
+ * names by an option of its own (`su -s PROGRAM`) — stands at the program position, run by the launcher at argv
65
+ * index `launcher` and handed the words from index `from` on (none where `from` is the argv's end: `sudo -s`,
66
+ * `chroot DIR`), as that launcher's row composes them (`launchedCommands`, shell-reentry-text.ts). Absent ⇒
67
+ * genuine control syntax, which IS one. */
68
+ readonly runsShell?: {
69
+ readonly launcher: number;
70
+ readonly from: number;
71
+ };
62
72
  };
63
73
  export interface ProgramPosition {
64
74
  /** Indices of the bare control words stripped in front (`!`, `if`, `time`, …), in order; their declared options