@sema-agent/core 5.32.0 → 5.34.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 (46) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/agents/cascade.d.ts +49 -1
  3. package/dist/agents/cascade.js +2 -2
  4. package/dist/agents/verify.d.ts +70 -4
  5. package/dist/agents/verify.js +62 -16
  6. package/dist/core/checkpoint-store.d.ts +95 -0
  7. package/dist/core/checkpoint-store.js +40 -0
  8. package/dist/core/hooks.d.ts +14 -6
  9. package/dist/core/hooks.js +14 -3
  10. package/dist/core/memory-engine/file-backend.d.ts +172 -22
  11. package/dist/core/memory-engine/file-backend.js +877 -79
  12. package/dist/core/memory-engine/memory-backend-contract.js +33 -0
  13. package/dist/core/push-queue.d.ts +4 -1
  14. package/dist/core/push-queue.js +2 -1
  15. package/dist/core/runner/assemble-result.d.ts +7 -0
  16. package/dist/core/runner/assemble-result.js +1 -1
  17. package/dist/core/runner/prepare-acquire-reconcile.d.ts +72 -0
  18. package/dist/core/runner/prepare-acquire-reconcile.js +126 -0
  19. package/dist/core/runner/prepare-config-doors.d.ts +140 -0
  20. package/dist/core/runner/prepare-config-doors.js +250 -0
  21. package/dist/core/runner/prepare-safety-scan.d.ts +53 -0
  22. package/dist/core/runner/prepare-safety-scan.js +80 -0
  23. package/dist/core/runner/prepare-task.d.ts +40 -81
  24. package/dist/core/runner/prepare-task.js +83 -586
  25. package/dist/core/runner/prepare-workspace-restore.d.ts +102 -0
  26. package/dist/core/runner/prepare-workspace-restore.js +144 -0
  27. package/dist/core/runner/runtask.js +18 -4
  28. package/dist/core/safe-notify.d.ts +9 -0
  29. package/dist/core/safe-notify.js +9 -0
  30. package/dist/core/tool-policy.d.ts +25 -0
  31. package/dist/core/types.d.ts +140 -13
  32. package/dist/index.d.ts +4 -3
  33. package/dist/index.js +1 -1
  34. package/dist/orchestration/workflow-governance.d.ts +6 -4
  35. package/dist/tools/fs/bash-readonly-classifier.d.ts +9 -3
  36. package/dist/tools/fs/bash-readonly-classifier.js +4 -1
  37. package/dist/tools/fs/fs-bash.d.ts +19 -3
  38. package/dist/tools/fs/fs-bash.js +26 -1
  39. package/dist/tools/fs/index.d.ts +20 -4
  40. package/dist/tools/fs/index.js +4 -1
  41. package/dist/tools/fs/read-deny.d.ts +66 -8
  42. package/dist/tools/fs/read-deny.js +75 -39
  43. package/dist/tools/fs/read-face.d.ts +3 -2
  44. package/dist/tools/fs/search.js +2 -0
  45. package/package.json +2 -1
  46. package/test/export-surface.snapshot.json +1552 -0
package/dist/index.js CHANGED
@@ -68,7 +68,7 @@ export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
68
68
  export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
69
69
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, } from "./tools/fs/index.js";
70
70
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
71
- export { READ_FACE_DEFAULT_DENY_ENTRIES, compileReadDeny, } from "./tools/fs/index.js";
71
+ export { READ_FACE_DEFAULT_DENY_ENTRIES, READ_FACE_BUILTIN_DENY_TABLE, READ_DENY_BUILTIN_TIERS, READ_DENY_DEFAULT_TIERS, resolveReadDenyBuiltins, compileReadDeny, } from "./tools/fs/index.js";
72
72
  export { deploymentReadFaceClampNotice, resolveReadFace } from "./tools/fs/index.js";
