@sema-agent/core 7.16.0 → 7.17.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 (193) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/dist/agents/builtin-agents.d.ts +3 -3
  3. package/dist/agents/cross-session-envelope.d.ts +0 -1
  4. package/dist/agents/cross-session-envelope.js +0 -1
  5. package/dist/agents/peer-notices.d.ts +0 -9
  6. package/dist/agents/peer-notices.js +0 -63
  7. package/dist/agents/subagent.js +7 -15
  8. package/dist/brain/request-params.d.ts +2 -2
  9. package/dist/config/defaults.d.ts +0 -7
  10. package/dist/config/defaults.js +0 -2
  11. package/dist/core/artifact-host.d.ts +218 -0
  12. package/dist/core/artifact-host.js +92 -0
  13. package/dist/core/ask-origin.d.ts +29 -3
  14. package/dist/core/ask-origin.js +17 -9
  15. package/dist/core/ask-question.d.ts +25 -0
  16. package/dist/core/ask-question.js +4 -0
  17. package/dist/core/ask-unresolvable-notice.d.ts +30 -21
  18. package/dist/core/ask-unresolvable-notice.js +16 -8
  19. package/dist/core/auto-compaction.d.ts +10 -10
  20. package/dist/core/auto-mode-arming.d.ts +2 -2
  21. package/dist/core/background-agent-store.d.ts +5 -5
  22. package/dist/core/background-shell.d.ts +1 -1
  23. package/dist/core/checkpoint-execution-record.d.ts +0 -3
  24. package/dist/core/checkpoint-execution-record.js +0 -4
  25. package/dist/core/checkpoint-store.d.ts +71 -99
  26. package/dist/core/checkpoint-store.js +9 -16
  27. package/dist/core/context-edit.d.ts +36 -104
  28. package/dist/core/context-edit.js +37 -60
  29. package/dist/core/effective-path-target.d.ts +68 -8
  30. package/dist/core/effective-path-target.js +42 -30
  31. package/dist/core/engine-notice.d.ts +46 -32
  32. package/dist/core/env-budget.d.ts +31 -0
  33. package/dist/core/env-budget.js +39 -0
  34. package/dist/core/exec-output-tail.d.ts +5 -5
  35. package/dist/core/file-history-store.js +3 -6
  36. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  37. package/dist/core/fs-write-gate-policy.js +3 -3
  38. package/dist/core/gate-lanes.js +9 -15
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +2 -0
  41. package/dist/core/hands-band.d.ts +1 -1
  42. package/dist/core/hooks.d.ts +18 -17
  43. package/dist/core/lsp-session.d.ts +1 -1
  44. package/dist/core/lsp.d.ts +1 -1
  45. package/dist/core/lsp.js +5 -4
  46. package/dist/core/mcp.d.ts +5 -5
  47. package/dist/core/mcp.js +4 -48
  48. package/dist/core/memory-recall.d.ts +0 -8
  49. package/dist/core/memory.d.ts +1 -1
  50. package/dist/core/oracle-isolation.js +2 -2
  51. package/dist/core/parent-spec-seat.d.ts +66 -0
  52. package/dist/core/parent-spec-seat.js +47 -0
  53. package/dist/core/permission-rule-model.d.ts +11 -6
  54. package/dist/core/permission-rule-model.js +53 -37
  55. package/dist/core/permission-rules.js +8 -3
  56. package/dist/core/protocol-naming.d.ts +2 -2
  57. package/dist/core/remote-env.d.ts +1 -1
  58. package/dist/core/retired-keys.d.ts +19 -0
  59. package/dist/core/retired-keys.js +15 -0
  60. package/dist/core/runner/active-skill-scope.js +3 -3
  61. package/dist/core/runner/assemble-result.d.ts +15 -13
  62. package/dist/core/runner/assemble-result.js +5 -4
  63. package/dist/core/runner/compaction-call-options.d.ts +25 -89
  64. package/dist/core/runner/contracts.d.ts +10 -9
  65. package/dist/core/runner/gate-exit.js +15 -1
  66. package/dist/core/runner/halt-attribution.d.ts +35 -0
  67. package/dist/core/runner/halt-attribution.js +16 -0
  68. package/dist/core/runner/memory-consolidation.d.ts +0 -2
  69. package/dist/core/runner/memory-consolidation.js +0 -1
  70. package/dist/core/runner/prepare-artifact.d.ts +53 -0
  71. package/dist/core/runner/prepare-artifact.js +77 -0
  72. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  73. package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
  74. package/dist/core/runner/prepare-config-doors.d.ts +3 -3
  75. package/dist/core/runner/prepare-config-doors.js +5 -2
  76. package/dist/core/runner/prepare-context-lane.d.ts +2 -2
  77. package/dist/core/runner/prepare-context-lane.js +2 -13
  78. package/dist/core/runner/prepare-file-history.d.ts +1 -1
  79. package/dist/core/runner/prepare-file-history.js +3 -52
  80. package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
  81. package/dist/core/runner/prepare-hands-readface.js +3 -2
  82. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
  83. package/dist/core/runner/prepare-inherited-gate.js +7 -8
  84. package/dist/core/runner/prepare-memory.d.ts +4 -6
  85. package/dist/core/runner/prepare-memory.js +4 -2
  86. package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
  87. package/dist/core/runner/prepare-policy-chain.js +15 -30
  88. package/dist/core/runner/prepare-prompt-assembly.js +1 -1
  89. package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
  90. package/dist/core/runner/prepare-suspend-saga.js +0 -2
  91. package/dist/core/runner/prepare-task.js +10 -9
  92. package/dist/core/runner/prepare-wiring-manifest.js +9 -7
  93. package/dist/core/runner/resume-admission.js +2 -1
  94. package/dist/core/runner/resume-internals-and-config.js +4 -0
  95. package/dist/core/runner/retired-runner-deps.d.ts +3 -0
  96. package/dist/core/runner/retired-runner-deps.js +7 -0
  97. package/dist/core/runner/run-harness-handlers.js +2 -3
  98. package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
  99. package/dist/core/runner/run-settle-and-teardown.js +3 -1
  100. package/dist/core/runner/session-file-state-replay.js +3 -3
  101. package/dist/core/runner/session-rule-policy.js +3 -4
  102. package/dist/core/runner/stream-halt-verbs.js +1 -1
  103. package/dist/core/runner/stream-steer-verb.js +1 -4
  104. package/dist/core/runner/workspace-path.js +3 -0
  105. package/dist/core/runner-deps.d.ts +30 -49
  106. package/dist/core/safe-notify.d.ts +1 -1
  107. package/dist/core/sensitive-path-policy.d.ts +1 -1
  108. package/dist/core/sensitive-path-policy.js +4 -4
  109. package/dist/core/session.d.ts +2 -2
  110. package/dist/core/skill-tool-specifier.d.ts +1 -1
  111. package/dist/core/skills-directory.d.ts +1 -1
  112. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  113. package/dist/core/strategy-store.d.ts +8 -11
  114. package/dist/core/task-event.d.ts +20 -17
  115. package/dist/core/task-limits.d.ts +1 -1
  116. package/dist/core/task-notification.d.ts +5 -5
  117. package/dist/core/task-registry-agent.d.ts +5 -5
  118. package/dist/core/task-registry-monitor.d.ts +2 -2
  119. package/dist/core/task-registry-shared.d.ts +27 -27
  120. package/dist/core/task-registry-shared.js +3 -13
  121. package/dist/core/task-registry.d.ts +8 -8
  122. package/dist/core/task-result.d.ts +37 -37
  123. package/dist/core/task-spec.d.ts +5 -5
  124. package/dist/core/task-stream.d.ts +16 -11
  125. package/dist/core/tool-catalog-entries.d.ts +1 -0
  126. package/dist/core/tool-catalog-entries.js +5 -0
  127. package/dist/core/tool-conformance.d.ts +0 -2
  128. package/dist/core/tool-conformance.js +1 -3
  129. package/dist/core/tool-errors.d.ts +5 -0
  130. package/dist/core/tool-errors.js +1 -0
  131. package/dist/core/tool-policy.d.ts +51 -22
  132. package/dist/core/tool-policy.js +28 -5
  133. package/dist/core/tool-result-budget.d.ts +1 -1
  134. package/dist/core/tool-result-store.d.ts +11 -11
  135. package/dist/core/tool-spec.d.ts +34 -38
  136. package/dist/core/tools.d.ts +2 -2
  137. package/dist/core/trace.d.ts +12 -15
  138. package/dist/core/untrusted-text.d.ts +1 -1
  139. package/dist/core/wiring-manifest.d.ts +4 -3
  140. package/dist/core/workflow-journal-store.d.ts +11 -11
  141. package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
  142. package/dist/engine/execution-env/node-execution-env.js +4 -2
  143. package/dist/engine/harness/agent-harness.d.ts +47 -4
  144. package/dist/engine/harness/agent-harness.js +52 -9
  145. package/dist/engine/loop/agent-loop.js +15 -8
  146. package/dist/engine/loop/types.d.ts +5 -0
  147. package/dist/index.d.ts +19 -14
  148. package/dist/index.js +15 -10
  149. package/dist/internal/harness.d.ts +1 -0
  150. package/dist/orchestration/run-spec.d.ts +3 -0
  151. package/dist/orchestration/run-spec.js +3 -3
  152. package/dist/orchestration/run-workflow-tool.d.ts +12 -6
  153. package/dist/orchestration/run-workflow-tool.js +23 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +4 -2
  155. package/dist/orchestration/workflow-primitives.js +1 -4
  156. package/dist/orchestration/workflow-types.d.ts +34 -4
  157. package/dist/orchestration/workflow-types.js +2 -0
  158. package/dist/orchestration/workflow.d.ts +39 -7
  159. package/dist/orchestration/workflow.js +234 -113
  160. package/dist/prompt-assembly/assemble.d.ts +2 -2
  161. package/dist/prompt-assembly/assemble.js +9 -9
  162. package/dist/prompt-assembly/composer.d.ts +6 -7
  163. package/dist/prompt-assembly/composer.js +10 -10
  164. package/dist/prompt-assembly/epoch.js +1 -1
  165. package/dist/prompt-assembly/event-registry.d.ts +0 -21
  166. package/dist/prompt-assembly/packs/sema-default.js +30 -30
  167. package/dist/prompt-assembly/types.d.ts +14 -14
  168. package/dist/prompts/coordinator.d.ts +0 -13
  169. package/dist/prompts/coordinator.js +0 -3
  170. package/dist/stores/file/checkpoint-store.js +1 -5
  171. package/dist/stores/file/file-history-store.js +2 -2
  172. package/dist/stores/file/permission-rule-store.d.ts +4 -35
  173. package/dist/stores/file/permission-rule-store.js +4 -148
  174. package/dist/tools/artifact/artifact-text.d.ts +62 -0
  175. package/dist/tools/artifact/artifact-text.js +68 -0
  176. package/dist/tools/artifact/artifact-tool.d.ts +52 -0
  177. package/dist/tools/artifact/artifact-tool.js +351 -0
  178. package/dist/tools/artifact/local-stub.d.ts +26 -0
  179. package/dist/tools/artifact/local-stub.js +296 -0
  180. package/dist/tools/fs/bash-readonly-classifier.d.ts +74 -8
  181. package/dist/tools/fs/bash-readonly-classifier.js +171 -60
  182. package/dist/tools/fs/fs-bash.d.ts +14 -10
  183. package/dist/tools/fs/fs-bash.js +19 -27
  184. package/dist/tools/fs/fs-search-tools.js +5 -2
  185. package/dist/tools/fs/fs-shared.d.ts +2 -0
  186. package/dist/tools/fs/fs-shared.js +3 -11
  187. package/dist/tools/fs/index.d.ts +4 -0
  188. package/dist/tools/fs/index.js +4 -1
  189. package/dist/tools/fs/safety.d.ts +117 -1
  190. package/dist/tools/fs/safety.js +131 -31
  191. package/dist/tools/fs/search.js +8 -6
  192. package/package.json +2 -1
  193. package/test/export-surface.snapshot.json +115 -39
