@sema-agent/core 7.33.7 → 7.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 (206) hide show
  1. package/CHANGELOG.md +357 -0
  2. package/dist/agents/cross-session-judge.d.ts +16 -7
  3. package/dist/agents/cross-session-judge.js +18 -15
  4. package/dist/agents/peer-session-drain.d.ts +4 -0
  5. package/dist/agents/peer-session-drain.js +5 -2
  6. package/dist/agents/repair-loop.d.ts +1 -1
  7. package/dist/agents/subagent.d.ts +3 -0
  8. package/dist/agents/subagent.js +12 -3
  9. package/dist/brain/anthropic.js +1 -0
  10. package/dist/brain/open-responses.js +1 -0
  11. package/dist/brain/openai.js +1 -0
  12. package/dist/brain/stream-engine.d.ts +3 -0
  13. package/dist/brain/stream-engine.js +2 -1
  14. package/dist/core/a2a.d.ts +3 -1
  15. package/dist/core/a2a.js +1 -1
  16. package/dist/core/ask-origin.d.ts +351 -154
  17. package/dist/core/ask-origin.js +223 -45
  18. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  19. package/dist/core/ask-unresolvable-notice.js +4 -3
  20. package/dist/core/auto-mode-arming.d.ts +17 -4
  21. package/dist/core/auto-mode-arming.js +31 -6
  22. package/dist/core/auto-mode-defaults.d.ts +16 -7
  23. package/dist/core/auto-mode-defaults.js +2 -1
  24. package/dist/core/auto-mode-directive.d.ts +18 -30
  25. package/dist/core/auto-mode-directive.js +9 -18
  26. package/dist/core/auto-mode-prompt.d.ts +62 -1
  27. package/dist/core/auto-mode-prompt.js +42 -4
  28. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  29. package/dist/core/auto-mode-rebuild.js +1 -0
  30. package/dist/core/auto-mode.d.ts +32 -3
  31. package/dist/core/auto-mode.js +80 -41
  32. package/dist/core/checkpoint-store.d.ts +50 -35
  33. package/dist/core/checkpoint-store.js +58 -3
  34. package/dist/core/child-clamp-seats.js +1 -0
  35. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  36. package/dist/core/classifier-unavailable-arm.js +17 -0
  37. package/dist/core/effective-path-target.d.ts +12 -0
  38. package/dist/core/effective-path-target.js +18 -7
  39. package/dist/core/engine-notice.d.ts +53 -2
  40. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  41. package/dist/core/fs-write-gate-policy.js +4 -1
  42. package/dist/core/gate-fold.js +17 -1
  43. package/dist/core/gate-lanes.js +111 -46
  44. package/dist/core/gate-outcome.d.ts +40 -7
  45. package/dist/core/gate-outcome.js +26 -4
  46. package/dist/core/governance-codes.d.ts +1 -1
  47. package/dist/core/governance-codes.js +12 -0
  48. package/dist/core/hooks.d.ts +50 -29
  49. package/dist/core/hooks.js +1 -3
  50. package/dist/core/mcp.js +1 -1
  51. package/dist/core/mode-policy.d.ts +182 -0
  52. package/dist/core/mode-policy.js +179 -0
  53. package/dist/core/oracle-isolation.d.ts +3 -3
  54. package/dist/core/permission-rule-model.d.ts +16 -3
  55. package/dist/core/permission-rule-model.js +36 -3
  56. package/dist/core/permission-rule-provider.d.ts +22 -11
  57. package/dist/core/permission-rule-provider.js +17 -7
  58. package/dist/core/permission-rules.d.ts +23 -1
  59. package/dist/core/permission-rules.js +35 -8
  60. package/dist/core/persisted-rule-arms.d.ts +10 -9
  61. package/dist/core/persisted-rule-arms.js +9 -6
  62. package/dist/core/present-plan-tool.d.ts +1 -1
  63. package/dist/core/present-plan-tool.js +3 -3
  64. package/dist/core/read-only-shell.d.ts +19 -19
  65. package/dist/core/read-only-shell.js +38 -12
  66. package/dist/core/retired-keys.d.ts +9 -3
  67. package/dist/core/retired-keys.js +5 -1
  68. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  69. package/dist/core/runner/classifier-ladder.js +29 -0
  70. package/dist/core/runner/contracts.d.ts +37 -35
  71. package/dist/core/runner/denial-limit-arms.d.ts +30 -7
  72. package/dist/core/runner/denial-limit-arms.js +15 -9
  73. package/dist/core/runner/env-capability-census.js +1 -1
  74. package/dist/core/runner/gate-exit.d.ts +30 -4
  75. package/dist/core/runner/gate-exit.js +28 -7
  76. package/dist/core/runner/permission-rule-lanes.d.ts +9 -5
  77. package/dist/core/runner/permission-rule-lanes.js +44 -40
  78. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  79. package/dist/core/runner/persisted-path-rows.js +89 -0
  80. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  81. package/dist/core/runner/prepare-artifact.js +9 -3
  82. package/dist/core/runner/prepare-ask-lane.d.ts +5 -3
  83. package/dist/core/runner/prepare-ask-lane.js +1 -1
  84. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  85. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  86. package/dist/core/runner/prepare-gate-stations.d.ts +33 -6
  87. package/dist/core/runner/prepare-gate-stations.js +32 -19
  88. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  89. package/dist/core/runner/prepare-hands-readface.js +64 -53
  90. package/dist/core/runner/prepare-inherited-gate.d.ts +22 -22
  91. package/dist/core/runner/prepare-inherited-gate.js +37 -23
  92. package/dist/core/runner/prepare-park-ask.d.ts +2 -2
  93. package/dist/core/runner/prepare-park-ask.js +2 -1
  94. package/dist/core/runner/prepare-policy-chain.d.ts +22 -2
  95. package/dist/core/runner/prepare-policy-chain.js +39 -12
  96. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  97. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  98. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  99. package/dist/core/runner/prepare-run-refs.js +9 -4
  100. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  101. package/dist/core/runner/prepare-safety-scan.js +19 -3
  102. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  103. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  104. package/dist/core/runner/prepare-task.js +51 -22
  105. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  106. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  107. package/dist/core/runner/read-root-grant.d.ts +9 -0
  108. package/dist/core/runner/read-root-grant.js +12 -0
  109. package/dist/core/runner/retired-runner-deps.js +1 -0
  110. package/dist/core/runner/run-identity-wiring.js +1 -1
  111. package/dist/core/runner/run-leg.js +24 -15
  112. package/dist/core/runner/run-notification-lane.js +1 -0
  113. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  114. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  115. package/dist/core/runner/run-turn-boundary.js +1 -1
  116. package/dist/core/runner/runtask.js +3 -0
  117. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  118. package/dist/core/runner/stream-mode-verb.js +19 -0
  119. package/dist/core/runner/turn-attachments.d.ts +3 -3
  120. package/dist/core/runner/turn-attachments.js +1 -1
  121. package/dist/core/runner/write-protection-station.d.ts +7 -1
  122. package/dist/core/runner/write-protection-station.js +6 -3
  123. package/dist/core/runner-deps.d.ts +31 -34
  124. package/dist/core/runtime-caps.d.ts +12 -3
  125. package/dist/core/safety-axis-vocab.d.ts +2 -2
  126. package/dist/core/safety-axis-vocab.js +1 -1
  127. package/dist/core/secret-env.d.ts +16 -2
  128. package/dist/core/secret-env.js +25 -1
  129. package/dist/core/sensitive-path-policy.d.ts +4 -2
  130. package/dist/core/sensitive-path-policy.js +39 -30
  131. package/dist/core/spec-contract.d.ts +15 -0
  132. package/dist/core/spec-contract.js +19 -10
  133. package/dist/core/task-event.d.ts +1 -1
  134. package/dist/core/task-result.d.ts +1 -1
  135. package/dist/core/task-spec.d.ts +41 -89
  136. package/dist/core/task-stream.d.ts +16 -0
  137. package/dist/core/terminal-cause.js +1 -0
  138. package/dist/core/tighten-task-spec.d.ts +2 -2
  139. package/dist/core/tighten-task-spec.js +12 -14
  140. package/dist/core/tool-catalog-entries.js +4 -3
  141. package/dist/core/tool-policy.d.ts +54 -25
  142. package/dist/core/tool-policy.js +43 -2
  143. package/dist/core/tool-registry.d.ts +2 -0
  144. package/dist/core/tool-registry.js +3 -0
  145. package/dist/core/tool-spec.d.ts +8 -9
  146. package/dist/core/trace.d.ts +7 -6
  147. package/dist/core/types.d.ts +2 -1
  148. package/dist/core/wiring-manifest.d.ts +31 -5
  149. package/dist/core/wiring-manifest.js +28 -1
  150. package/dist/core/write-protect.d.ts +11 -1
  151. package/dist/core/write-protect.js +26 -15
  152. package/dist/engine/execution-env/kill-tree.js +4 -1
  153. package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
  154. package/dist/engine/execution-env/node-execution-env.js +34 -35
  155. package/dist/engine/harness/types.d.ts +6 -7
  156. package/dist/engine/llm/types.d.ts +2 -4
  157. package/dist/engine/loop/types.d.ts +7 -0
  158. package/dist/index.d.ts +15 -5
  159. package/dist/index.js +12 -3
  160. package/dist/orchestration/run-spec.d.ts +4 -0
  161. package/dist/orchestration/run-spec.js +4 -3
  162. package/dist/orchestration/workflow-governance.js +1 -1
  163. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  164. package/dist/orchestration/workflow-size-guideline.js +2 -2
  165. package/dist/stores/file/fs-atomic.js +2 -1
  166. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  167. package/dist/tools/artifact/artifact-tool.js +16 -1
  168. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  169. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  170. package/dist/tools/fs/containment-face.d.ts +9 -3
  171. package/dist/tools/fs/containment-face.js +2 -0
  172. package/dist/tools/fs/file-read-check.d.ts +49 -0
  173. package/dist/tools/fs/file-read-check.js +71 -0
  174. package/dist/tools/fs/fs-bash.d.ts +3 -3
  175. package/dist/tools/fs/fs-read.d.ts +1 -0
  176. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  177. package/dist/tools/fs/fs-search-tools.js +4 -4
  178. package/dist/tools/fs/fs-shared.js +20 -6
  179. package/dist/tools/fs/fs-write.d.ts +4 -3
  180. package/dist/tools/fs/fs-write.js +9 -9
  181. package/dist/tools/fs/index.d.ts +39 -3
  182. package/dist/tools/fs/index.js +38 -15
  183. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  184. package/dist/tools/fs/read-deny-home.js +225 -0
  185. package/dist/tools/fs/read-deny.d.ts +176 -34
  186. package/dist/tools/fs/read-deny.js +268 -61
  187. package/dist/tools/fs/safety.d.ts +6 -1
  188. package/dist/tools/fs/safety.js +18 -5
  189. package/dist/tools/fs/search.d.ts +3 -0
  190. package/dist/tools/fs/search.js +47 -9
  191. package/dist/tools/fs/shell-read-check.d.ts +42 -11
  192. package/dist/tools/fs/shell-read-check.js +188 -13
  193. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  194. package/dist/tools/fs/shell-read-establish.js +134 -21
  195. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  196. package/dist/tools/fs/shell-read-identity.js +21 -0
  197. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  198. package/dist/tools/fs/shell-read-judge.js +43 -18
  199. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  200. package/dist/tools/fs/shell-read-plan.js +26 -4
  201. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  202. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  203. package/package.json +3 -1
  204. package/test/export-surface.snapshot.json +103 -11
  205. package/wire-changes/core@7.33.8.json +165 -0
  206. package/wire-changes/core@7.34.0.json +2621 -0
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
3
  import { brandPolicyAskClass } from "./ask-class.js";