73
73
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, toolResultContentSegment, MAX_MINTED_TOOL_RESULT_REF_CHARS, assertToolResultProvenanceMatch, normalizeToolResultProvenance, toolResultProvenanceOf, ToolResultRefConflictError, TOOL_RESULT_REF_CONFLICT_CODE, } from "./core/tool-result-store.js";
74
74
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
@@ -50,10 +50,12 @@ export interface WorkflowAgentSpec {
50
50
  */
51
51
  readFace?: "roots";
52
52
  /**
53
- * design/199 件B, TIGHTEN-ONLY: additional read-deny entries. Add-only at every layer (the built-in
54
- * table and the baseline's entries are always in force and cannot be removed or replaced see
55
- * `compileReadDeny`'s zero-shrink contract and `tightenTaskSpec`'s union), so anything a script
56
- * writes here can only ever narrow what the child may read.
53
+ * design/199 件B, TIGHTEN-ONLY: additional read-deny entries. Add-only at every layer a governed
54
+ * SCRIPT can reach: the baseline's entries and the deployment-ACTIVE built-in set are always in
55
+ * force and a script cannot remove or replace them (`compileReadDeny` treats additions as
56
+ * add-only; `tightenTaskSpec` unions). Which built-in tiers are active is a DEPLOYMENT seat
57
+ * (#245: `readDenyBuiltinTiers`/`readDenyBuiltinExclude` on RunnerDeps) that no script-side key
58
+ * reaches — so anything a script writes here can only ever narrow what the child may read.
57
59
  */
58
60
  readDenyPatterns?: readonly ReadDenyEntry[];
59
61
  }
@@ -95,9 +95,15 @@ export interface BashReadonlyRootBoundary {
95
95
  * demotes the command (ask, never auto-allow), independently of the roots — in-root operands are
96
96
  * judged too. Returns the matched pattern, or null. TWO named residuals, both inherited from this
97
97
  * classifier's declared purity (synchronous, zero I/O — RB-448/RB-451 state the same scope for the
98
- * containment half): ① operand TARGET matching only — no ancestor intersection, so `grep -r x ~/`
99
- * whose operand is `~` itself does not demote here (§3.4; the recursive reach residual belongs to
100
- * the full-bash lane's honest scope note); LEXICAL only an in-root symlink whose target is a
98
+ * containment half): ① operand TARGET matching only — no ancestor intersection: the judge sees
99
+ * the operand's own resolved spelling, never its subtree. The RECURSIVE-reach half of that
100
+ * residual has since been narrowed (backlog #222): a listed recursive/expanding verb's path
101
+ * operand under this wired seat rides {@link CompoundReadonlyVerdict.recursiveReadPaths}
102
+ * (⊂ undecidedPaths), so `grep -r x ~/` no longer auto-allows — it demotes to ask through the
103
+ * undecided contract. What REMAINS of ① is the form table's stated open set (see the KNOWN OPEN
104
+ * SET note at {@link RECURSIVE_READ_FORMS}: a traversal verb absent there that a deployment
105
+ * allowlists classifies by its ordinary form and can auto-run a traversal unjudged);
106
+ * ② LEXICAL only — an in-root symlink whose target is a
101
107
  * guarded path reads as its innocent spelling here, exactly as it does for the containment half
102
108
  * (the enforcing/canonicalizing recheck is the bash_readonly leg's job via checkedPaths; the
103
109
  * classify auto-allow lane has no I/O seat by contract). The structured read faces judge BOTH
@@ -109,6 +109,7 @@ function foldQuoteRemovalToken(tok) {
109
109
  }
110
110
  return out;
111
111
  }