@@ -508,7 +508,7 @@ export declare function missingRestoreSurface(env: ExecutionEnv): readonly ("res
508
508
  * A composite type guard (structural AND capability) so a `suspendVM` call site narrowed by THIS predicate
509
509
  * gets the {@link RemoteExecutionEnv} type without a cast — `true` always implies the structural check too.
510
510
  *
511
- * RB-439-b: it also requires the RESTORE surface ({@link missingRestoreSurface}). A suspend is only durable
511
+ * It also requires the RESTORE surface ({@link missingRestoreSurface}). A suspend is only durable
512
512
  * if something can undo it; an adapter that offers `suspendVM` but no `resumeVM` used to pass this guard,
513
513
  * take a real snapshot, commit a real checkpoint — and then blow up on the resume leg with an untyped
514
514
  * TypeError. "Suspendable" now means the whole round trip, so a half-adapter is rejected at the suspend site
@@ -0,0 +1,19 @@
1
+ /**
2
+ * ONE presence rule for a RETIRED key on a caller-supplied bag (a deps object, an options object): the key is
3
+ * PRESENT iff ordinary property lookup answers a defined value — prototype and non-enumerable members included,
4
+ * exactly what the former direct read of the seat honored; an explicit `undefined` reads as absence, as every
5
+ * optional seat reads. Every retired-key door reads through here so no door grows its own idea of "present";
6
+ * each composes its own refusal from the answer. A table row is `{ key: replacement }` — the sentence that says
7
+ * what replaced the seat, quoted in the refusal.
8
+ */
9
+ /** The first retired key present on `bag`, in table order, or undefined when none is. */
10
+ export declare function retiredKeyOf(bag: object, table: Readonly<Record<string, string>>): {
11
+ key: string;
12
+ replacement: string;
13
+ } | undefined;
14
+ /**
15
+ * The deps-bag door: a retired seat still wired by a deployment is refused BY NAME (`config.deps_retired_key`)
16
+ * instead of vanishing silently — a seat that is silently dropped reads to the deployment as wired. `owner` is
17
+ * the bag's type name for the sentence (`RunnerDeps`, `RunWorkflowToolDeps`).
18
+ */
19
+ export declare function assertNoRetiredKeys(bag: object, table: Readonly<Record<string, string>>, owner: string): void;
@@ -0,0 +1,15 @@
1
+ export function retiredKeyOf(bag, table) {
2
+ for (const [key, replacement] of Object.entries(table)) {
3
+ if (Reflect.get(bag, key) !== undefined)
4
+ return { key, replacement };
5
+ }
6
+ return undefined;
7
+ }
8
+ export function assertNoRetiredKeys(bag, table, owner) {
9
+ const hit = retiredKeyOf(bag, table);
10
+ if (hit === undefined)
11
+ return;
12
+ const e = new Error(`${owner}.${hit.key} is retired and is not read by this engine — ${hit.replacement}. Refused rather than ignored: a seat that is silently dropped reads to the deployment as wired.`);
13
+ e.code = "config.deps_retired_key";
14
+ throw e;
15
+ }
@@ -1,4 +1,4 @@
1
- import { canonicalizeTarget } from "../../tools/fs/safety.js";
1
+ import { canonicalizeTarget, pathFamilyOf } from "../../tools/fs/safety.js";
2
2
  import { pathTargetValue } from "../tool-registry.js";
3
3
  import { pathTargetBaseOf } from "../effective-path-target.js";
4
4
  import { PATH_WRITE_TOOLS, isWithin } from "./session-rule-policy.js";
@@ -100,7 +100,7 @@ export function createActiveSkillScopePolicy(opts) {
100
100
  decisionReason: "safety",
101
101
  };
102
102
  }