4
+ import { decisionAskClassesOf, headlessDenialMessage } from "./ask-origin.js";
4
5
  import { ownReadRootCandidate, readRootGrantSeat } from "./read-root-grant-screen.js";
5
6
  import { decidersNote, settledByPerson } from "./gate-outcome.js";
6
7
  import { sanitizeAutoModeArmingRecipe } from "./auto-mode-arming.js";
@@ -121,6 +122,26 @@ export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "una
121
122
  export function decisionText(d) {
122
123
  return d.message;
123
124
  }
125
+ export function toolPolicyPathDenyRules(policies) {
126
+ return policies.flatMap((p) => (Array.isArray(p?.pathDenyRules) ? p.pathDenyRules : []));
127
+ }
128
+ export function toolPolicyWriteTargetGuard(policies) {
129
+ const guards = policies.flatMap((p) => (typeof p?.writeTargetGuard === "function" ? [p.writeTargetGuard] : []));
130
+ if (guards.length === 0)
131
+ return undefined;
132
+ return async (target, signal) => {
133
+ for (const guard of guards) {
134
+ try {
135
+ if ((await guard(target, signal)) !== false)
136
+ return true;
137
+ }
138
+ catch {
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ };
144
+ }
124
145
  export function namespacedCoveringEntries(entries) {
125
146
  return (entries ?? []).filter(isNamespacedCoveringRuleName);
126
147
  }
@@ -452,6 +473,8 @@ export function createApprovalPolicy(opts) {
452
473
  }
453
474
  export function combinePolicies(...policies) {
454
475
  const nameSets = policies.flatMap(toolPolicyNameSets);
476
+ const pathDenyRules = toolPolicyPathDenyRules(policies);
477
+ const writeTargetGuard = toolPolicyWriteTargetGuard(policies);
455
478
  const childProjections = policies.map((p) => p.projection);
456
479
  const projectedComponents = [];
457
480
  let combinedRemainder = false;
@@ -472,6 +495,8 @@ export function combinePolicies(...policies) {
472
495
  return {
473
496
  ...(combinedProjection !== undefined ? { projection: combinedProjection } : {}),
474
497
  ...(nameSets.length > 0 ? { nameSets } : {}),
498
+ ...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
499
+ ...(writeTargetGuard !== undefined ? { writeTargetGuard } : {}),
475
500
  async check(req, signal) {
476
501
  let asked;
477
502
  let current = req;
@@ -482,6 +507,8 @@ export function combinePolicies(...policies) {
482
507
  let probeMandateSeen = false;
483
508
  let mandateSeen = false;
484
509
  let fallbackSeen;
510
+ let authoredBySeen = false;
511
+ const contributors = [];
485
512
  for (const p of policies) {
486
513
  const d = refuseOutOfContractDecision(await p.check(current, signal));
487
514
  if (d.action === "deny") {
@@ -516,6 +543,10 @@ export function combinePolicies(...policies) {
516
543
  mandateSeen = true;
517
544
  if (d.action === "ask" && d.denialLimitFallback !== undefined && fallbackSeen === undefined)
518
545
  fallbackSeen = d.denialLimitFallback;
546
+ if (d.action === "ask" && d.authoredBy === "deployment")
547
+ authoredBySeen = true;
548
+ if (d.action === "ask")
549
+ contributors.push(d);
519
550
  }
520
551
  if (asked) {
521
552
  const merged = rewrite?.updatedInput;
@@ -523,7 +554,14 @@ export function combinePolicies(...policies) {
523
554
  const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
524
555
  const withMandate = mandateSeen && withMark.mandated !== true ? { ...withMark, mandated: true } : withMark;
525
556
  const withFallback = fallbackSeen !== undefined && withMandate.denialLimitFallback === undefined ? { ...withMandate, denialLimitFallback: fallbackSeen, requiresRealApproval: true } : withMandate;
526
- return merged !== undefined ? { ...withFallback, updatedInput: merged } : withFallback;
557
+ const withAuthor = authoredBySeen && withFallback.authoredBy === undefined ? { ...withFallback, authoredBy: "deployment" } : withFallback;
558
+ const folded = new Set(withAuthor.foldedAskClasses ?? []);
559
+ for (const c of contributors)
560
+ if (c !== asked)
561
+ for (const k of decisionAskClassesOf(c))
562
+ folded.add(k);
563
+ const withClasses = folded.size > 0 ? { ...withAuthor, foldedAskClasses: [...folded] } : withAuthor;
564
+ return merged !== undefined ? { ...withClasses, updatedInput: merged } : withClasses;
527
565
  }
528
566
  const allowed = rewrite ?? ALLOW;
529
567
  if (!anyRecord)
@@ -829,6 +867,7 @@ export function createUnverifiableDeletePolicy(opts) {
829
867
  action: "ask",
830
868
  decisionReason: "safety",
831
869
  requiresRealApproval: true,
870
+ guard: "unverifiable_delete",
832
871
  message: `Unverifiable recursive delete (fail-closed unless cleared): ${finding}. ` +
833
872
  `Re-run the delete with the resolved literal path written into the command itself ` +
834
873
  `(or assign the variable in the same command, e.g. \`DIR=/exact/path; rm -rf "$DIR"\`) so the target can be verified.`,
@@ -905,6 +944,7 @@ export function createTranscriptIntegrityPolicy(opts) {
905
944
  action: "ask",
906
945
  decisionReason: "safety",
907
946
  requiresRealApproval: true,
947
+ guard: "transcript_write",
908
948
  message: `Session-transcript write (fail-closed unless cleared): ${what}. Session transcripts (the .jsonl files ` +
909
949
  `under the agent data dir's sessions/ directory) are harness-written session state, not agent working ` +
910
950
  `files — modifying or deleting them tampers with the run's own audit trail. Reading them (ls/cat/grep ` +
@@ -956,6 +996,7 @@ export function isLiveApproverSeat(onAsk) {
956
996
  return typeof onAsk === "function";
957
997
  }
958
998
  export const DURABLE_MANDATE_SOURCES = ["force_durable_gate", "durable_approval"];
999
+ Object.freeze(DURABLE_MANDATE_SOURCES);
959
1000
  export function durableMandateSourceOf(args) {
960
1001
  if (args.forceDurableGate)
961
1002
  return "force_durable_gate";
@@ -1250,7 +1291,7 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
1250
1291
  if (onAsk === undefined || onAsk === "deny") {
1251
1292
  return {
1252
1293
  action: "deny",
1253
- message: `approval required for "${req.toolName}" but no approver is wired (headless auto-deny): ${req.message}`,
1294
+ message: headlessDenialMessage(req.toolName, req.message),
1254
1295
  decisionReason: "mode",
1255
1296
  settlement: settledByNobody("no_approver"),
1256
1297
  };
@@ -28,6 +28,8 @@ export declare function pathConfinableWriteToolNames(): ReadonlySet<string>;
28
28
  export declare function skillScopeWriteToolNames(): ReadonlySet<string>;
29
29
  /** A-8 — every definition that names a path target at all (read or write): the file-tool glob lane. */
30
30
  export declare function pathTargetingToolNames(): ReadonlySet<string>;
31
+ /** The tools whose declared path target is a READ (a file read, a search root): the file read station's membership. */
32
+ export declare function readTargetToolNames(): ReadonlySet<string>;
31
33
  /** A-10 — the micro-compaction whitelist (`compactable: true`). */
32
34
  export declare function compactableToolNames(): ReadonlySet<string>;
33
35
  /** A-5 — the aggregate-budget exemptions (`budgetExempt: true`). */
@@ -31,6 +31,9 @@ export function skillScopeWriteToolNames() {
31
31
  export function pathTargetingToolNames() {
32
32
  return names((e) => e.face.pathTarget !== undefined);
33
33
  }
34
+ export function readTargetToolNames() {
35
+ return names((e) => e.face.pathTarget?.access === "read");
36
+ }
34
37
  export function compactableToolNames() {
35
38
  return names((e) => e.face.compactable === true);
36
39
  }
@@ -1113,7 +1113,7 @@ export interface ToolExecuteContext {
1113
1113
  /**
1114
1114
  * RB-201 FO-3 (form-one audit, CC 220 `Ipd`/`ein` parity) — the auto-mode classifier decider ARMED
1115
1115
  * for THIS task (auto-mode intent ∧ `RunnerDeps.autoMode` present ∧ `RuntimeCaps.autoMode !== false`
1116
- * — see {@link TaskSpec.autoModeRequested}; the same
1116
+ * — see {@link TaskSpec.permissionMode}; the same
1117
1117
  * instance `runToolGate`'s per-call ask review already consults, carrying its own live breaker
1118
1118
  * state — not a fresh one built from raw config). Runner-filled, trusted, undefined when auto-mode
1119
1119
  * is not armed or the tool runs outside a Runner task.
@@ -1286,13 +1286,12 @@ export interface ToolExecuteContext {
1286
1286
  */
1287
1287
  requestStopAfterTurn?: () => void;
1288
1288
  /**
1289
- * design/108 — enter mid-run READ-ONLY plan mode: the rest of THIS run rejects write-effect tool calls
1290
- * (`effect !== "read"`), while read/research tools and `present_plan` still run. A run-local one-way flag
1291
- * (NOT durable — a later approve-resume re-prepares with it clear, so the approved plan can write). The
1292
- * first-party `enter_plan_mode` tool (CC `EnterPlanMode` parity) is the only caller; the engine enforces the
1293
- * read-only restriction in a dedicated plan-mode tool-call check (separate from the safety gate — plan mode
1294
- * is a FIDELITY mode, not a security boundary; the real safety gates apply independently). Idempotent; a
1295
- * no-op for tools that don't call it. The Runner fills it; not a tool arg. Absent outside a Runner.
1289
+ * Enter plan mode mid-run: a steer of the leg's ONE permission-mode seat into `plan` — the same mode the spec's
1290
+ * `permissionMode` and `TaskStream.setPermissionMode` set. For the rest of THIS run the mode gate asks before every
1291
+ * covered write and every call to a tool the engine does not define that is not read-only (the plan sentence; refused
1292
+ * where nobody can answer), while read tools and `ExitPlanMode` run. The word it left is recorded: an approve-resume
1293
+ * returns to it; a rejected plan steers the resumed leg back into `plan`. The first-party `EnterPlanMode`
1294
+ * tool is the only caller. Idempotent. The Runner fills it; not a tool arg. Absent outside a Runner.
1296
1295
  */
1297
- enterPlanMode?: () => void;
1296
+ enterPlanMode?: () => Promise<void>;
1298
1297
  }
@@ -253,11 +253,12 @@ export type TraceEvent = {
253
253
  taskId: string;
254
254
  /** Which spec field the skipped entry came from. Absent = `additionalDirectories` (the
255
255
  * original face, kept implicit for existing readers); `"additionalReadDirectories"` = the
256
- * read-only whitelist arm (ruled 2026-08-05). */
257
- field?: "additionalReadDirectories";
256
+ * read-only whitelist arm (ruled 2026-08-05); `"persistedReadRule"` = a person's persisted `Read(//dir/**)` allow row. */
257
+ field?: "additionalReadDirectories" | "persistedReadRule";
258
258
  /** The spec entry as written (raw, not canonical — canonicalization is what failed). */
259
259
  entry: string;
260
- /** The canonicalization failure, `code: message` form. */
260
+ /** Why the entry was skipped, `code: message` form: the canonicalization failure, or (a persisted row only)
261
+ * `admits_artifact_store` — the in-flight grant's store rule. */
261
262
  reason: string;
262
263
  ts: number;
263
264
  } | {
@@ -442,9 +443,9 @@ export type TraceEvent = {
442
443
  ts: number;
443
444
  } | {
444
445
  /**
445
- * design/179 — the persisted allow-rule store could not be read, so this call was adjudicated with
446
- * ZERO rules (it asks, rather than silently behaving like an empty store). A loosening face that
447
- * fails must say so.
446
+ * design/179 — the person's persisted rule store could not be read (at prepare, or for one call), so
447
+ * that read's rows took no part: the decision it served was made with ZERO persisted rows. The operator's
448
+ * per-read record; the person hears it once per run (`permission.rule_store_unavailable`).
448
449
  */
449
450
  kind: "permission.rule_store_unreadable";
450
451
  version: 1;
@@ -13,7 +13,8 @@ import type { AgentTool, ThinkingLevel, ToolInputValidationContext, ToolInputVer
13
13
  import type { Model, StreamFn } from "../internal/llm.js";
14
14
  export type { ModelRef, ModelRole, RoleSpec, ModelRoles, Brain } from "./model-seat.js";
15
15
  export { MODEL_ROLES } from "./model-seat.js";
16
- export type { StaleToolResultOffloadOptions, ImageInput, TaskSpec, ShellGateDoctrine, ApproverPosture } from "./task-spec.js";
16
+ export type { StaleToolResultOffloadOptions, ImageInput, TaskSpec, ShellGateDoctrine } from "./task-spec.js";
17
+ export type { PermissionMode } from "./ask-origin.js";
17
18
  export type { ToolEffect, ToolContentOrigin, ReversibilityVerdict, ReadRootGrantCandidate, ToolSpec, ToolReturn, NestedUsage, NestedUsageAccum, ToolExecuteContext } from "./tool-spec.js";
18
19
  export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook, TrackEditRequest, TrackEditResult, TrackFileEditHook, FileEditedNotice, FileEditedHook, HandsBandOptions } from "./hands-band.js";
19
20
  export type { AgentDefinition, SkillManifest, SkillSpec } from "./agent-definition.js";
@@ -64,8 +64,8 @@ export type ParkLaneReason = "no_checkpoint_store" | "no_durable_approval_opt_in
64
64
  * The closed set of auto-mode ARMING outcomes a leg's manifest reports
65
65
  * ({@link WiringManifest.autoMode}). Minted from the arming predicate's own three-arm read, as the
66
66
  * FIRST failing arm in predicate order:
67
- * - `no_intent` — no `TaskSpec.autoModeRequested` seat and no intent inherited on the chain /
68
- * recorded on the resumed checkpoint (a non-auto run; byte-identical arming).
67
+ * - `no_intent` — the declared permission mode is not `auto` (no `permissionMode: "auto"` seat, none
68
+ * inherited on the chain / recorded on the resumed checkpoint; a non-auto run).
69
69
  * - `no_face` — intent, but no `RunnerDeps.autoMode` deployment face (the trust gate).
70
70
  * - `denied` — intent + face, but the organization's deny bit (`RuntimeCaps.autoMode:false`)
71
71
  * as the resolver STATED it.
@@ -306,7 +306,8 @@ export interface WiringManifest {
306
306
  * classifier decider was minted for this leg), and `reason` names the first arm that failed
307
307
  * otherwise — see {@link AUTO_MODE_ARM_REASONS}. A serving layer that used to infer the mode from
308
308
  * spec shape reads it here instead. The classifier's availability is a per-CALL fact (an unavailable
309
- * round is a deny that says so, and the next round is classified afresh), so there is no session-level
309
+ * round goes on to the approval seat or is refused with a sentence that says so, and the next round is
310
+ * classified afresh), so there is no session-level
310
311
  * breaker face here and nothing about availability enters {@link configFingerprint}.
311
312
  */
312
313
  autoMode?: AutoModeArmFact;
@@ -369,6 +370,17 @@ export interface WiringManifest {
369
370
  mounted: boolean;
370
371
  reason?: HandsUnmountedReason;
371
372
  };
373
+ /**
374
+ * EFFECTIVE half only, ALWAYS present on an engine-minted effective manifest: the leg's PERMISSION MODE as the engine
375
+ * resolved it — `requested` (the caller's word; absent = declared nothing), `effective` (the mode the leg runs under:
376
+ * the request clamped under a parent's ceiling, then folded to `default` where the deployment denies it or the
377
+ * classifier face is absent), `foldedBy` (present iff the two differ: `operator_deny` / `face_absent` /
378
+ * `inherited_ceiling`). The same three members answer every steer of the mode (`TaskStream.setPermissionMode`) and
379
+ * ride the fold notice's detail. A shell renders its mode indicator from `effective`, never from what it sent.
380
+ * ABSENT = an older mint, never a state. Leg-local (excluded from {@link configFingerprint}: a child clamped under its
381
+ * parent legitimately differs from it).
382
+ */
383
+ permissionMode?: import("./mode-policy.js").PermissionModeReceipt;
372
384
  /**
373
385
  * EFFECTIVE half only, PRESENT IFF this leg compiled a write-protection judge — a deployment that
374
386
  * replaced the table with `[]` opted the layer out entirely, and reports nothing here rather than a
@@ -407,8 +419,8 @@ export interface WiringManifest {
407
419
  * of one assembly agree.
408
420
  *
409
421
  * @contract read_deny.builtin_activation_disclosed — WHETHER the built-in sensitive-path read deny
410
- * table judges a leg is DISCLOSED, never inferred: the table is a deployment opt-in whose default
411
- * selection is empty, the leg that compiles the judge relays the tiers its own resolution materially
422
+ * table judges a leg is DISCLOSED, never inferred: the tier selection is the deployment's (absent = the default
423
+ * tiers, an explicit `[]` = none), the leg that compiles the judge relays the tiers its own resolution materially
412
424
  * activated, and the manifest carries that section iff at least one built-in row is active (absence
413
425
  * = no built-in row judges; an empty array is never emitted — "off" and "on to nothing" are one
414
426
  * fact). The disclosure changes no verdict.
@@ -524,6 +536,9 @@ export interface WiringFacts {
524
536
  mounted: boolean;
525
537
  reason?: HandsUnmountedReason;
526
538
  };
539
+ /** Effective half only — the leg's permission-mode receipt ({@link WiringManifest.permissionMode}); the engine always passes it.
540
+ * Screened: `effective` a mode word, `requested` a mode word or absent, `foldedBy` a fold reason or absent. */
541
+ permissionMode?: import("./mode-policy.js").PermissionModeReceipt;
527
542
  /** Effective half only — the compiled write-protection judge's OWN target-view word
528
543
  * ({@link WiringManifest.writeProtection}); absent ⇒ this leg compiled no judge (an emptied table). */
529
544
  writeProtectionTargetView?: WriteProtectionTargetView;
@@ -596,6 +611,17 @@ export declare function resolveSubagentTranscriptTier(agentStoreWired: boolean,
596
611
  * else the headless degrade `auto_deny`.
597
612
  */
598
613
  export declare function deriveAskEffective(form: AskSeamForm, parkEffective: boolean | "unresolved"): AskEffective;
614
+ /**
615
+ * Does an ask this leg hands on carrying `requiresRealApproval` reach a PERSON — never an automatic yes? The leg's
616
+ * APPROVAL SEAT, read off the ONE ask derivation above, never re-derived: `human_reachable` (a live approver) and
617
+ * `park_only` (the durable park; where the park cannot take the ask, a blanket seat behind it refuses the bit) are a
618
+ * person; `auto_allow`, `auto_deny` and `unresolved` are nobody (the last the fail-closed reading). The auto-mode
619
+ * classifier stations read it to decide where a classification that could not rule goes: to that person (the ask
620
+ * carrying the bit), or refused.
621
+ */
622
+ export declare function askReachesPerson(ask: {
623
+ readonly effective?: AskEffective;
624
+ }): boolean;
599
625
  export declare function deriveWiringManifest(facts: WiringFacts): WiringManifest;
600
626
  /** The one ask-seat resolution (`spec` wins over `deps`), shared with the effective half so the
601
627
  * form/provenance read cannot fork. Junk values (a JS caller passing something outside the OnAsk
@@ -1,3 +1,5 @@
1
+ import { isPermissionMode } from "./ask-origin.js";
2
+ import { PERMISSION_MODE_FOLD_REASONS } from "./mode-policy.js";
1
3
  import { createHash } from "node:crypto";
2
4
  import { canonicalize } from "./canonical-json.js";
3
5
  import { resolveCheckpointStore } from "./checkpoint-store.js";
@@ -57,6 +59,23 @@ export function deriveAskEffective(form, parkEffective) {
57
59
  }
58
60
  }
59
61
  }
62
+ export function askReachesPerson(ask) {
63
+ const effective = ask.effective ?? "unresolved";
64
+ switch (effective) {
65
+ case "human_reachable":
66
+ case "park_only":
67
+ return true;
68
+ case "auto_allow":
69
+ case "auto_deny":
70
+ case "unresolved":
71
+ return false;
72
+ default: {
73
+ const _exhaustive = effective;
74
+ void _exhaustive;
75
+ throw new Error(`unreachable ask effective ${String(effective)}`);
76
+ }
77
+ }
78
+ }
60
79
  function deriveParkLane(facts) {
61
80
  if (!facts.parkCapable) {
62
81
  return { capable: false, effective: false, reasons: ["no_checkpoint_store"] };
@@ -87,6 +106,13 @@ function readAutoModeFact(fact) {
87
106
  }
88
107
  return { armed, reason, ...(deniedSource !== undefined ? { deniedSource } : {}) };
89
108
  }
109
+ function readPermissionModeFact(fact) {
110
+ const { requested, effective, foldedBy } = typeof fact === "object" && fact !== null ? fact : { requested: undefined, effective: undefined, foldedBy: undefined };
111
+ if (!isPermissionMode(effective) || (requested !== undefined && !isPermissionMode(requested)) || (foldedBy !== undefined && !PERMISSION_MODE_FOLD_REASONS.includes(foldedBy))) {
112
+ throw new Error(`the permission-mode wiring fact is inconsistent (${JSON.stringify(fact)}) — effective must be a permission mode, requested one or absent, foldedBy one of ${PERMISSION_MODE_FOLD_REASONS.join("|")} or absent`);
113
+ }
114
+ return { ...(requested !== undefined ? { requested } : {}), effective, ...(foldedBy !== undefined ? { foldedBy } : {}) };
115
+ }
90
116
  function readHandsFact(fact) {
91
117
  const { mounted, reason } = typeof fact === "object" && fact !== null ? fact : { mounted: undefined, reason: undefined };
92
118
  if (typeof mounted !== "boolean" || (mounted ? reason !== undefined : reason !== "no_execution_env")) {
@@ -157,6 +183,7 @@ export function deriveWiringManifest(facts) {
157
183
  ...(facts.half === "effective" && facts.hooks !== undefined && facts.hooks.length > 0 ? { hooks: facts.hooks.map((h) => ({ ...h })) } : {}),
158
184
  ...(facts.half === "effective" && facts.lsp !== undefined ? { lsp: { ...facts.lsp } } : {}),
159
185
  ...(facts.half === "effective" && facts.hands !== undefined ? { hands: readHandsFact(facts.hands) } : {}),
186
+ ...(facts.half === "effective" && facts.permissionMode !== undefined ? { permissionMode: readPermissionModeFact(facts.permissionMode) } : {}),
160
187
  ...(facts.half === "effective" && facts.writeProtectionTargetView !== undefined ? { writeProtection: { targetView: facts.writeProtectionTargetView } } : {}),
161
188
  ...(facts.half === "effective" && facts.readDenyBuiltinTiersActive !== undefined && facts.readDenyBuiltinTiersActive.length > 0
162
189
  ? { readDeny: { builtinTiers: [...facts.readDenyBuiltinTiersActive] } }
@@ -169,7 +196,7 @@ export function deriveWiringManifest(facts) {
169
196
  : {}),
170
197
  };
171
198
  if (facts.half === "effective") {
172
- const { leg: _leg, mcp: _mcp, tools: _tools, lsp: _lsp, hooks: _hooks, teamRole: _teamRole, peerPresence: _peerPresence, fileHistory: _fileHistory, ...assembly } = manifest;
199
+ const { leg: _leg, mcp: _mcp, tools: _tools, lsp: _lsp, hooks: _hooks, teamRole: _teamRole, peerPresence: _peerPresence, fileHistory: _fileHistory, permissionMode: _permissionMode, ...assembly } = manifest;
173
200
  const { provenance: _askSeat, ...askForHash } = assembly.ask;
174
201
  const { provenance: _questionSeat, ...questionForHash } = assembly.question;
175
202
  manifest.configFingerprint = createHash("sha256")
@@ -20,7 +20,7 @@ export interface WriteProtectedRow {
20
20
  * String shorthand: a bare name ≡ `{ name, kind: "segment" }` (the WIDER single-segment kind —
21
21
  * over-matching is the fail-safe direction for a tighten); a name containing `/` ≡
22
22
  * `{ name, kind: "segment-run" }`. Spell `kind: "basename"` explicitly when last-segment-only
23
- * matching is the intent.
23
+ * matching is the intent. A name opening with the home word (`~/…`) refuses: the table places no row at a home.
24
24
  */
25
25
  export type WriteProtectedEntry = string | WriteProtectedRow;
26
26
  /** A table hit: which row matched (the row's canonical name + kind). */
@@ -108,6 +108,13 @@ export interface WriteProtectionJudgeContext {
108
108
  readonly base?: string;
109
109
  /** Bounds the filesystem probes of the target view. */
110
110
  readonly signal?: AbortSignal;
111
+ /** The path is a CANONICAL key the caller already resolved at the seat its program writes it (the shell read station's
112
+ * established write target: where the path lands, or — for a removal or a rename — the entry itself, its canonical
113
+ * parent joined with the last name, a link there not followed). It is judged where a key is judged — the readings and
114
+ * the canonical data-root exemption — never as a spelling (the lexical passthrough reads a spelling, and a key reached
115
+ * through a linked data root does not spell itself inside it) and never followed: no link chain is walked from it, as
116
+ * the program walks none from where it writes. Without an environment the key is judged as a spelling (the absence law). */
117
+ readonly canonical?: true;
111
118
  }
112
119
  /**
113
120
  * WHICH reading of a write's TARGET a compiled judge has — its own word, and the vocabulary the wiring
@@ -258,3 +265,6 @@ export interface WriteProtectionMount {
258
265
  * `deny > ask > allow` fold, which is what makes the missing seat cost nothing.
259
266
  */
260
267
  export declare function createWriteProtectionCheck(entries?: readonly WriteProtectedEntry[], opts?: WriteProtectionCompileOptions): WriteProtectionMount | undefined;
268
+ /** {@link createWriteProtectionCheck} over a judge already compiled — the engine's leg compiles once and hands the same
269
+ * judge to the call check and to the shell station (engine-internal; not re-exported from the package root). */
270
+ export declare function writeProtectionMountOver(matcher: WriteProtectionMatcher, rootPath: string | undefined): WriteProtectionMount;
@@ -3,6 +3,7 @@ import { protectivePathTargetOf } from "./tool-registry.js";
3
3
  import { pathTargetBaseOf } from "./effective-path-target.js";
4
4
  import { canonicalDataRootKeys, dataRootSpellings, relativeUnderRoot } from "./sensitive-path-policy.js";
5
5
  import { symlinkChainHops } from "./path-identity.js";
6
+ import { readDenyPatternRun } from "../tools/fs/read-deny.js";
6
7
  import { thrownText } from "./thrown-value.js";
7
8
  const freezeTable = (rows) => Object.freeze(rows.map((r) => Object.freeze(r)));
8
9
  export const WRITE_PROTECTED_DEFAULT_TABLE = freezeTable([
@@ -57,6 +58,7 @@ export const WRITE_PROTECTED_DEFAULT_TABLE = freezeTable([
57
58
  { name: ".gnupg", kind: "segment" },
58
59
  { name: ".sema.json", kind: "basename" },
59
60
  { name: ".sema", kind: "segment" },
61
+ { name: "credentials.env", kind: "basename" },
60
62
  ]);
61
63
  export function foldWriteProtectSegment(s) {
62
64
  const cased = s.toLowerCase().replace(/ı/g, "i").replace(/ſ/g, "s");
@@ -91,6 +93,11 @@ function normalizeWriteProtectedEntry(entry) {
91
93
  throw new Error(`writeProtectedPaths: entry ${describeEntryValue(entry)} has kind ${describeEntryValue(shape.kind)} — known kinds: ${KINDS.join(", ")}.`);
92
94
  }
93
95
  const kind = shape.kind;
96
+ const { homeAnchored, run } = readDenyPatternRun(name);
97
+ if (homeAnchored) {
98
+ throw new Error(`writeProtectedPaths: entry ${describeEntryValue(name)} opens with the home word "~", which this table does not place — its names are matched wherever their segments appear. ` +
99
+ (run.length > 0 ? `Spell the name without it (${describeEntryValue(run)}), which covers the home's file and every same-named one.` : "The home directory itself is not a row; spell the name of what it holds."));
100
+ }
94
101
  if (name.includes("*") || name.includes("?")) {
95
102
  throw new Error(`writeProtectedPaths: entry ${describeEntryValue(name)} contains a wildcard metacharacter — this table speaks LITERAL names only (a silently-literal "*" would guard less than it reads); pattern semantics live in createSensitivePathPolicy.`);
96
103
  }
@@ -254,7 +261,8 @@ export function compileWriteProtection(entries, opts) {
254
261
  return judgeSpelling(path, cwd);
255
262
  },
256
263
  async judge(path, ctx = {}) {
257
- const bySpelling = judgeSpelling(path, ctx.cwd);
264
+ const givenKey = ctx.canonical === true && env !== undefined;
265
+ const bySpelling = givenKey ? null : judgeSpelling(path, ctx.cwd);
258
266
  if (bySpelling !== null)
259
267
  return { kind: "hit", hit: bySpelling, via: "spelling" };
260
268
  if (env === undefined)
@@ -268,16 +276,18 @@ export function compileWriteProtection(entries, opts) {
268
276
  noted = true;
269
277
  opts?.onViewAbsent?.({ cause, unresolvedSymlink });
270
278
  };
271
- let canonKey;
272
- try {
273
- const canon = await canonicalizeTarget(env, path, ctx.signal, family, base);
274
- if (canon.ok)
275
- canonKey = canon.key;
276
- else
277
- noteAbsence(canon.message, canon.unresolvedSymlink === true);
278
- }
279
- catch (err) {
280
- noteAbsence(thrownText(err), false);
279
+ let canonKey = givenKey ? path : undefined;
280
+ if (!givenKey) {
281
+ try {
282
+ const canon = await canonicalizeTarget(env, path, ctx.signal, family, base);
283
+ if (canon.ok)
284
+ canonKey = canon.key;
285
+ else
286
+ noteAbsence(canon.message, canon.unresolvedSymlink === true);
287
+ }
288
+ catch (err) {
289
+ noteAbsence(thrownText(err), false);
290
+ }
281
291
  }
282
292
  try {
283
293
  if (canonKey !== undefined) {
@@ -295,7 +305,7 @@ export function compileWriteProtection(entries, opts) {
295
305
  return { kind: "hit", hit: byTarget, via: "target" };
296
306
  }
297
307
  }
298
- for (const hop of await symlinkChainHops(env, path, ctx.signal, family, base, (cause) => noteAbsence(cause, false))) {
308
+ for (const hop of givenKey ? [] : await symlinkChainHops(env, path, ctx.signal, family, base, (cause) => noteAbsence(cause, false))) {
299
309
  const byHop = judgeSpelled(hop);
300
310
  if (byHop === null)
301
311
  continue;
@@ -346,8 +356,9 @@ export function writeProtectionUnresolved(verdict) {
346
356
  }
347
357
  export function createWriteProtectionCheck(entries, opts) {
348
358
  const matcher = compileWriteProtection(entries, opts);
349
- if (matcher === undefined)
350
- return undefined;
359
+ return matcher === undefined ? undefined : writeProtectionMountOver(matcher, opts?.rootPath);
360
+ }
361
+ export function writeProtectionMountOver(matcher, rootPath) {
351
362
  const check = (toolName, args, face, cwd, signal) => {
352
363
  const call = { toolName, args, ...(face !== undefined ? { face } : {}) };
353
364
  const protective = protectivePathTargetOf(call);
@@ -356,7 +367,7 @@ export function createWriteProtectionCheck(entries, opts) {
356
367
  const path = writeTargetPath(call, protective);
357
368
  if (typeof path !== "string" || path.length === 0)
358
369
  return null;
359
- return matcher.judge(path, { ...(cwd !== undefined ? { cwd } : {}), ...(signal !== undefined ? { signal } : {}), ...(() => { const b = pathTargetBaseOf(protective, { root: opts?.rootPath, cwd }); return b !== undefined ? { base: b } : {}; })() });
370
+ return matcher.judge(path, { ...(cwd !== undefined ? { cwd } : {}), ...(signal !== undefined ? { signal } : {}), ...(() => { const b = pathTargetBaseOf(protective, { root: rootPath, cwd }); return b !== undefined ? { base: b } : {}; })() });
360
371
  };
361
372
  return { check, targetView: matcher.targetView };
362
373
  }
@@ -1,4 +1,5 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { scrubSecretEnv } from "../../core/secret-env.js";
2
3
  import { existsSync } from "node:fs";
3
4
  import { join } from "node:path";
4
5
  const DEFAULT_GRACE_MS = 3000;
@@ -144,6 +145,7 @@ function enumerateDescendantsSync(rootPid) {
144
145
  let out;
145
146
  try {
146
147
  const result = spawnSync("ps", ["-A", "-o", "pid=", "-o", "ppid=", "-o", "etime="], {
148
+ env: scrubSecretEnv(process.env),
147
149
  timeout: PS_ENUMERATION_TIMEOUT_MS,
148
150
  encoding: "utf8",
149
151
  windowsHide: true,
@@ -209,6 +211,7 @@ function parseEtimeSeconds(etime) {
209
211
  function runTaskkill(args) {
210
212
  try {
211
213
  const child = spawn("taskkill", args, {
214
+ env: scrubSecretEnv(process.env),
212
215
  stdio: "ignore",
213
216
  detached: true,
214
217
  windowsHide: true,
@@ -268,7 +271,7 @@ function tryMsysGroupKill(bashPath, pid) {
268
271
  `[ -n "$g" ] && [ "$g" != "0" ] && kill -9 -"$g" 2>/dev/null; ` +
269
272
  `done; exit 0`;
270
273
  try {
271
- const child = spawn(bashPath, ["-c", script], { stdio: "ignore", detached: true, windowsHide: true });
274
+ const child = spawn(bashPath, ["-c", script], { env: scrubSecretEnv(process.env), stdio: "ignore", detached: true, windowsHide: true });
272
275
  child.on("error", () => { });
273
276
  return true;
274
277
  }
@@ -187,6 +187,9 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
187
187
  setFileMode(path: string, mode: number, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
188
188
  listDir(path: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
189
189
  canonicalPath(path: string): Promise<Result<string, FileError>>;
190
+ /** {@link FileSystem.fileIdentity}: `stat` (links followed) read as BIGINT — an inode past 2^53 (an APFS volume root)
191
+ * is not exact as a number, and two inodes that collapse to one number would name two directories as one. */
192
+ fileIdentity(path: string): Promise<Result<string, FileError>>;
190
193
  readLink(path: string): Promise<Result<string, FileError>>;
191
194
  /**
192
195
  * The LEAF symlink chain ({@link FileSystem.canonicalChain}): `readlink` on the whole spelling, then on each
@@ -1,13 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { closeSync, constants, createReadStream, mkdtempSync, openSync, readSync, statSync, truncateSync, unlinkSync } from "node:fs";
4
- import { access, appendFile, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, rename, rm, unlink, writeFile, } from "node:fs/promises";
4
+ import { access, appendFile, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, rename, rm, stat, unlink, writeFile, } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { dirname, isAbsolute, join, resolve, sep } from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
9
9
  import { killProcessTree, shutdownDebug } from "./kill-tree.js";
10
- import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, scrubSecretEnv } from "../../core/secret-env.js";
10
+ import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, inheritedHostEnv, scrubSecretEnv } from "../../core/secret-env.js";
11
11
  import { physicalPathOfAsync } from "../../core/physical-path.js";
12
12
  import { isUncSpelling } from "../../tools/fs/safety.js";
13
13
  import { RollingTailBuffer, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
@@ -119,6 +119,7 @@ async function runCommand(command, args, timeoutMs) {
119
119
  child = spawn(command, args, {
120
120
  stdio: ["ignore", "pipe", "ignore"],
121
121
  windowsHide: true,
122
+ env: scrubSecretEnv(process.env),
122
123
  });
123
124
  }
124
125
  catch {
@@ -203,44 +204,32 @@ export function __resetSecretScrubAnnouncement() {
203
204
  __resetSecretEnvScrubNoticeAnnouncement();
204
205
  }
205
206
  function getShellEnv(inheritEnv, baseEnv, extraEnv, onScrub, onNotice) {
206
- let inherited;
207
- if (inheritEnv === "all") {
208
- inherited = process.env;
209
- }
210
- else if (Array.isArray(inheritEnv)) {
211
- inherited = {};
212
- for (const k of inheritEnv)
213
- if (process.env[k] !== undefined)
214
- inherited[k] = process.env[k];
215
- }
216
- else {
217
- const scrubbed = [];
218
- inherited = scrubSecretEnv(process.env, scrubbed, process.platform);
219
- const findings = scrubbed.filter((f) => baseEnv?.[f.key] === undefined && extraEnv?.[f.key] === undefined);
220
- if (findings.length > 0) {
221
- if (typeof onScrub === "function") {
222
- try {
223
- const r = onScrub(findings);
224
- if (typeof r?.then === "function") {
225
- r.then(undefined, () => {
226
- });
227
- }
228
- }
229
- catch {
207
+ const scrubbed = [];
208
+ const inherited = inheritedHostEnv(inheritEnv, scrubbed);
209
+ const findings = scrubbed.filter((f) => baseEnv?.[f.key] === undefined && extraEnv?.[f.key] === undefined);
210
+ if (findings.length > 0) {
211
+ if (typeof onScrub === "function") {
212
+ try {
213
+ const r = onScrub(findings);
214
+ if (typeof r?.then === "function") {
215
+ r.then(undefined, () => {
216
+ });
230
217
  }
231
218
  }
232
- else if (onScrub !== undefined && !secretScrubSeatDefectAnnounced) {
233
- secretScrubSeatDefectAnnounced = true;
234
- try {
235
- console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
236
- }
237
- catch {
238
- }
219
+ catch {
239
220
  }
240
- if (!(typeof onScrub === "function" && onNotice === undefined)) {
241
- announceSecretEnvScrub(onNotice, findings);
221
+ }
222
+ else if (onScrub !== undefined && !secretScrubSeatDefectAnnounced) {
223
+ secretScrubSeatDefectAnnounced = true;
224
+ try {
225
+ console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
226
+ }
227
+ catch {
242
228
  }
243
229
  }
230
+ if (!(typeof onScrub === "function" && onNotice === undefined)) {
231
+ announceSecretEnvScrub(onNotice, findings);
232
+ }
244
233
  }
245
234
  return {
246
235
  ...inherited,
@@ -1021,6 +1010,16 @@ export class NodeExecutionEnv {
1021
1010
  return err(toFileError(error, resolved));
1022
1011
  }
1023
1012
  }
1013
+ async fileIdentity(path) {
1014
+ const resolved = resolvePath(this.cwd, path);
1015
+ try {
1016
+ const s = await stat(resolved, { bigint: true });
1017
+ return ok(`${s.dev}:${s.ino}`);
1018
+ }
1019
+ catch (error) {
1020
+ return err(toFileError(error, resolved));
1021
+ }
1022
+ }
1024
1023
  async readLink(path) {
1025
1024
  const resolved = resolvePath(this.cwd, path);
1026
1025
  try {