112
+ const POLL_LOOP_CAUSE_MAX_PATHS = 3;
112
113
  export function formatOutOfRootReadApprovalOption(directory) {
113
114
  const trimmed = directory.replace(/[/\\]+$/, "");
114
115
  const cut = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
@@ -843,7 +844,9 @@ export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
843
844
  if (verdict.reason !== undefined)
844
845
  return verdict.reason;
845
846
  if (verdict.recursiveReadPaths !== undefined) {
846
- return `the loop body reads recursively from ${verdict.recursiveReadPaths.join(", ")} — the traversal's reach is not covered by this lexical check, so it is not auto-allowed`;
847
+ const roots = verdict.recursiveReadPaths;
848
+ const shown = roots.slice(0, POLL_LOOP_CAUSE_MAX_PATHS);
849
+ return `this lexical check cannot bound what a recursive read form in the loop body traverses — ${roots.length} path(s) taken as traversal roots: ${shown.join(", ")}${roots.length > shown.length ? `, and ${roots.length - shown.length} more` : ""}`;
847
850
  }
848
851
  if (verdict.undecidedPaths !== undefined) {
849
852
  return `the loop body carries an unexpanded glob (${verdict.undecidedPaths.join(", ")}) — what a REPEATED read touches is decided at run time, so it is not auto-allowed`;
@@ -1,6 +1,7 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import { type TaskRegistry } from "../../core/task-registry.js";
3
3
  import { type CwdRef } from "./fs-shared.js";
4
+ import type { ReversibilityVerdict } from "../../core/types.js";
4
5
  import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
5
6
  /**
6
7
  * design/80 D-2 (part-1): a parsed-command classifier for the `bash` tool, exposed as a
@@ -36,10 +37,25 @@ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
36
37
  * allowlisted readers + literal `sleep`, the same read boundary, everything else banned; the full
37
38
  * safety argument lives on that function). Additive by construction: the plain verdict is tried
38
39
  * first and is never overridden.
40
+ *
41
+ * backlog #239 — the RECURSIVE-READ demotion returns a STRUCTURED cause alongside the verdict. That
42
+ * demotion is the one arm whose reason is genuinely unrecoverable downstream: a recursive read is
43
+ * refused for what the traversal REACHES, and the reached set is by definition not in the command text
44
+ * an approval card shows. The cause is minted from the SAME classification pass that produced the
45
+ * verdict, so nothing re-parses the command to recover it.
46
+ *
47
+ * It is a `cause` (code + operand arrays), not a `reason` sentence, and that is a correction rather than
48
+ * a preference: every sentence tried here was true of one call and false of another — "a recursive read
49
+ * of X" is false for the zero-operand over-fire, "operand the check treated as a root" is false when the
50
+ * check SYNTHESIZES the root, and any of them, flattened into one length-capped string, silently dropped
51
+ * whichever family sorted last. The consumer now writes the sentence, from a code that cannot go stale
52
+ * and arrays that cannot be truncated into a lie.
53
+ *
54
+ * The other non-reversible arms stay bare on purpose — their cause is legible in the command itself (a
55
+ * write verb, a pipe, an unlisted name), and manufacturing an account for them would put text on a card
56
+ * that adds nothing the reader cannot already see.
39
57
  */
40
- export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => {
41
- reversible: boolean;
42
- };
58
+ export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => ReversibilityVerdict;
43
59
  /**
44
60
  * design/199 D-6 — the FULL shell's contract id, single-sourced: both shell faces share the wire
45
61
  * name "Bash", so this id is the ONE structural discriminator between the write-capable shell and
@@ -10,7 +10,18 @@ import { imageMagicMatches, withinAnyRoot } from "./safety.js";
10
10
  import { isRemoteExecutionEnv, hasDestroy, isIsolated } from "../../core/remote-env.js";
11
11
  import { ghRateLimitHint } from "./gh-rate-limit.js";
12
12
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
13
+ import { PROBE_CAUSE_PATH_MAX } from "../../core/checkpoint-store.js";
14
+ import { inlineUntrusted } from "../../core/untrusted-text.js";
13
15
  import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop, classifyCompoundReadonlyDetailed, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
16
+ const RECURSIVE_CAUSE_MAX_PATHS = 3;
17
+ const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
18
+ function operandFamily(paths) {
19
+ const enc = (s) => s.replace(/%/g, "%25").replace(/;/g, "%3B").replace(/:/g, "%3A");
20
+ return {
21
+ shown: paths.slice(0, RECURSIVE_CAUSE_MAX_PATHS).map((p) => enc(inlineUntrusted(p, PROBE_CAUSE_PATH_MAX))),
22
+ total: paths.length,
23
+ };
24
+ }
14
25
  export function bashReversibilityProbe(allow, boundary) {
15
26
  const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
16
27
  return (args) => {
@@ -23,8 +34,22 @@ export function bashReversibilityProbe(allow, boundary) {
23
34
  const resolved = typeof boundary === "function" ? boundary() : boundary;
24
35
  const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
25
36
  if (detailed.reason === undefined) {
26
- if (detailed.undecidedPaths !== undefined && detailed.undecidedPaths.length > 0)
37
+ if (detailed.undecidedPaths !== undefined && detailed.undecidedPaths.length > 0) {
38
+ const recursive = detailed.recursiveReadPaths;
39
+ if (recursive !== undefined && recursive.length > 0) {
40
+ const recursiveSet = new Set(recursive);
41
+ const others = detailed.undecidedPaths.filter((p) => !recursiveSet.has(p));
42
+ return {
43
+ reversible: false,
44
+ cause: {
45
+ code: RECURSIVE_READ_CAUSE_CODE,
46
+ roots: operandFamily(recursive),
47
+ ...(others.length > 0 ? { further: operandFamily(others) } : {}),
48
+ },
49
+ };
50
+ }
27
51
  return { reversible: false };
52
+ }
28
53
  return { reversible: true };
29
54
  }
30
55
  return { reversible: classifyBoundedReadonlyPollLoop(command, allowSet, resolved) === undefined };
@@ -120,11 +120,27 @@ export interface HandsToolkitOptions {
120
120
  monitorToolActive?: boolean;
121
121
  /** design/199 件B — ADDITIONS to the built-in sensitive-path READ deny set
122
122
  * ({@link import("./read-deny.js").READ_FACE_DEFAULT_DENY_ENTRIES}), judged by the structured read
123
- * faces (Read/Grep/Glob/RepoMap and their traversals) in BOTH containment modes. Add-only at every
124
- * layer (D-4 zero-shrink ruling): the built-ins are always in force, `[]` ≡ absent (union
125
- * identity), and there is no replacement escape hatch. Bad entry shapes throw at wiring time
126
- * (#123). The write faces are untouched (their guard is createSensitivePathPolicy). */
123
+ * faces (Read/Grep/Glob/RepoMap and their traversals) in BOTH containment modes. Add-only:
124
+ * additions union with the active built-ins, `[]` ≡ absent (union identity), and additions can
125
+ * never remove a built-in the one removal channel is the deployment's built-in configuration
126
+ * ({@link readDenyBuiltinTiers} / {@link readDenyBuiltinExclude}, #245 revision of D-4). Bad entry
127
+ * shapes throw at wiring time (#123). The write faces are untouched (their guard is
128
+ * createSensitivePathPolicy). */
127
129
  readDenyPatterns?: readonly ReadDenyEntry[];
130
+ /** #245 — the deployment's built-in-table TIER selection ({@link import("./read-deny.js").ReadDenyBuiltinConfig}
131
+ * `tiers` key): EXACTLY the listed tiers of {@link import("./read-deny.js").READ_FACE_BUILTIN_DENY_TABLE}
132
+ * are active (`[]` = none — explicit and legal); absent = the default selection
133
+ * ({@link import("./read-deny.js").READ_DENY_DEFAULT_TIERS} — every tier except `shell-history`).
134
+ * Composes with {@link readDenyBuiltinExclude} (tier selection first, then row removal) and never
135
+ * touches the additions seats. Unknown tier names throw at wiring time (#123). Deployment seat
136
+ * ONLY — deliberately not a TaskSpec key, so no task can widen below its deployment. */
137
+ readDenyBuiltinTiers?: readonly string[];
138
+ /** #245 — per-row removal from the built-in table by STABLE NAME (= the row's canonical pattern
139
+ * text, e.g. `".ssh"`, `".config/gcloud"`), applied after {@link readDenyBuiltinTiers} selection.
140
+ * Naming a row of an inactive tier is a satisfied intent (no-op); a name matching NO table row
141
+ * throws at wiring time (#123 — never silently ignored). Deployment seat ONLY, same clamp
142
+ * argument as the tiers key. */
143
+ readDenyBuiltinExclude?: readonly string[];
128
144
  /** design/199 件A — the READ-face containment state for the structured read faces
129
145
  * ({@link import("./read-face.js").ReadFace}; resolved through the SAME
130
146
  * {@link import("./read-face.js").resolveReadFace} order prepare-task uses). Absent ⇒ the
@@ -34,7 +34,10 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
34
34
  scope: c.principal ?? opts.taskScope,
35
35
  ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
36
36
  }, env);
37
- const readDeny = compileReadDeny(opts.readDenyPatterns ?? [], "HandsToolkitOptions.readDenyPatterns");
37
+ const readDeny = compileReadDeny(opts.readDenyPatterns ?? [], "HandsToolkitOptions.readDenyPatterns", {
38
+ ...(opts.readDenyBuiltinTiers !== undefined ? { tiers: opts.readDenyBuiltinTiers } : {}),
39
+ ...(opts.readDenyBuiltinExclude !== undefined ? { exclude: opts.readDenyBuiltinExclude } : {}),
40
+ });
38
41
  const readFace = resolveReadFace({
39
42
  depsReadFace: opts.readFace,
40
43
  readOnlyMount: readOnly,
@@ -52,9 +52,30 @@ export interface ReadDenyMatcher {
52
52
  readonly rgProbeGlobs: readonly ReadDenyRgGlob[];
53
53
  }
54
54
  /**
55
- * READ_FACE_DEFAULT_DENY_ENTRIES the built-in table (D-4 ruling: ONE tier, credential-class path
56
- * families; the design's `outside_workspace` tier was ruled OUT, and the dotenv family is deliberately
57
- * NOT here workspace `.env` files are working material for the tasks this engine runs).
55
+ * The closed set of built-in tiers (#245). A tier is the deployment's selection unit
56
+ * ({@link ReadDenyBuiltinConfig.tiers}); every {@link READ_FACE_BUILTIN_DENY_TABLE} row belongs to
57
+ * exactly one. Unknown tier names refuse loudly (#123) this array is the validation source and the
58
+ * admin-face vocabulary, never re-spelled downstream.
59
+ */
60
+ export declare const READ_DENY_BUILTIN_TIERS: readonly ["credentials", "shell-history", "browser", "wallet"];
61
+ /** One member of {@link READ_DENY_BUILTIN_TIERS}. */
62
+ export type ReadDenyBuiltinTier = (typeof READ_DENY_BUILTIN_TIERS)[number];
63
+ /**
64
+ * One row of the built-in table: the canonical deny pattern (also the row's STABLE NAME — the
65
+ * `readDenyBuiltinExclude` key; renaming a pattern is a semantic change and deliberately invalidates
66
+ * configs naming the old spelling, loudly) and the tier it belongs to.
67
+ */
68
+ export interface ReadDenyBuiltinRow {
69
+ readonly pattern: string;
70
+ readonly tier: ReadDenyBuiltinTier;
71
+ }
72
+ /**
73
+ * READ_FACE_BUILTIN_DENY_TABLE — the FULL built-in table with tier metadata (#245: the material
74
+ * basis of the deployment admin face — what an org administrator "sees" before selecting tiers /
75
+ * excluding rows). Activation is a separate fact: the default face is the table under
76
+ * {@link READ_DENY_DEFAULT_TIERS} (shell-history rows exist here but are OFF by default).
77
+ * The dotenv family is deliberately NOT here — workspace `.env` files are working material for the
78
+ * tasks this engine runs (the design's `outside_workspace` tier was also ruled OUT).
58
79
  *
59
80
  * Each row states its tradeoff. Rows are matched as segment runs anywhere in the path, so a repo
60
81
  * fixture spelled `fixtures/.ssh/id_rsa` is refused too — deliberate: the refusal names the pattern,
@@ -67,6 +88,39 @@ export interface ReadDenyMatcher {
67
88
  * `.mcp.json` (agent config is routinely read for debugging); the engine's own data root
68
89
  * (transcript integrity policy owns it with an ask on the write face).
69
90
  */
91
+ export declare const READ_FACE_BUILTIN_DENY_TABLE: readonly ReadDenyBuiltinRow[];
92
+ /**
93
+ * The DEFAULT tier selection (#245 ruling ②): every tier EXCEPT `shell-history` — history files are
94
+ * unrestricted unless a deployment opts the tier in. Derived, not re-listed: a future tier added to
95
+ * the closed set is default-ON unless this expression is deliberately revisited.
96
+ */
97
+ export declare const READ_DENY_DEFAULT_TIERS: readonly ReadDenyBuiltinTier[];
98
+ /**
99
+ * The deployment's built-in-table configuration (#245 — deployment seat ONLY: RunnerDeps /
100
+ * HandsToolkitOptions; deliberately NOT a TaskSpec key and NOT in the governed workflow whitelist,
101
+ * so no task or governed script can widen below its deployment).
102
+ * - `tiers` — EXACTLY-these tier selection: listed tiers are active, unlisted ones are not
103
+ * (`[]` = no built-in tiers at all — an explicit, legal posture; absent = {@link READ_DENY_DEFAULT_TIERS}).
104
+ * - `exclude` — per-row removal by canonical pattern name, applied AFTER tier selection.
105
+ * Unknown tier names / unknown row names refuse loudly (#123) — never silently ignored.
106
+ */
107
+ export interface ReadDenyBuiltinConfig {
108
+ tiers?: readonly string[];
109
+ exclude?: readonly string[];
110
+ }
111
+ /**
112
+ * Resolve the ACTIVE built-in rows under a deployment configuration (validated, loud — #123's three
113
+ * bad-value states all throw: garbage shapes, unknown tier names, unknown row names). The one
114
+ * resolution used by {@link compileReadDeny}; exported so an admin face can preview the effective
115
+ * table under a candidate configuration with the engine's own rules.
116
+ */
117
+ export declare function resolveReadDenyBuiltins(config?: ReadDenyBuiltinConfig): readonly ReadDenyBuiltinRow[];
118
+ /**
119
+ * READ_FACE_DEFAULT_DENY_ENTRIES — the DEFAULT-ACTIVE built-in set: {@link READ_FACE_BUILTIN_DENY_TABLE}
120
+ * under {@link READ_DENY_DEFAULT_TIERS} (#245: shell-history rows are in the table, not in this face).
121
+ * Kept as the historical name for consumers rendering "what does an unconfigured deployment refuse";
122
+ * the tier-annotated table is the admin-face export.
123
+ */
70
124
  export declare const READ_FACE_DEFAULT_DENY_ENTRIES: readonly ReadDenyEntry[];
71
125
  /** A compiled segment-run pattern (shared engine — the write guard delegates here too). */
72
126
  export interface CompiledSegmentPattern {
@@ -102,9 +156,13 @@ export declare function matchSegmentPatterns(path: string, compiled: readonly Co
102
156
  */
103
157
  export declare function persistedReadDenyEntryProblem(e: unknown): string | null;
104
158
  /**
105
- * Compile the read-face deny judge: built-in table ∪ additions (add-only at every layer — D-4
106
- * zero-shrink; `[]` additions ≡ absent, deliberately a no-op rather than a refusal: an array API
107
- * cannot observe a "replace" intent, and union-with-empty is the identity, not a silent fallback).
108
- * Bad entry shapes / zero-segment patterns throw loudly, naming the layer (#123).
159
+ * Compile the read-face deny judge: ACTIVE built-in rows ∪ additions. Additions are add-only at every
160
+ * task layer (`[]` additions ≡ absent, deliberately a no-op rather than a refusal: an array API
161
+ * cannot observe a "replace" intent, and union-with-empty is the identity, not a silent fallback);
162
+ * the built-in half is selected by the DEPLOYMENT's `builtin` config (#245 — absent = the default
163
+ * tier selection, see {@link resolveReadDenyBuiltins}). A deployment that turned a tier off can still
164
+ * re-add single patterns through the additions seats — they then ride as ordinary additions.
165
+ * Bad entry shapes / zero-segment patterns / unknown tier or row names throw loudly, naming the
166
+ * layer or knob (#123).
109
167
  */
110
- export declare function compileReadDeny(additions?: readonly ReadDenyEntry[], layer?: string): ReadDenyMatcher;
168
+ export declare function compileReadDeny(additions?: readonly ReadDenyEntry[], layer?: string, builtin?: ReadDenyBuiltinConfig): ReadDenyMatcher;
@@ -1,40 +1,76 @@
1
- export const READ_FACE_DEFAULT_DENY_ENTRIES = [
2
- ".ssh",
3
- "id_rsa*",
4
- "id_ed25519*",
5
- "id_ecdsa*",
6
- ".gnupg",
7
- ".aws",
8
- ".config/gcloud",
9
- ".azure",
10
- ".kube",
11
- ".netrc",
12
- "_netrc",
13
- ".git-credentials",
14
- ".docker/config.json",
15
- ".config/gh",
16
- ".npmrc",
17
- ".pypirc",
18
- ".local/share/keyrings",
19
- "Library/Keychains",
20
- ".bash_history",
21
- ".zsh_history",
22
- "Library/Application Support/Google/Chrome",
23
- "Library/Application Support/Firefox",
24
- "Library/Safari",
25
- ".config/google-chrome",
26
- ".config/chromium",
27
- ".mozilla/firefox",
28
- "AppData/Local/Google/Chrome/User Data",
29
- "AppData/Local/Microsoft/Edge/User Data",
30
- "AppData/Roaming/Mozilla/Firefox",
31
- ".bitcoin",
32
- ".ethereum",
33
- ".electrum",
34
- "Library/Application Support/Exodus",
35
- "Library/Application Support/Ledger Live",
36
- "wallet.dat",
1
+ export const READ_DENY_BUILTIN_TIERS = ["credentials", "shell-history", "browser", "wallet"];
2
+ export const READ_FACE_BUILTIN_DENY_TABLE = [
3
+ { pattern: ".ssh", tier: "credentials" },
4
+ { pattern: "id_rsa*", tier: "credentials" },
5
+ { pattern: "id_ed25519*", tier: "credentials" },
6
+ { pattern: "id_ecdsa*", tier: "credentials" },
7
+ { pattern: ".gnupg", tier: "credentials" },
8
+ { pattern: ".aws", tier: "credentials" },
9
+ { pattern: ".config/gcloud", tier: "credentials" },
10
+ { pattern: ".azure", tier: "credentials" },
11
+ { pattern: ".kube", tier: "credentials" },
12
+ { pattern: ".netrc", tier: "credentials" },
13
+ { pattern: "_netrc", tier: "credentials" },
14
+ { pattern: ".git-credentials", tier: "credentials" },
15
+ { pattern: ".docker/config.json", tier: "credentials" },
16
+ { pattern: ".config/gh", tier: "credentials" },
17
+ { pattern: ".npmrc", tier: "credentials" },
18
+ { pattern: ".pypirc", tier: "credentials" },
19
+ { pattern: ".local/share/keyrings", tier: "credentials" },
20
+ { pattern: "Library/Keychains", tier: "credentials" },
21
+ { pattern: ".bash_history", tier: "shell-history" },
22
+ { pattern: ".zsh_history", tier: "shell-history" },
23
+ { pattern: "Library/Application Support/Google/Chrome", tier: "browser" },
24
+ { pattern: "Library/Application Support/Firefox", tier: "browser" },
25
+ { pattern: "Library/Safari", tier: "browser" },
26
+ { pattern: ".config/google-chrome", tier: "browser" },
27
+ { pattern: ".config/chromium", tier: "browser" },
28
+ { pattern: ".mozilla/firefox", tier: "browser" },
29
+ { pattern: "AppData/Local/Google/Chrome/User Data", tier: "browser" },
30
+ { pattern: "AppData/Local/Microsoft/Edge/User Data", tier: "browser" },
31
+ { pattern: "AppData/Roaming/Mozilla/Firefox", tier: "browser" },
32
+ { pattern: ".bitcoin", tier: "wallet" },
33
+ { pattern: ".ethereum", tier: "wallet" },
34
+ { pattern: ".electrum", tier: "wallet" },
35
+ { pattern: "Library/Application Support/Exodus", tier: "wallet" },
36
+ { pattern: "Library/Application Support/Ledger Live", tier: "wallet" },
37
+ { pattern: "wallet.dat", tier: "wallet" },
37
38
  ];
39
+ export const READ_DENY_DEFAULT_TIERS = READ_DENY_BUILTIN_TIERS.filter((t) => t !== "shell-history");
40
+ export function resolveReadDenyBuiltins(config) {
41
+ let activeTiers;
42
+ if (config?.tiers === undefined) {
43
+ activeTiers = new Set(READ_DENY_DEFAULT_TIERS);
44
+ }
45
+ else {
46
+ if (!Array.isArray(config.tiers)) {
47
+ throw new Error(`readDenyBuiltinTiers: expected an array of tier names, got ${JSON.stringify(config.tiers)}.`);
48
+ }
49
+ for (const t of config.tiers) {
50
+ if (typeof t !== "string" || !READ_DENY_BUILTIN_TIERS.includes(t)) {
51
+ throw new Error(`readDenyBuiltinTiers: unknown tier ${JSON.stringify(t)} — known tiers: ${READ_DENY_BUILTIN_TIERS.join(", ")}.`);
52
+ }
53
+ }
54
+ activeTiers = new Set(config.tiers);
55
+ }
56
+ let excluded;
57
+ if (config?.exclude === undefined) {
58
+ excluded = new Set();
59
+ }
60
+ else {
61
+ if (!Array.isArray(config.exclude)) {
62
+ throw new Error(`readDenyBuiltinExclude: expected an array of built-in row names (canonical pattern texts), got ${JSON.stringify(config.exclude)}.`);
63
+ }
64
+ for (const name of config.exclude) {
65
+ if (typeof name !== "string" || !READ_FACE_BUILTIN_DENY_TABLE.some((r) => r.pattern === name)) {
66
+ throw new Error(`readDenyBuiltinExclude: ${JSON.stringify(name)} names no built-in row — row names are the canonical pattern texts of READ_FACE_BUILTIN_DENY_TABLE (e.g. ".ssh", ".config/gcloud").`);
67
+ }
68
+ }
69
+ excluded = new Set(config.exclude);
70
+ }
71
+ return READ_FACE_BUILTIN_DENY_TABLE.filter((r) => activeTiers.has(r.tier) && !excluded.has(r.pattern));
72
+ }
73
+ export const READ_FACE_DEFAULT_DENY_ENTRIES = resolveReadDenyBuiltins().map((r) => r.pattern);
38
74
  function asciiLower(s) {
39
75
  let out = "";
40
76
  for (let i = 0; i < s.length; i++) {
@@ -107,7 +143,7 @@ export function persistedReadDenyEntryProblem(e) {
107
143
  return "pattern is not in canonical a/b form";
108
144
  return null;
109
145
  }
110
- export function compileReadDeny(additions = [], layer = "additions") {
146
+ export function compileReadDeny(additions = [], layer = "additions", builtin) {
111
147
  const normalized = [];
112
148
  const seen = new Set();
113
149
  const push = (e) => {
@@ -117,8 +153,8 @@ export function compileReadDeny(additions = [], layer = "additions") {
117
153
  seen.add(key);
118
154
  normalized.push(e);
119
155
  };
120
- for (const entry of READ_FACE_DEFAULT_DENY_ENTRIES)
121
- push(normalizeEntry(entry, "built-in"));
156
+ for (const row of resolveReadDenyBuiltins(builtin))
157
+ push(normalizeEntry(row.pattern, "built-in"));
122
158
  for (const entry of additions)
123
159
  push(normalizeEntry(entry, layer));
124
160
  const compiled = normalized.map((e) => {
@@ -5,8 +5,9 @@ export type ReadFace = "open" | "roots";
5
5
  /** Loud value gate (#123): a `readFace` seat carries exactly "open" | "roots" — anything else
6
6
  * (case variants, truthy garbage) refuses at wiring time, never folds to a default. Exported
7
7
  * module-internally (not on the package surface) so prepare's unconditional config-guard leg can
8
- * screen the seats on runs that never mount hands the resolver only runs beside a mount, and a
9
- * garbage value must not become silently legal on the hands-less path (5.30 re-review). */
8
+ * screen the seats on runs that never mount hands early and unconditionally (since #242 the
9
+ * resolver itself ALSO runs on every hands-less leg; this screen still fires first, so a garbage
10
+ * value refuses before any leg-shape branching). */
10
11
  export declare function assertReadFaceValue(v: unknown, seat: string): ReadFace | undefined;
11
12
  /** Inputs to {@link resolveReadFace} — all structural/declaration facts, never permission modes
12
13
  * (a deliberate axis separation: this shape carries facts, not verdicts). */
@@ -1147,6 +1147,8 @@ async function rgDenyExistenceProbe(env, root, deny, target, signal) {
1147
1147
  return { kind: "probe_failed" };
1148
1148
  const first = res.value.stdout.split("\n").find((l) => l.trim().length > 0);
1149
1149
  if (first !== undefined) {
1150
+ if (/^__RG_PROBE_EXIT_\d+$/.test(first.trim()))
1151
+ return { kind: "probe_failed" };
1150
1152
  const hit = deny.matchPath(first.trim());
1151
1153
  return { kind: "existence", ...(hit !== null ? { patterns: [hit.pattern] } : {}) };
1152
1154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.32.0",
3
+ "version": "5.34.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "files": [
38
38
  "dist",
39
+ "test/export-surface.snapshot.json",
39
40
  "README.md",
40
41
  "NOTICE.md",
41
42
  "CHANGELOG.md",