103
- const canon = await canonicalizeTarget(env, path, signal, pathTargetBaseOf(declaredTarget, { root: rootPath, cwd: req.cwd }));
103
+ const canon = await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootPath, cwd: req.cwd ?? env.cwd }), pathTargetBaseOf(declaredTarget, { root: rootPath, cwd: req.cwd }));
104
104
  if (!canon.ok) {
105
105
  return {
106
106
  action: "deny",
@@ -111,7 +111,7 @@ export function createActiveSkillScopePolicy(opts) {
111
111
  for (const m of constraining) {
112
112
  const roots = [];
113
113
  for (const raw of m.allowPaths ?? []) {
114
- const r = await canonicalizeTarget(env, raw, signal, rootPath);
114
+ const r = await canonicalizeTarget(env, raw, signal, pathFamilyOf({ root: rootPath ?? env.cwd }), rootPath);
115
115
  if (r.ok)
116
116
  roots.push(r.key);
117
117
  }
@@ -1,5 +1,6 @@
1
1
  import type { AssistantMessage } from "../../internal/llm.js";
2
2
  import type { NestedUsage, PausedCause, TaskResult, TaskSpec, TerminalCause } from "../types.js";
3
+ import type { HaltConsumption } from "../../internal/harness.js";
3
4
  /** Per-task usage/cost accumulator (filled across the run loop, surfaced as `TaskResult.stats`). */
4
5
  export interface Stats {
5
6
  turns: number;
@@ -134,15 +135,14 @@ export interface ResultFlags {
134
135
  * path the terminal is `completed`, and this is what tells that completion apart from a natural
135
136
  * one — the model did not finish; the person stopped it and the run awaits their direction). */
136
137
  haltedOnUserRejection?: boolean;
137
- /** A `TaskStream.halt` (the bare user interrupt, CC Esc form) was accepted
138
- * while the run was live; echoed on `TaskResult.haltedByUser`. Pure pass-through on every
139
- * terminal (the sibling `haltedOnUserRejection` law: the fact is about the leg that ran) PLUS
140
- * one status decision it owns (slot 9.7 below): a run whose ONLY stop was this halt reads
141
- * `"completed"`, never the `!final` failure and never the aborted terminal whether the halt
142
- * landed before the model said anything (empty result) or cut a turn whose partial text the
143
- * brain kept (that text IS the result). The person stopped the run, which is the verb working,
144
- * not a defect. A REAL abort/limit that also fired still owns the terminal above it. */
145
- userHalted?: boolean;
138
+ /** the harness's STATEMENT of what the run's halt did (see `HaltConsumption`):
139
+ * the loop's report that the halt's cut settled the final turn, and/or the boundary consult that
140
+ * stopped the loop for the halt's request. This is the ONLY input to `TaskResult.haltedByUser`
141
+ * (`haltConsumed`, halt-attribution.ts) and to slot 9.7 the assembly never infers causation from the
142
+ * final's shape or from the verb's acceptance. A halt that was accepted but consumed nothing (the run
143
+ * ended for its own reason first) leaves this empty: the original terminal stands and the seat is
144
+ * NOT signed (the settle lane mints `task.halt_unconsumed` for it). */
145
+ haltConsumption?: HaltConsumption;
146
146
  /** Call ids of answered-but-never-collected questions, echoed on
147
147
  * `TaskResult.strandedHumanAnswers`. Pure pass-through; empty/absent ⇒ the field is omitted. The
148
148
  * optional `onError` alert is NOT the disclosure — this mandatory result face is. */
@@ -298,13 +298,15 @@ export declare function terminalCauseOf(final: AssistantMessage | undefined, sta
298
298
  * 9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
299
299
  * (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
300
300
  * normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
301
- * EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`),
301
+ * EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`: the
302
+ * loop reported the halt's cut settled the final turn — never shape inference),
302
303
  * and EXCEPT when the answer had already settled (`answerSettled`: a clean `"stop"` final, not
303
304
  * partial) — a stop landing on the run's tail cuts no work, so the run falls through to 12
304
305
  * (`completed`, the answer as `result`, no `limits.*` code for the axis; three axes, one law)
305
- * 9.7 `userHalted` + (no `final` | the halt's own aborted `final`) → status `"completed"` (a
306
- * bare user halt — the verb working, not a defect; below the aborts on purpose, a real
307
- * abort/limit that also fired owns the terminal and the halt rides as the pass-through seat)
306
+ * 9.7 halt CONSUMED (`haltConsumed(flags.haltConsumption)`) + (no `final` | the halt's own aborted
307
+ * `final`) → status `"completed"` (a bare user halt — the verb working, not a defect; below the
308
+ * aborts on purpose, a real abort/limit that also fired owns the terminal and the halt rides as
309
+ * the pass-through seat; an accepted-but-unconsumed halt never reaches this slot)
308
310
  * 10. no `final` → status `"failed"`
309
311
  * 11. stopReason error → brain `[code]` prefix (or `"conflict"` from the storage-layer flag)
310
312
  * 12. else → `"completed"`
@@ -1,6 +1,7 @@
1
1
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
2
  import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
3
3
  import { terminalProjection } from "./terminal-projection.js";
4
+ import { haltConsumed } from "./halt-attribution.js";
4
5
  const SALVAGE_ELIGIBLE_TERMINALS = new Set([
5
6
  "output.degenerate",
6
7
  "limits.max_tokens_exceeded",
@@ -47,7 +48,7 @@ export function answerSettled(final, continuationCut = false) {
47
48
  export function terminalCauseOf(final, stats, flags) {
48
49
  let terminal;
49
50
  let apiFailure;
50
- const haltOwnsAbortedFinal = flags.userHalted === true &&
51
+ const haltOwnsAbortedFinal = flags.haltConsumption?.turnCut === "halt" &&
51
52
  final?.stopReason === "aborted" &&
52
53
  !flags.abortedLive &&
53
54
  flags.abortedForTimeout !== true &&
@@ -103,7 +104,7 @@ export function terminalCauseOf(final, stats, flags) {
103
104
  const code = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
104
105
  terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message: final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted") };
105
106
  }
106
- else if (flags.userHalted === true && (!final || haltOwnsAbortedFinal)) {
107
+ else if (haltConsumed(flags.haltConsumption) && (!final || haltOwnsAbortedFinal)) {
107
108
  terminal = { kind: "completed" };
108
109
  }
109
110
  else if (!final) {
@@ -155,7 +156,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
155
156
  const result = text;
156
157
  let salvagedOutput;
157
158
  const { terminal, apiFailure } = terminalCauseOf(final, stats, flags);
158
- const { status, errorCode } = terminalProjection(terminal);
159
+ const { errorCode } = terminalProjection(terminal);
159
160
  if (errorCode !== undefined && SALVAGE_ELIGIBLE_TERMINALS.has(errorCode)) {
160
161
  salvagedOutput = text.trim() || undefined;
161
162
  }
@@ -164,6 +165,6 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
164
165
  void _internalCompaction;
165
166
  if (flags.unpricedSpend)
166
167
  delete publicStats.costMicroUsd;
167
- const stampHaltedByUser = flags.userHalted === true && status !== "suspended" && status !== "needs_review";
168
+ const stampHaltedByUser = haltConsumed(flags.haltConsumption);
168
169
  return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
169
170
  }
@@ -30,96 +30,32 @@ import type { Prepared } from "./contracts.js";
30
30
  */
31
31
  export declare function buildWorkingFileAttachments(spec: TaskSpec, prepared: Prepared): MaybeCompactOptions["workingFileAttachments"];
32
32
  /**
33
- * `MaybeCompactOptions.contextInstructionFiles`: the seat that delivers the deployment's own
34
- * instruction-file content (the CLAUDE.md family) to the SUMMARIZER, assembled once here and spread
35
- * on all three lanes. Without it the compaction boundary is the one place a long task loses the
36
- * standing instructions every other turn has been reading — and it loses them exactly where the
37
- * summary REPLACES the transcript that carried them, so the loss is durable.
33
+ * `MaybeCompactOptions.contextInstructionFiles`: the deployment's instruction-file content (the CLAUDE.md
34
+ * family) delivered to the SUMMARIZER, assembled once here and spread on all three lanes — the compaction
35
+ * boundary is where the summary REPLACES the transcript that carried those instructions.
38
36
  *
39
- * WHICH BYTES, and why the neighbours are not candidates. `Prepared.projectInstructionContent` =
40
- * the RAW loader text (`ProjectMemoryLoad.content`, exactly the string prepare-task feeds
41
- * `composeMemoryBlock(projectMem, "project")`), captured beside the existing `instructionSources`
42
- * capture under the SAME non-blank predicate that composes the project layer:
43
- * · the assembled `memoryBlock` is NOT a candidate. It is the memory ENGINE's composed layers —
44
- * bytes the model itself authored in-band this session through the memory tools — with the
45
- * project layer appended at the tail. The seat's contract is host/systemPrompt tier and says in
46
- * terms: never from tool output, model text, or any other in-band channel. Handing
47
- * model-authored bytes an envelope whose preamble tells the summarizer they may instruct it is
48
- * the laundering shape, not a purity nit. (Mechanically moot too — `memoryBlock` is a
49
- * prepare-task local and never reaches `Prepared`.)
50
- * · the COMPOSED project block is not it either, and this one is checkable rather than a
51
- * judgement: `contextInstructionFilesSection` neutralizes the whole authority family
52
- * (ENGINE_AUTHORITY_ENVELOPE_TAGS) over its payload, and `user_memory`/`scope` ARE in that
53
- * family so feeding a composed block would have the engine rewrite the tags of its own
54
- * composition. Raw text in, one fence at the mint.
55
- * · `prepared.instructionSources` carries path + contentHash only, by design (core never learns a
56
- * file-name policy), so it cannot serve this seat at all.
57
- *
58
- * PROVENANCE, stated plainly because "the host supplies it" is the easy half of the truth. On this
59
- * engine the value is REPOSITORY-CONTROLLED text: the loading block one file over calls a CLAUDE.md
60
- * of unknown authorship untrusted in so many words, the MAIN lane's own framing tells the model it is
61
- * "repository-controlled DATA, not instructions to obey", and this seat hands the same bytes to the
62
- * summarizer with instruction standing for the summarization task. Delivery THROUGH a host callback
63
- * is not host AUTHORSHIP: a prior agent, or anyone who can land a commit, can write a
64
- * "## Compact Instructions" section, and the mint's neutralization stops tag breakout — not a
65
- * sentence asking for a fact to be left out of the summary that replaces the transcript.
66
- * Why it is nevertheless this value: the seat's own contract names this path ("hosts pass the same
67
- * instruction-file content their prompt assembly mounts... on the runner path"), the parity form this
68
- * channel exists to reach reads the user's and the project's instruction files at every boundary with
69
- * exactly this standing, and the two alternatives are worse for reasons above rather than better.
70
- * The standing is also narrower than the framing gap suggests — the section's preamble scopes it to
71
- * guidance about HOW to summarize and classifies the rest as background — and a deployment that does
72
- * not want it has two exits that need no code: do not wire the loader, or return content it vouches
73
- * for. Recorded as an open design candidate rather than built here, because it is a contract change
74
- * and not a wiring one: a SEPARATE deployment-vouched summarization-instructions seat, leaving
75
- * repository-derived context at data tier.
76
- *
77
- * RULING ① — BOTH summary forms, no per-form split. Fork eligibility is decided per pass INSIDE
78
- * maybeCompact (main-model-only, recorded-request-present, not lossy), so a call site cannot say
79
- * "independent form only"; the closest approximation — pass the seat only when `prepared.compModel`
80
- * is set, since an independent compaction model has no main prefix to fork — leaves the hole the
81
- * wrong way round: the configuration it does NOT cover (no compaction model) is precisely the one
82
- * where a fork degrades mid-pass to the independent form, which would then run WITHOUT the files.
83
- * Feeding both forms closes that case by construction, and the library face already handles the fork
84
- * half (the section rides the APPENDED instruction message, never the cached prefix, so the fork's
85
- * whole economic point is untouched).
86
- *
87
- * RULING ② — no new size knob. The bound is the library's own `fitContextInstructionFilesSection`
88
- * (independent form: cap against the compaction model's window, disclosed elision, and a complete
89
- * yield when not even the framing fits). Its three KNOWN LIMITS are accepted here KNOWINGLY, as the
90
- * pre-condition of this wiring rather than something it discovered: a token-dense payload (CJK,
91
- * emoji) can pass a chars-domain fit and still overflow the gateway's tokenizer; an unknown
92
- * `contextWindow` leaves the section unbounded (the conversation clamp declines to guess there too);
93
- * and the FORK form cannot fit at all — its bound is a prompt-too-long that degrades ONCE to the
94
- * independent form, which the engine's own note records as real but NOT total (an oversize rejection
95
- * the classifier cannot read as prompt-too-long returns terminally instead).
96
- *
97
- * HOW BIG THAT ACCEPTANCE ACTUALLY IS — measured, because the sentence above is easy to read more
98
- * comfortably than it deserves. The fitted form is NOT the default path: when a fork is eligible and
99
- * the model answers in its envelope, the fork IS the pass, and its section is delivered WHOLE at any
100
- * size. The independent form — the only one that fits — runs when there is no fork to take or the
101
- * fork's answer did not conform. Both facts are pinned in the REF-A2 suite (a 55 KB payload: whole on
102
- * the fork, elided on the independent fallback). And when a limit does bite, the failure is not a
103
- * quiet degradation: the section is a FIXED part of the request, the reactive retry loop may drop
104
- * only CONVERSATION groups, so a request the SECTION overflowed cannot converge and the boundary
105
- * fails — the task loses that compaction (or that prompt-too-long recovery, or its end-of-task
106
- * summary) rather than sending a shorter one. The exposure is what the wiring widened: this was
107
- * previously reachable only by hosts calling the library directly, and is now reachable by every
108
- * deployment that wires `loadProjectMemory` with a large instruction file and an unknown or small
109
- * window. Making the section participate in the retry loop is the structural answer; it is a change
110
- * to that loop's contract, tracked at the library face, and a runner-side knob would only move the
111
- * decision to a place with less information.
112
- *
113
- * WHAT IT COSTS, measured rather than rounded: every deployment that wires `loadProjectMemory` now
114
- * sends its instruction-file content plus EXACTLY 716 chars of framing (the preamble and the fence)
115
- * on EVERY summarization request, both forms, once per pass — where a real project instruction file
116
- * in this family runs to tens of KB, so the payload, not the framing, is the bill. The number is
117
- * pinned in the REF-A2 suite against the engine's own renderer, so a preamble edit shows up as a
118
- * changed assertion rather than as prose drifting away from the code it describes.
119
- * Deployments that never wired the loader pay nothing: the key is absent, not
120
- * `undefined`, so their requests are byte-identical to the pre-wiring shape on the independent and
121
- * UPDATE passes (the FORK form appends its ~860-char authority clause with or without the seat, and
122
- * always did).
37
+ * Contract:
38
+ * - Bytes = `Prepared.projectInstructionContent`, the RAW loader text captured under the same non-blank
39
+ * predicate that composes the project layer. Not the assembled `memoryBlock` (model-authored in-band
40
+ * bytes would gain an instructing envelope), not the COMPOSED project block (`contextInstructionFilesSection`
41
+ * neutralizes the whole authority family, `user_memory`/`scope` included), not `instructionSources`
42
+ * (path + hash only). Raw text in, one fence at the mint.
43
+ * - Provenance: REPOSITORY-CONTROLLED text delivered through a host callback delivery is not authorship;
44
+ * the mint's neutralization stops tag breakout, not a sentence asking for an omission. Two no-code exits
45
+ * for a deployment that does not want it: do not wire the loader, or return vouched content. A separate
46
+ * deployment-vouched summarization-instructions seat is an open design candidate, not built here.
47
+ * - Both summary forms, no per-form split: fork eligibility is decided per pass inside maybeCompact, and the
48
+ * form NOT covered by a `compModel`-gated split is exactly the fork that degrades mid-pass to the
49
+ * independent form; the section rides the APPENDED instruction message, never the cached prefix.
50
+ * - No new size knob: the bound is `fitContextInstructionFilesSection` (independent form only). Its three
51
+ * known limits are accepted knowingly: a token-dense payload can pass a chars-domain fit and still overflow;
52
+ * an unknown `contextWindow` leaves the section unbounded; the FORK form cannot fit and degrades ONCE. When
53
+ * a limit bites the boundary FAILS (the section is a fixed request part the retry loop cannot drop) — the
54
+ * task loses that compaction rather than sending a shorter one; making the section participate in the
55
+ * retry loop is a library-face contract change, tracked there.
56
+ * - Cost: instruction-file content + exactly 716 chars of framing on every summarization request, both
57
+ * forms, once per pass (pinned in the REF-A2 suite). Deployments that never wired the loader pay nothing:
58
+ * the key is ABSENT, so their requests are byte-identical to the pre-wiring shape.
123
59
  */
124
60
  export declare function contextInstructionFilesOption(prepared: Prepared): Partial<Pick<MaybeCompactOptions, "contextInstructionFiles">>;
125
61
  /**
@@ -28,7 +28,7 @@ import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
28
28
  import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
29
29
  import type { ActorAssertion, Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
30
30
  import type { ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
31
- import type { ClearedProjectionLedger, ContextEditMachine, OccurrenceIndex } from "../context-edit.js";
31
+ import type { ClearedProjectionLedger, MicroCompactMachine, OccurrenceIndex } from "../context-edit.js";
32
32
  import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, PostToolBatchCall, PostToolBatchResult, ToolGateInput } from "../hooks.js";
33
33
  import type { MaterializedMcp } from "../mcp.js";
34
34
  import type { MemoryEngine } from "../memory-engine/engine.js";
@@ -1051,16 +1051,14 @@ export interface Prepared {
1051
1051
  * `context-edit.ts`'s ledger note; per-run in-memory, so durable resume / `resumeAt` rebuilds
1052
1052
  * start EMPTY by construction), the last request's projection seat (what the provider actually
1053
1053
  * saw — the MC-R rejection arm computes its candidates and savings on THIS view, never on the
1054
- * raw session rebuild), the MC-R knob, and the slice-3 arm-B seat. The explicit opt-out
1055
- * (`machine: "legacy"` + MC-R off) ⇒ the ledger never gains an entry and every replay is a
1056
- * same-reference no-op (opt-out bytes unchanged). */
1054
+ * raw session rebuild), the MC-R knob, and the slice-3 arm-B seat. */
1057
1055
  microCompact: PreparedMicroCompact;
1058
1056
  }
1059
1057
  /** See {@link Prepared.microCompact}. */
1060
1058
  export interface PreparedMicroCompact {
1061
- /** The frontier-machine selection — `"off"` = no proactive frontier clearing (the unified
1062
- * machine instead gets its one blocking-point shot, slice-3 arm A). */
1063
- machine: "off" | ContextEditMachine;
1059
+ /** The frontier-machine selection — `"off"` = no proactive frontier clearing (the machine
1060
+ * instead gets its one blocking-point shot, slice-3 arm A). */
1061
+ machine: MicroCompactMachine;
1064
1062
  /** MC-R: one-shot clear-and-retry on a provider input-too-long rejection.
1065
1063
  * Default true since the slice-3 flip. */
1066
1064
  clearOnRejection: boolean;
@@ -1105,7 +1103,7 @@ export interface PrepareResume {
1105
1103
  * crash-interrupted; closing them with `[INTERRUPTED]` would DESTROY the suspended batch, §15.2 #7). */
1106
1104
  suspendedBatch: ReadonlySet<string>;
1107
1105
  /** The §4.bis correctness-state snapshot to re-seed (activeTools / outputRef / nestedStats /
1108
- * consolidationNotes / readFileState). */
1106
+ * readFileState). */
1109
1107
  seed: CheckpointState;
1110
1108
  /** How many times this task already suspended (the resumed checkpoint's
1111
1109
  * `suspendCount`). The next suspend mints `priorSuspendCount + 1`; past `maxSuspends` it fails
@@ -1393,7 +1391,10 @@ export interface RunInternals {
1393
1391
  * `"interactive"` tree's engine-stripped legs are exempt). TRUST POSTURE (ruled 2026-08-05):
1394
1392
  * RunInternals is a public trusted-caller parameter, so this flag is an honesty channel, not a
1395
1393
  * security gate — a caller forging it is a deployment lying to itself; resource-face safety
1396
- * stays with its own fail-closed mechanisms.
1394
+ * stays with its own fail-closed mechanisms. INHERITS DOWN THE TREE: a stripped leg's descendants are faceless by
1395
+ * the same engine act (the trusted ctx carries it as `ToolExecuteContext.questionFaceStripped`, the Workflow mount as
1396
+ * `parentQuestionFaceStripped`), so the posture-door exemption travels with the facelessness it explains; a leg
1397
+ * that strips on its own mints the same flag.
1397
1398
  */
1398
1399
  questionFaceStripped?: true;
1399
1400
  /**
@@ -1,5 +1,5 @@
1
1
  import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
2
- import { classifierDenyCauseOf, decisionText, engineSettlementOf } from "../tool-policy.js";
2
+ import { askUnresolvableCauseOf, classifierDenyCauseOf, decisionText, engineSettlementOf } from "../tool-policy.js";
3
3
  import { inlineUntrusted } from "../untrusted-text.js";
4
4
  import { formatHookFeedback } from "../reminder-mint.js";
5
5
  export function mintGateOutcome(facts) {
@@ -96,6 +96,15 @@ export function hookDenyExit(pass, r) {
96
96
  preToolContext: pass.preToolContext,
97
97
  };
98
98
  }
99
+ function unresolvableCauseOfSettlement(gate) {
100
+ if (gate.disposition.kind !== "denied" || gate.disposition.deniedBy !== "ask_resolution" || gate.settlement === undefined)
101
+ return undefined;
102
+ if (gate.settlement.kind === "approver_unavailable")
103
+ return "approver_unavailable";
104
+ if (gate.settlement.kind === "no_approver")
105
+ return "no_approver_wired";
106
+ return undefined;
107
+ }
99
108
  export async function exitGate(pass) {
100
109
  const { input, event, toolName, toolCallId, ledger, notifyPermissionDenied } = pass;
101
110
  if (pass.decision.action === "deny") {
@@ -106,6 +115,11 @@ export async function exitGate(pass) {
106
115
  if (pass.deniedBy === undefined)
107
116
  throw new Error(`the tool gate refused "${toolName}" without a refusing layer — every deny site attributes itself`);
108
117
  const gate = mintGateOutcome({ deniedBy: pass.deniedBy, ...(pass.denyCause !== undefined ? { cause: pass.denyCause } : {}), ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}) });
118
+ const unresolvable = askUnresolvableCauseOf(pass.decision, { toolCallId, toolName }) ?? unresolvableCauseOfSettlement(gate);
119
+ if (unresolvable !== undefined) {
120
+ const cause = unresolvable;
121
+ pass.notifier.notify(() => input.onAskUnresolvable?.({ toolName, toolCallId, cause, parkLaneExisted: pass.suspendAsk !== undefined }), "toolGate.onAskUnresolvable");
122
+ }
109
123
  await notifyPermissionDenied({
110
124
  toolName,
111
125
  input: cloneObserverInput(pass.currentInput),
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Halt attribution, the consumer half: the ONE predicate that turns the harness's
3
+ * consumption statement ({@link HaltConsumption}) into the result's `haltedByUser` seat, and the disclosure
4
+ * for the halt that was accepted but never consumed. Both readers of the statement live here so the law
5
+ * has one spelling: signed ⇔ consumed; unconsumed ⇔ absent + `task.halt_unconsumed`.
6
+ */
7
+ import type { HaltConsumption } from "../../internal/harness.js";
8
+ import { type RunnerDeps } from "../types.js";
9
+ /**
10
+ * The attribution law: `haltedByUser` ⇔ the halt was CONSUMED by the machinery — its cut settled a turn (the
11
+ * loop's report, stamped `"halt"` at the seat's abort) OR its stop request was among the sources a boundary
12
+ * consult stopped the loop for. A halt merely ACCEPTED (latched, receipt answered) while the run ended for its
13
+ * own reason is not consumed and never signs (fail-closed: rather absent than guessed).
14
+ */
15
+ export declare function haltConsumed(c: HaltConsumption | undefined): boolean;
16
+ export interface UnconsumedHaltDisclosureInput {
17
+ /** The verb's acceptance fact (`loopLatch.userHalted`): a halt was latched on this run. */
18
+ accepted: boolean;
19
+ /** The harness's statement, read after the prompt settled. */
20
+ consumption: HaltConsumption | undefined;
21
+ onNotice: RunnerDeps["onNotice"];
22
+ sessionId: string;
23
+ runId: string;
24
+ taskId: string | undefined;
25
+ }
26
+ /**
27
+ * The ACCEPTED-but-UNCONSUMED halt: the verb latched the stop (and may have cut a seat), but the run ended for
28
+ * its own reason before the machinery consumed it — no settlement took the halt's cut as its cause and no
29
+ * boundary consult stopped the loop for it (a brain minting its own `aborted` final, a natural completion past
30
+ * the final commit point, an approval park whose commit won the race). The result reads the run's own terminal
31
+ * without `haltedByUser`; this notice is the halt's trace — the same code the verb mints on its own arm (a halt
32
+ * refused by an already-owned ending), one fact with one spelling, the two mint points disjoint: the verb arm mints
33
+ * only while NO halt has been accepted on the run; once one was, this arm alone speaks. One per such run. Returns whether it was minted.
34
+ */
35
+ export declare function discloseUnconsumedHalt(input: UnconsumedHaltDisclosureInput): boolean;
@@ -0,0 +1,16 @@
1
+ import { deliverEngineNotice } from "../types.js";
2
+ export function haltConsumed(c) {
3
+ return c?.turnCut === "halt" || c?.boundaryStop?.sources.includes("halt") === true;
4
+ }
5
+ export function discloseUnconsumedHalt(input) {
6
+ if (!input.accepted || haltConsumed(input.consumption))
7
+ return false;
8
+ deliverEngineNotice(input.onNotice, {
9
+ code: "task.halt_unconsumed",
10
+ message: "a user halt was accepted, but the run ended for its own reason before the halt stopped or cut anything: " +
11
+ "no turn was settled as its cut and no boundary stopped for it — the run's own ending stands, and the result " +
12
+ "will not carry haltedByUser for this halt.",
13
+ detail: { sessionId: input.sessionId, runId: input.runId, ...(input.taskId !== undefined ? { taskId: input.taskId } : {}) },
14
+ });
15
+ return true;
16
+ }
@@ -23,8 +23,6 @@ export declare const DEFAULT_CONSOLIDATION_BAND: {
23
23
  export declare const DEFAULT_CONSOLIDATION_SEARCH_LIMIT = 20;
24
24
  /** Default cap on this-task notes consolidated in one batch (F6 token bound). */
25
25
  export declare const DEFAULT_CONSOLIDATION_MAX_NOTES = 50;
26
- /** Default independent timeout for the whole consolidation pass (seconds). */
27
- export declare const DEFAULT_CONSOLIDATION_TIMEOUT_SEC = 30;
28
26
  /** Normalize a note body for the EXACT-restatement check in the `<lo` near-dup cheap path. Case- + whitespace-
29
27
  * insensitive ONLY (lower-case, collapse whitespace runs, trim); it PRESERVES every symbol/punctuation so it can
30
28
  * NOT false-equate semantically-distinct facts. (The earlier `[^\p{L}\p{N}]`-stripping
@@ -4,7 +4,6 @@ import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
4
4
  export const DEFAULT_CONSOLIDATION_BAND = { lo: 0.05, hi: 0.3 };
5
5
  export const DEFAULT_CONSOLIDATION_SEARCH_LIMIT = 20;
6
6
  export const DEFAULT_CONSOLIDATION_MAX_NOTES = 50;
7
- export const DEFAULT_CONSOLIDATION_TIMEOUT_SEC = 30;
8
7
  export function normalizeForExactMatch(s) {
9
8
  return s.toLowerCase().replace(/\s+/g, " ").trim();
10
9
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * PrepareTask's Artifact phase: the `RunnerDeps.artifactHost` seat is read once; an absent seat mounts
3
+ * nothing and announces nothing; a seat whose declaration fails an invariant mounts nothing and announces
4
+ * `config.artifact_host_invalid` (operator audience) so a bad wiring can never read as "no hosting"; a
5
+ * valid seat mounts ONE `Artifact` tool assembled from the declaration, on the same optional band the LSP
6
+ * seat uses. The tool's local-file read goes through the run's fs fence (root containment, read-deny,
7
+ * read face) exactly as `Read` does, then reads the bytes once from the execution env. A local stub's store
8
+ * (`capabilities.storageDir`) must lie OUTSIDE every root the run may write or read — the task root and both
9
+ * classes of additional directories — because a ledger the model can edit through the fs tools is not a trust
10
+ * boundary; a store inside one refuses the mount with the same loud notice, naming the directory and the root.
11
+ * Synchronous: the stretch has no await (the file read is a closure the tool calls later).
12
+ */
13
+ import type { ExecutionEnv } from "../../internal/harness.js";
14
+ import { type ArtifactPlane } from "../artifact-host.js";
15
+ import { RosterBuilder } from "../tool-roster.js";
16
+ import type { AgentTool, RunnerDeps } from "../types.js";
17
+ import type { ReadDenyMatcher, ReadFace } from "../../tools/fs/index.js";
18
+ import type { Prepared } from "./contracts.js";
19
+ export interface PrepareArtifactInput {
20
+ /** borrowed-readonly — the deployment's hosting seat and the notice sink the refusal arm announces on. */
21
+ deps: Pick<RunnerDeps, "artifactHost" | "onNotice">;
22
+ /** borrowed-readonly — the engine-minted run id: the host's `onMount` key and every call context's `runId`. */
23
+ runId: string;
24
+ /** borrowed-readonly — the host task id, the call context's audit attribution. */
25
+ taskId: string;
26
+ /** borrowed-readonly — the acquired session id (notice attribution and the call context's `sessionId`). */
27
+ sessionId: string;
28
+ /** borrowed-readonly — the spawned name this run carries, when any (call-context attribution only). */
29
+ agentName: string | undefined;
30
+ /** borrowed-readonly — the run's resolved env: the byte source of a published local file. */
31
+ executionEnv: ExecutionEnv;
32
+ /** borrowed-readonly — the canonical task root the fs fence contains reads to; undefined ⇒ no local-file publish. */
33
+ rootCanonical: string | undefined;
34
+ /** borrowed-readonly — the extra containment roots (write and read-only) the fence admits. */
35
+ additionalRootsCanonical: readonly string[];
36
+ /** borrowed-readonly — the read-deny matcher in force for the run, when any. */
37
+ readDenyMatcher: ReadDenyMatcher | undefined;
38
+ /** borrowed-readonly — the resolved read face (`"open"` lifts root containment for reads). */
39
+ resolvedReadFace: ReadFace | undefined;
40
+ /** borrowed-readonly — the tracked-cwd cell; `current` is read per call as the relative-path base. */
41
+ handsCwdRef: Prepared["cwdRef"];
42
+ /** borrowed-mutable — the run's roster builder; writer here: ONE mount when the seat is valid. */
43
+ roster: RosterBuilder;
44
+ /** borrowed-readonly — the first-party result-offload wrapper every core mount passes through (a large read or a conflict snapshot is offloaded, not inlined). */
45
+ firstPartyOffload: (tool: AgentTool) => AgentTool;
46
+ /** borrowed-mutable — the env-hand membership set the memory content-origin face reads (a tool that reads the execution env's files is a hand); writer here: `add("Artifact")` on the mount. */
47
+ envHandToolNames: Set<string>;
48
+ }
49
+ export interface PrepareArtifactResult {
50
+ /** owned — the mounted face's plane, or undefined when nothing mounted (absent seat, or an invalid one). */
51
+ artifactPlane: ArtifactPlane | undefined;
52
+ }
53
+ export declare function prepareArtifact(input: PrepareArtifactInput): PrepareArtifactResult;
@@ -0,0 +1,77 @@
1
+ import { readArtifactHostCapabilities } from "../artifact-host.js";
2
+ import { ARTIFACT_TOOL_NAME } from "../tool-catalog-entries.js";
3
+ import { deliverEngineNotice } from "../engine-notice.js";
4
+ import { RosterBuilder } from "../tool-roster.js";
5
+ import { defineTool } from "../tools.js";
6
+ import { inlineUntrusted } from "../untrusted-text.js";
7
+ import { existsSync, realpathSync } from "node:fs";
8
+ import { basename, dirname, join, resolve } from "node:path";
9
+ import { resolveKey, violationDetails, violationText, withinAnyRoot } from "../../tools/fs/safety.js";
10
+ import { createArtifactTool } from "../../tools/artifact/artifact-tool.js";
11
+ function realAncestorPath(p) {
12
+ const abs = resolve(p);
13
+ const tail = [];
14
+ let cur = abs;
15
+ while (!existsSync(cur)) {
16
+ const parent = dirname(cur);
17
+ if (parent === cur)
18
+ return abs;
19
+ tail.unshift(basename(cur));
20
+ cur = parent;
21
+ }
22
+ return tail.length === 0 ? realpathSync(cur) : join(realpathSync(cur), ...tail);
23
+ }
24
+ export function prepareArtifact(input) {
25
+ const { deps, runId, taskId, sessionId, agentName, executionEnv, rootCanonical, additionalRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, roster, firstPartyOffload, envHandToolNames } = input;
26
+ const host = deps.artifactHost;
27
+ if (host === undefined)
28
+ return { artifactPlane: undefined };
29
+ const refuse = (problem) => {
30
+ deliverEngineNotice(deps.onNotice, {
31
+ code: "config.artifact_host_invalid",
32
+ message: `RunnerDeps.artifactHost is wired but unusable, so no Artifact tool is mounted for this task: ${problem}`,
33
+ detail: { problem, sessionId, runId },
34
+ });
35
+ return { artifactPlane: undefined };
36
+ };
37
+ const verdict = readArtifactHostCapabilities(host);
38
+ if (!verdict.ok)
39
+ return refuse(verdict.problem);
40
+ const storageDir = verdict.capabilities.storageDir;
41
+ if (storageDir !== undefined && rootCanonical !== undefined) {
42
+ const real = realAncestorPath(storageDir);
43
+ const inside = [rootCanonical, ...additionalRootsCanonical].find((root) => withinAnyRoot([root], real));
44
+ if (inside !== undefined)
45
+ return refuse(`the local artifact store ${storageDir} lies inside a root this run may reach through its file tools (${inside}); put the store outside every task root and additional directory`);
46
+ }
47
+ try {
48
+ host.onMount?.(runId);
49
+ }
50
+ catch (err) {
51
+ return refuse(`artifactHost.onMount threw: ${err instanceof Error ? err.message : String(err)}`);
52
+ }
53
+ const readLocalFile = async (path, maxBytes, signal) => {
54
+ if (rootCanonical === undefined)
55
+ return { ok: false, reason: "refused", text: "Error (Artifact): this task has no filesystem root, so a local file cannot be published." };
56
+ const rk = await resolveKey(executionEnv, rootCanonical, path, signal, handsCwdRef?.current, additionalRootsCanonical, undefined, readDenyMatcher, resolvedReadFace);
57
+ if (!rk.ok)
58
+ return { ok: false, reason: "refused", text: violationText("Artifact", rk.violation), details: violationDetails(rk.violation) };
59
+ const meta = await executionEnv.fileInfo(rk.key, signal);
60
+ if (!meta.ok)
61
+ return { ok: false, reason: "refused", text: `Error (Artifact): cannot stat ${inlineUntrusted(path, 200)} (${inlineUntrusted(meta.error.code, 40)}).` };
62
+ if (meta.value.kind !== "file")
63
+ return { ok: false, reason: "not_a_file", kind: meta.value.kind };
64
+ if (meta.value.size > maxBytes)
65
+ return { ok: false, reason: "too_large", size: meta.value.size, limit: maxBytes, judgedOn: "metadata" };
66
+ const r = await executionEnv.readBinaryFile(rk.key, signal);
67
+ if (!r.ok)
68
+ return { ok: false, reason: "refused", text: `Error (Artifact): cannot read ${inlineUntrusted(path, 200)} (${inlineUntrusted(r.error.code, 40)}).` };
69
+ if (r.value.byteLength > maxBytes)
70
+ return { ok: false, reason: "too_large", size: r.value.byteLength, limit: maxBytes, judgedOn: "bytes" };
71
+ return { ok: true, key: rk.key, bytes: r.value };
72
+ };
73
+ const artifactSpec = createArtifactTool({ host, capabilities: verdict.capabilities, runId, taskId, sessionId, ...(agentName !== undefined ? { agentName } : {}), readLocalFile });
74
+ roster.mount(firstPartyOffload(defineTool(artifactSpec)), { source: "builtin", mountedBy: "optional" });
75
+ envHandToolNames.add(ARTIFACT_TOOL_NAME);
76
+ return { artifactPlane: verdict.capabilities.plane };
77
+ }