@sema-agent/core 5.29.0 → 5.31.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 (66) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/dist/agents/send-message-tool.js +2 -0
  3. package/dist/agents/subagent.d.ts +2 -0
  4. package/dist/agents/subagent.js +6 -0
  5. package/dist/agents/teacher.js +2 -0
  6. package/dist/agents/verify.js +2 -0
  7. package/dist/core/auto-compaction.d.ts +5 -1
  8. package/dist/core/auto-compaction.js +10 -1
  9. package/dist/core/checkpoint-store.d.ts +51 -5
  10. package/dist/core/checkpoint-store.js +2 -1
  11. package/dist/core/hooks.d.ts +12 -1
  12. package/dist/core/hooks.js +8 -2
  13. package/dist/core/permission-rules.js +2 -2
  14. package/dist/core/runner/prepare-task.d.ts +21 -5
  15. package/dist/core/runner/prepare-task.js +115 -26
  16. package/dist/core/runner/runtask.js +29 -3
  17. package/dist/core/runner/session-rule-policy.d.ts +3 -2
  18. package/dist/core/runner/tool-output-projection.js +1 -1
  19. package/dist/core/sensitive-path-policy.js +5 -16
  20. package/dist/core/store-contracts/tool-result-store-contract.d.ts +4 -1
  21. package/dist/core/store-contracts/tool-result-store-contract.js +26 -1
  22. package/dist/core/tighten-task-spec.js +18 -0
  23. package/dist/core/tool-policy.d.ts +20 -1
  24. package/dist/core/tool-policy.js +31 -4
  25. package/dist/core/tool-result-store.d.ts +6 -4
  26. package/dist/core/tool-result-store.js +3 -1
  27. package/dist/core/types.d.ts +63 -1
  28. package/dist/engine/harness/types.d.ts +10 -0
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +3 -1
  31. package/dist/orchestration/run-workflow-tool.d.ts +26 -0
  32. package/dist/orchestration/run-workflow-tool.js +7 -4
  33. package/dist/orchestration/workflow-governance.d.ts +53 -3
  34. package/dist/orchestration/workflow-governance.js +162 -25
  35. package/dist/orchestration/workflow-primitives.d.ts +15 -1
  36. package/dist/orchestration/workflow-primitives.js +13 -2
  37. package/dist/prompt-assembly/epoch.js +2 -0
  38. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  39. package/dist/prompt-assembly/types.d.ts +4 -0
  40. package/dist/prompts/default.d.ts +14 -9
  41. package/dist/prompts/default.js +13 -3
  42. package/dist/tools/fs/bash-readonly-classifier.d.ts +53 -4
  43. package/dist/tools/fs/bash-readonly-classifier.js +148 -16
  44. package/dist/tools/fs/fs-bash.d.ts +7 -0
  45. package/dist/tools/fs/fs-bash.js +8 -3
  46. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  47. package/dist/tools/fs/fs-pdf.js +2 -2
  48. package/dist/tools/fs/fs-read.d.ts +1 -1
  49. package/dist/tools/fs/fs-read.js +11 -7
  50. package/dist/tools/fs/fs-search-tools.d.ts +4 -2
  51. package/dist/tools/fs/fs-search-tools.js +15 -8
  52. package/dist/tools/fs/fs-shared.d.ts +5 -1
  53. package/dist/tools/fs/fs-shared.js +8 -3
  54. package/dist/tools/fs/index.d.ts +18 -0
  55. package/dist/tools/fs/index.js +13 -2
  56. package/dist/tools/fs/read-deny.d.ts +110 -0
  57. package/dist/tools/fs/read-deny.js +159 -0
  58. package/dist/tools/fs/read-face.d.ts +49 -0
  59. package/dist/tools/fs/read-face.js +38 -0
  60. package/dist/tools/fs/repo-map.d.ts +3 -1
  61. package/dist/tools/fs/repo-map.js +11 -5
  62. package/dist/tools/fs/safety.d.ts +34 -11
  63. package/dist/tools/fs/safety.js +108 -8
  64. package/dist/tools/fs/search.d.ts +54 -5
  65. package/dist/tools/fs/search.js +107 -23
  66. package/package.json +1 -1
@@ -103,10 +103,12 @@ export interface ToolResultStore {
103
103
  * - **concurrency-tolerant**: an entry that disappears between enumeration and removal is honest
104
104
  * absence, not an error.
105
105
  *
106
- * Typed OPTIONAL, and deliberately NOT part of the published contract kit
107
- * (`toolResultStoreContract`): unlike `ownerOf`, a backend that cannot enumerate by owner is still
108
- * a usable offload store, so its absence must stay a checkable fact rather than a contract breach.
109
- * Present the store can complete a session deletion; absent the deployment owns that gap.
106
+ * Typed OPTIONAL, and an OPTIONAL leg of the published contract kit (`toolResultStoreContract`,
107
+ * its #196 case): unlike `ownerOf`, a backend that cannot enumerate by owner is still a usable
108
+ * offload store, so absence is never a contract breach the kit verifies the four-state
109
+ * semantics when the member is present and reports absence through its `onOptionalMember`
110
+ * callback (a checkable fact, not a failure). Present ⇒ the store can complete a session
111
+ * deletion; absent ⇒ the deployment owns that gap.
110
112
  *
111
113
  * Implementing it does NOT make a store `retention: "managed"` — that declaration promises the whole
112
114
  * {@link import("./retention.js").ManagedRetentionCapability} (domain enumeration, tombstones,
@@ -366,7 +366,9 @@ async function offloadOversizedDetailStrings(details, store, thresholdChars, ses
366
366
  const value = walk(details, []);
367
367
  const settled = await Promise.allSettled(pending.map((p) => p.done));
368
368
  const notices = new Map();
369
- pending.forEach((p, i) => void notices.set(p.marker, p.render(settled[i].status === "fulfilled")));
369
+ pending.forEach((p, i) => {
370
+ notices.set(p.marker, p.render(settled[i].status === "fulfilled"));
371
+ });
370
372
  const seen = new Set();
371
373
  const finalize = (node) => {
372
374
  if (typeof node !== "object" || node === null)
@@ -650,6 +650,21 @@ export interface ToolExecuteContext {
650
650
  * read-only boundary ended at one level. Strictly a capability REMOVAL — it can never widen a child.
651
651
  */
652
652
  handsReadOnly?: true;
653
+ /**
654
+ * design/199 — the READ-face clamp carrier ({@link TaskSpec.readFace}): present (as "roots") ONLY
655
+ * when this run RESOLVED to the roots face, so a delegation tool pins every child spec to roots —
656
+ * without it a roots-narrowed parent's children would resolve the shared deployment seat (possibly
657
+ * "open") and read wider than the parent could. Strictly a tightening carrier: an open parent adds
658
+ * no key, and nothing in the delegation lane can spell "open". F10 ruling (v1): this seat exists
659
+ * ONLY as the delegation carrier — no tool adapts its model-facing behavior on it.
660
+ */
661
+ readFace?: "roots";
662
+ /**
663
+ * design/199 件B — the parent task's OWN deny-set additions ({@link TaskSpec.readDenyPatterns}),
664
+ * traveling the delegation tree add-only (a child judges at least every entry its parent judged).
665
+ * Runner-filled trusted seat, never a model argument.
666
+ */
667
+ readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
653
668
  /**
654
669
  * The HARD-HEADLESS clamp ({@link TaskSpec.interactiveTools} set to `false` — "never mount a
655
670
  * human-facing tool on this run, whatever faces exist"), Runner-filled read-only on the same trusted
@@ -2109,6 +2124,27 @@ export interface TaskSpec {
2109
2124
  * config read.
2110
2125
  */
2111
2126
  additionalReadDirectories?: string[];
2127
+ /**
2128
+ * design/199 件B — TASK-layer ADDITIONS to the built-in sensitive-path READ deny set
2129
+ * ({@link import("../tools/fs/read-deny.js").READ_FACE_DEFAULT_DENY_ENTRIES}). Judged by the
2130
+ * structured read faces (Read/Grep/Glob/RepoMap, their traversals, the classify shell gate's
2131
+ * auto-allow probe and the compaction attachment reader) in BOTH containment modes. Add-only
2132
+ * everywhere (D-4 zero-shrink ruling): entries here UNION with the built-ins and the deployment's
2133
+ * {@link RunnerDeps.readDenyPatterns}; `[]` ≡ absent (union identity); nothing can remove a
2134
+ * built-in. Bad entry shapes refuse loudly at prepare (#123). The write faces are untouched.
2135
+ */
2136
+ readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
2137
+ /**
2138
+ * design/199 件A — the TASK layer's read-face containment declaration
2139
+ * ({@link import("../tools/fs/read-face.js").ReadFace}). "roots" pins this task (and, via the
2140
+ * tighten-only clamp, its whole delegation subtree) to the historical containment; "open" lifts
2141
+ * the roots judgment for the structured read faces — refused loudly under org governance (the
2142
+ * task layer only tightens there; the deployment opens via {@link RunnerDeps.readFace}) and
2143
+ * beside `handsReadOnly: true` (the verifier containment is load-bearing). Absent ⇒ the
2144
+ * deployment seat, then the engine default ("roots" — D-1b: an upgrade never opens implicitly).
2145
+ * Bad values refuse loudly (#123). Never affects the write faces.
2146
+ */
2147
+ readFace?: import("../tools/fs/read-face.js").ReadFace;
2112
2148
  /**
2113
2149
  * design/80 D-B — opt in to PLAN MODE: mount the first-party `present_plan` tool (CC `ExitPlanMode` parity).
2114
2150
  * The model calls it to present a plan; the engine then pauses with a durable `plan_review` checkpoint
@@ -4187,7 +4223,14 @@ export interface EngineNotice {
4187
4223
  * failed; THIS attempt stored nothing (the failure arm reports, it never re-inserts under the
4188
4224
  * ref) — an earlier attempt of the same idempotent re-put may already have stored the row, so
4189
4225
  * the notice claims a failed write, not an empty ref; `detail: { ref, sessionId, cause }`.
4190
- * Per-occurrence, not per-process-deduplicated: each failed write is a distinct fact. */
4226
+ * Per-occurrence, not per-process-deduplicated: each failed write is a distinct fact.
4227
+ * - `"workflow.governance_key_stripped"` (#235) — the fields of an LLM-authored workflow `agent(spec)`
4228
+ * that did NOT cross the governed default-deny whitelist: an unrecognized/control-plane field, or a
4229
+ * `readFace` value that is not the containment-tightening one. The spawn PROCEEDS on the deployment
4230
+ * baseline (the strip is not a refusal in this window), which is why the drop is announced;
4231
+ * `detail: { total, stripped: [{ key, reason }], omitted? }`, the rendered key list bounded in count
4232
+ * and length because the names come from the untrusted script. One aggregated notice per governed
4233
+ * child build, not de-duplicated across builds: each spec is a distinct fact. */
4191
4234
  code: string;
4192
4235
  /** The exact human-readable line the unwired build prints via `console.warn` — same words, one text. */
4193
4236
  message: string;
@@ -4265,6 +4308,25 @@ export interface RunnerDeps {
4265
4308
  * Runner 私有编排字段(taskRegistry/detachHub/execClamp/cwdRef/…)不在此面,永不开放。
4266
4309
  */
4267
4310
  hands?: HandsBandOptions;
4311
+ /**
4312
+ * design/199 件B — DEPLOYMENT-layer ADDITIONS to the built-in sensitive-path READ deny set
4313
+ * ({@link import("../tools/fs/read-deny.js").READ_FACE_DEFAULT_DENY_ENTRIES}); see
4314
+ * {@link TaskSpec.readDenyPatterns} for the judged surfaces. Add-only (D-4 zero-shrink ruling):
4315
+ * unions with the built-ins and any task-layer additions; there is no whole-table replacement
4316
+ * escape hatch, and `[]` ≡ absent. Bad entry shapes refuse loudly at prepare (#123).
4317
+ */
4318
+ readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
4319
+ /**
4320
+ * design/199 件A — the DEPLOYMENT's read-face declaration
4321
+ * ({@link import("../tools/fs/read-face.js").ReadFace}; see {@link TaskSpec.readFace} for the
4322
+ * task layer and the resolution order). An interactive single-user product declares
4323
+ * `readFace: "open"` here in one line; the engine default stays "roots" (D-1b). Under org
4324
+ * governance this seat may still open (it is the deployment's own declaration); the task layer
4325
+ * may not. Beside a read-only (verifier) mount this seat's "open" silently CLAMPS to roots (a
4326
+ * deployment default cannot override a load-bearing containment wall) — TaskSpec.readFace's own
4327
+ * "open" there is the genuine per-task contradiction, and that one still refuses loudly (#123).
4328
+ */
4329
+ readFace?: import("../tools/fs/read-face.js").ReadFace;
4268
4330
  /**
4269
4331
  * design/96 §20 (P1a) — DEPLOYMENT-DECLARED training knowledge cutoff per model id (e.g.
4270
4332
  * `{ "qwen-3.5-35b": "2025-01", "deepseek-pro": "2024-07" }`). When the resolved model id is present, the
@@ -349,6 +349,16 @@ export interface ExecutionEnv extends FileSystem, Shell {
349
349
  * mechanism exists to prevent.
350
350
  */
351
351
  readonly lifetimeStartedAt?: number;
352
+ /**
353
+ * #211 (design/199 seam) — does this environment's path NAMESPACE live on the CONTROL-PLANE HOST's
354
+ * filesystem? Declared by the ADAPTER: `false` for a virtual/sandboxed env whose paths never map
355
+ * onto the host (an in-memory env, a container the host cannot mount) — consumers that record
356
+ * host-path receipts (the workflow workspace observer's `worktreeDir`) then refuse to present this
357
+ * env's cwd as a host path. Omitted ⇒ the historical structural inference stays in force
358
+ * (isRemoteExecutionEnv shape detection; a plain NodeExecutionEnv is host-local). `true` is a
359
+ * harmless explicit spelling of the local default.
360
+ */
361
+ readonly hostLocalPaths?: boolean;
352
362
  }
353
363
  /** Base fields shared by append-only session tree entries. */
354
364
  export interface SessionTreeEntryBase {
package/dist/index.d.ts CHANGED
@@ -87,6 +87,8 @@ export type { InvariantKind, FunctionContract, Invariant, InvariantViolation, Ch
87
87
  export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
88
88
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, type BashReadonlyRootBoundary, type CompoundReadonlyVerdict, } from "./tools/fs/index.js";
89
89
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
90
+ export { READ_FACE_DEFAULT_DENY_ENTRIES, compileReadDeny, type ReadDenyEntry, type ReadDenyMatcher, type NormalizedReadDenyEntry, } from "./tools/fs/index.js";
91
+ export { resolveReadFace, type ReadFace, type ReadFaceInputs } from "./tools/fs/index.js";
90
92
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, toolResultContentSegment, MAX_MINTED_TOOL_RESULT_REF_CHARS, type ToolResultProvenance, assertToolResultProvenanceMatch, normalizeToolResultProvenance, toolResultProvenanceOf, ToolResultRefConflictError, TOOL_RESULT_REF_CONFLICT_CODE, type ToolResultStore, type ToolResultSlice, type ToolResultDeletionReport, } from "./core/tool-result-store.js";
91
93
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, type ActorAssertion, type PendingSteerEntry, type PendingSteerInput, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type RealApprovalGateBit, type ResourceLedger, type ResourceLimitReason, type PlatformLimitReason, } from "./core/checkpoint-store.js";
92
94
  export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, type UsageWindow, type UsageWindowStore, type UsageWindowReading, type UsageWindowRecord, type UsageSlot, type UsageBucketRow, } from "./core/usage-window-store.js";
@@ -168,7 +170,7 @@ export { encodeSurfacedKey, buildManifestText, validateSelectedIds, composeSelec
168
170
  export { runMemoryConsolidation, CONSOLIDATION_SYSTEM_PROMPT, DEFAULT_CONSOLIDATION_BAND, DEFAULT_CONSOLIDATION_SEARCH_LIMIT, DEFAULT_CONSOLIDATION_MAX_NOTES, DEFAULT_CONSOLIDATION_TIMEOUT_SEC, normalizeForExactMatch, type ConsolidationParams, type ConsolidationStats, type ConsolidationNote, type ConsolidationLLM, } from "./core/runner/memory-consolidation.js";
169
171
  export { consolidateScope, advanceCursorAfterInline, type ConsolidateScopeDeps, type ConsolidateScopeOptions, } from "./core/consolidate-scope.js";
170
172
  export { DEFAULT_COMPACTION_INSTRUCTIONS } from "./core/auto-compaction.js";
171
- export { DEFAULT_SYSTEM_PROMPT, CODE_AGENT_PROMPT, SUBAGENT_PROMPT, CODE_SYSTEM_PROMPT, MEMORY_GUIDANCE, MEMORY_SAFETY, MEMORY_HYGIENE, NO_PERSISTENT_MEMORY_NOTICE, OUTPUT_EFFICIENCY, CYBER_RISK, HARNESS_SECTION_ANCHOR, URL_SAFETY, SUMMARIZE_TOOL_RESULTS, EXECUTION_ENVIRONMENT, harnessContext, buildEnvironmentContext, buildGitSnapshot, buildScratchpadSection, GIT_STATUS_MAX_CHARS, formatLocalDate, type EnvironmentFacts, defaultPromptProvider, composeSystemPrompt, composeConstitution, constitutionBlocks, type PromptBlock, analyzePromptCacheFriendliness, assertPromptCacheFriendly, type PromptProvider, type StablePromptContext, type PromptCacheReport, type PromptTextDeclaration, } from "./prompts/default.js";
173
+ export { DEFAULT_SYSTEM_PROMPT, CODE_AGENT_PROMPT, SUBAGENT_PROMPT, CODE_SYSTEM_PROMPT, MEMORY_GUIDANCE, MEMORY_SAFETY, MEMORY_HYGIENE, NO_PERSISTENT_MEMORY_NOTICE, OUTPUT_EFFICIENCY, CYBER_RISK, HARNESS_SECTION_ANCHOR, URL_SAFETY, SUMMARIZE_TOOL_RESULTS, EXECUTION_ENVIRONMENT, EXECUTION_ENVIRONMENT_OPEN_READS, harnessContext, buildEnvironmentContext, buildGitSnapshot, buildScratchpadSection, GIT_STATUS_MAX_CHARS, formatLocalDate, type EnvironmentFacts, defaultPromptProvider, composeSystemPrompt, composeConstitution, constitutionBlocks, type PromptBlock, analyzePromptCacheFriendliness, assertPromptCacheFriendly, type PromptProvider, type StablePromptContext, type PromptCacheReport, type PromptTextDeclaration, } from "./prompts/default.js";
172
174
  export { SUPERVISOR_PROMPT, ORCHESTRATION_GUIDANCE, ORCHESTRATION_AWARENESS, GOAL_COMPLETION_GUIDANCE } from "./prompts/supervisor.js";
173
175
  export { compose, validatePack } from "./prompt-assembly/composer.js";
174
176
  export { SEMA_DEFAULT_PACK } from "./prompt-assembly/packs/sema-default.js";
package/dist/index.js CHANGED
@@ -68,6 +68,8 @@ export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
68
68
  export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
69
69
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, } from "./tools/fs/index.js";
70
70
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
71
+ export { READ_FACE_DEFAULT_DENY_ENTRIES, compileReadDeny, } from "./tools/fs/index.js";
72
+ export { resolveReadFace } from "./tools/fs/index.js";
71
73
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, toolResultContentSegment, MAX_MINTED_TOOL_RESULT_REF_CHARS, assertToolResultProvenanceMatch, normalizeToolResultProvenance, toolResultProvenanceOf, ToolResultRefConflictError, TOOL_RESULT_REF_CONFLICT_CODE, } from "./core/tool-result-store.js";
72
74
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
73
75
  export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, } from "./core/usage-window-store.js";
@@ -131,7 +133,7 @@ export { encodeSurfacedKey, buildManifestText, validateSelectedIds, composeSelec
131
133
  export { runMemoryConsolidation, CONSOLIDATION_SYSTEM_PROMPT, DEFAULT_CONSOLIDATION_BAND, DEFAULT_CONSOLIDATION_SEARCH_LIMIT, DEFAULT_CONSOLIDATION_MAX_NOTES, DEFAULT_CONSOLIDATION_TIMEOUT_SEC, normalizeForExactMatch, } from "./core/runner/memory-consolidation.js";
132
134
  export { consolidateScope, advanceCursorAfterInline, } from "./core/consolidate-scope.js";
133
135
  export { DEFAULT_COMPACTION_INSTRUCTIONS } from "./core/auto-compaction.js";
134
- export { DEFAULT_SYSTEM_PROMPT, CODE_AGENT_PROMPT, SUBAGENT_PROMPT, CODE_SYSTEM_PROMPT, MEMORY_GUIDANCE, MEMORY_SAFETY, MEMORY_HYGIENE, NO_PERSISTENT_MEMORY_NOTICE, OUTPUT_EFFICIENCY, CYBER_RISK, HARNESS_SECTION_ANCHOR, URL_SAFETY, SUMMARIZE_TOOL_RESULTS, EXECUTION_ENVIRONMENT, harnessContext, buildEnvironmentContext, buildGitSnapshot, buildScratchpadSection, GIT_STATUS_MAX_CHARS, formatLocalDate, defaultPromptProvider, composeSystemPrompt, composeConstitution, constitutionBlocks, analyzePromptCacheFriendliness, assertPromptCacheFriendly, } from "./prompts/default.js";
136
+ export { DEFAULT_SYSTEM_PROMPT, CODE_AGENT_PROMPT, SUBAGENT_PROMPT, CODE_SYSTEM_PROMPT, MEMORY_GUIDANCE, MEMORY_SAFETY, MEMORY_HYGIENE, NO_PERSISTENT_MEMORY_NOTICE, OUTPUT_EFFICIENCY, CYBER_RISK, HARNESS_SECTION_ANCHOR, URL_SAFETY, SUMMARIZE_TOOL_RESULTS, EXECUTION_ENVIRONMENT, EXECUTION_ENVIRONMENT_OPEN_READS, harnessContext, buildEnvironmentContext, buildGitSnapshot, buildScratchpadSection, GIT_STATUS_MAX_CHARS, formatLocalDate, defaultPromptProvider, composeSystemPrompt, composeConstitution, constitutionBlocks, analyzePromptCacheFriendliness, assertPromptCacheFriendly, } from "./prompts/default.js";
135
137
  export { SUPERVISOR_PROMPT, ORCHESTRATION_GUIDANCE, ORCHESTRATION_AWARENESS, GOAL_COMPLETION_GUIDANCE } from "./prompts/supervisor.js";
136
138
  export { compose, validatePack } from "./prompt-assembly/composer.js";
137
139
  export { SEMA_DEFAULT_PACK } from "./prompt-assembly/packs/sema-default.js";
@@ -219,6 +219,12 @@ export interface RunWorkflowToolDeps {
219
219
  /** Call-time getter for the HOST task's current thinking level — a spawned agent with no explicit
220
220
  * script/baseline `thinking` inherits it (the parentCwd/model-snapshot companion). */
221
221
  parentThinking?: () => import("../core/types.js").TaskSpec["thinking"];
222
+ /** 5.30 merge-rescan (design/199 parity gap) — call-time getter for the HOST task's RESOLVED
223
+ * read-face containment; folds stricter-wins into every spawned workflow child (see
224
+ * `buildWorkflowPrimitives`'s injection block for the full rationale). */
225
+ parentReadFace?: () => import("../core/types.js").TaskSpec["readFace"];
226
+ /** Twin of the above for the deny-set additions (union, not replace). */
227
+ parentReadDenyPatterns?: () => import("../core/types.js").TaskSpec["readDenyPatterns"];
222
228
  /** [1238](A) — call-time getter for the HOST task's RESOLVED Model object: a spawned agent whose
223
229
  * fold chain produced no model inherits the parent's full object (baseUrl/key routing included),
224
230
  * mirroring the subagent lane's ctx.model semantics. */
@@ -254,6 +260,26 @@ export interface RunWorkflowToolDeps {
254
260
  autoModeReview?: () => {
255
261
  decider: import("../core/auto-mode.js").AutoModeDecider;
256
262
  } | undefined;
263
+ /** 5.30 merge-rescan round 2 — the HOST task's durable OFF SWITCH (`spec.checkpointStore === null`,
264
+ * ruled 2026-08-04 to inherit into every spawned agent, see `buildWorkflowPrimitives`'s call
265
+ * above). Known at prepare-time (no lazy getter needed — `spec` is available at mount). A dep for
266
+ * the same reason as `forwardEvent`/`inheritedGateForChildren`: the auto-mounted tool's execute
267
+ * ctx is minimal `{toolCallId, signal}`, so `ctx.checkpointStoreDisabledForChildren` was a DEAD
268
+ * read there — the off-switch never actually reached this lane's children. */
269
+ parentCheckpointStoreDisabled?: boolean;
270
+ /** #235 — the deployment's structured notice sink (`RunnerDeps.onNotice`), forwarded into the governed
271
+ * build so the fields a script's agent spec wrote that did NOT reach the child are announced instead of
272
+ * vanishing (`workflow.governance_key_stripped`). A dep for the same reason as the seats above: this
273
+ * auto-mount's execute ctx is minimal. Absent ⇒ the historic `console.warn` loudness. */
274
+ onNotice?: (n: import("../core/types.js").EngineNotice) => void;
275
+ /** Twin of the above for design/148 S1's center-artifact inheritance (see the `ctx.centerArtifactDigest`/
276
+ * `ctx.centerSourceRevision` reads at the `startWorkflow` options site) — `centerAdoption` resolves
277
+ * LATE in prepare-task.ts (well after this tool's mount point), so this is a call-time getter, not
278
+ * a value, mirroring `parentReadFace`. `ctx.centerArtifactDigest` still wins when a wrapping path
279
+ * provides it. */
280
+ parentCenterArtifactDigest?: () => string | undefined;
281
+ /** Publish-provenance companion of the above. */
282
+ parentCenterSourceRevision?: () => string | undefined;
257
283
  }
258
284
  /**
259
285
  * Build the `run_workflow` tool. ASSERTS the runner is a hard sandbox (defense in depth — prepare-task only
@@ -167,7 +167,7 @@ export async function createRunWorkflowTool(d) {
167
167
  : {}),
168
168
  }
169
169
  : d.governanceBaseline;
170
- const governance = { baseline: baselineWithParentFace, models: d.models, caps: childCaps };
170
+ const governance = { baseline: baselineWithParentFace, models: d.models, caps: childCaps, onNotice: d.onNotice };
171
171
  const builtinsEnabled = d.builtinWorkflows !== false;
172
172
  const namedWorkflowSection = renderNamedWorkflowListing(await collectNamedWorkflowListings(d.scriptStore, builtinsEnabled));
173
173
  const sizeGuidelineSection = workflowSizeGuidelineSection(d.sizeGuideline ?? lim.sizeGuideline);
@@ -380,7 +380,7 @@ export async function createRunWorkflowTool(d) {
380
380
  return structuredError(`workflow script failed to compile: ${err instanceof Error ? err.message : String(err)}`);
381
381
  }
382
382
  const scriptFn = (wfCtx) => {
383
- const primitives = buildWorkflowPrimitives(wfCtx, governance, d.onAgentSpawn, d.parentThinking, principal, ctx.checkpointStoreDisabledForChildren === true);
383
+ const primitives = buildWorkflowPrimitives(wfCtx, governance, d.onAgentSpawn, d.parentThinking, principal, ctx.checkpointStoreDisabledForChildren === true || d.parentCheckpointStoreDisabled === true, d.parentReadFace, d.parentReadDenyPatterns);
384
384
  return d.scriptRunner.run({ scriptSource: script, primitives, scriptArgs: effectiveArgs, signal: wfCtx.signal }).then((r) => r.result);
385
385
  };
386
386
  if (ctx.signal?.aborted) {
@@ -389,6 +389,9 @@ export async function createRunWorkflowTool(d) {
389
389
  let handle;
390
390
  const workflowTaskId = d.taskRegistry?.mintTaskId("workflow");
391
391
  const autoModeReview = ctx.autoModeReview ?? d.autoModeReview?.();
392
+ const centerFromCtx = ctx.centerArtifactDigest !== undefined;
393
+ const parentCenterArtifactDigest = centerFromCtx ? ctx.centerArtifactDigest : d.parentCenterArtifactDigest?.();
394
+ const parentCenterSourceRevision = centerFromCtx ? ctx.centerSourceRevision : d.parentCenterSourceRevision?.();
392
395
  try {
393
396
  handle = startWorkflow(d.runner, scriptFn, {
394
397
  store: d.store,
@@ -417,8 +420,8 @@ export async function createRunWorkflowTool(d) {
417
420
  ...((ctx.interactionPosture ?? d.parentInteractionPosture) !== undefined ? { interactionPosture: (ctx.interactionPosture ?? d.parentInteractionPosture) } : {}),
418
421
  ...(d.parentModel !== undefined ? { defaultModel: d.parentModel } : {}),
419
422
  ...(d.parentGetApiKeyAndHeaders !== undefined ? { defaultGetApiKeyAndHeaders: d.parentGetApiKeyAndHeaders } : {}),
420
- ...(ctx.centerArtifactDigest !== undefined ? { parentCenterArtifactDigest: ctx.centerArtifactDigest } : {}),
421
- ...(ctx.centerSourceRevision !== undefined ? { parentCenterSourceRevision: ctx.centerSourceRevision } : {}),
423
+ ...(parentCenterArtifactDigest !== undefined ? { parentCenterArtifactDigest } : {}),
424
+ ...(parentCenterSourceRevision !== undefined ? { parentCenterSourceRevision } : {}),
422
425
  ...((ctx.forwardEvent ?? d.forwardEvent) !== undefined ? { onForwardEvent: (ctx.forwardEvent ?? d.forwardEvent) } : {}),
423
426
  ...((ctx.inheritedGateForChildren ?? d.inheritedGateForChildren) !== undefined
424
427
  ? { inheritedGate: (ctx.inheritedGateForChildren ?? d.inheritedGateForChildren)() }
@@ -13,7 +13,8 @@
13
13
  * cost/token caps — codex Q4).
14
14
  */
15
15
  import type { Model } from "../internal/llm.js";
16
- import type { ImageInput, TaskSpec, ThinkingLevel, WorkflowGovernanceBaseline } from "../core/types.js";
16
+ import type { EngineNotice, ImageInput, TaskSpec, ThinkingLevel, WorkflowGovernanceBaseline } from "../core/types.js";
17
+ import { type ReadDenyEntry } from "../tools/fs/read-deny.js";
17
18
  /** Thrown when an LLM-authored script picks a `modelName` not in the workflow model allowlist (or no
18
19
  * allowlist is configured). FAIL-CLOSED: a script can only ever name a model the deployment pre-approved. */
19
20
  export declare class WorkflowModelNotAllowedError extends Error {
@@ -41,12 +42,56 @@ export interface WorkflowAgentSpec {
41
42
  maxTokens?: number;
42
43
  maxCostUsd?: number;
43
44
  };
45
+ /**
46
+ * design/199 件A, TIGHTEN-ONLY: `"roots"` — the read-face containment judgment — is the ONLY value a
47
+ * script may set. `"open"` is the WIDENING direction (it removes the containment step), so it is
48
+ * stripped and announced rather than applied; the type states the asymmetry, and
49
+ * {@link pickWhitelist} enforces it on the untrusted value.
50
+ */
51
+ readFace?: "roots";
52
+ /**
53
+ * design/199 件B, TIGHTEN-ONLY: additional read-deny entries. Add-only at every layer (the built-in
54
+ * table and the baseline's entries are always in force and cannot be removed or replaced — see
55
+ * `compileReadDeny`'s zero-shrink contract and `tightenTaskSpec`'s union), so anything a script
56
+ * writes here can only ever narrow what the child may read.
57
+ */
58
+ readDenyPatterns?: readonly ReadDenyEntry[];
44
59
  }
45
60
  /**
46
61
  * The SINGLE source of truth for the whitelist (a test pins that it contains no control-plane key). `objective`
47
62
  * + `modelName` are handled explicitly in {@link buildGovernedChildSpec}; the rest map 1:1 onto `TaskSpec`.
63
+ *
64
+ * WHAT HAPPENS TO EVERY OTHER FIELD — {@link pickWhitelist} READS these keys and only these keys; it never
65
+ * enumerates the script's fields to judge them, which is precisely the complete-by-construction property this
66
+ * module is built on (nothing can leak by being forgotten in a denylist). The consequence is that an
67
+ * unrecognized field is STRIPPED, not refused: the spawn proceeds on the baseline as if the field had never
68
+ * been written. That silence is what {@link STRIPPED_KEYS_NOTICE_CODE} exists to close — one aggregated
69
+ * notice per governed build names the fields that did not cross the seam, so a mistyped budget axis (a
70
+ * top-level `maxCostUsd` that belongs inside `limits` since design/164, which would run the child unbounded
71
+ * on exactly the axis the author tried to bound) or a rejected containment request is visible to the operator
72
+ * instead of evaporating. Turning the strip into a LOUD REFUSAL that fails the spawn is a deliberate
73
+ * NON-GOAL of this window: it would break every existing script carrying a harmless extra field, and is held
74
+ * for a BREAKING window.
75
+ */
76
+ export declare const WHITELIST_KEYS: readonly ["objective", "modelName", "thinking", "systemPrompt", "images", "limits", "readFace", "readDenyPatterns"];
77
+ /**
78
+ * One field of an untrusted agent spec that did NOT reach the child, and why.
79
+ * - `not_whitelisted` — the field is outside {@link WHITELIST_KEYS}: a control-plane field, a work field
80
+ * spelled at the wrong nesting level, or plain noise. It was never read.
81
+ * - `not_a_tightening_value` — the KEY is whitelisted but only its containment-TIGHTENING value may cross
82
+ * the seam, and the script wrote a different one. Today that is `readFace`, where `"roots"` tightens and
83
+ * anything else (`"open"`, or garbage) would either widen the child's read face or ask for a value the
84
+ * resolver does not define.
48
85
  */
49
- export declare const WHITELIST_KEYS: readonly ["objective", "modelName", "thinking", "systemPrompt", "images", "limits"];
86
+ export interface StrippedSpecKeyNote {
87
+ key: string;
88
+ reason: "not_whitelisted" | "not_a_tightening_value";
89
+ }
90
+ /** The {@link EngineNotice} family for the strip announcement (see {@link WHITELIST_KEYS}). ONE notice per
91
+ * governed build, listing every field that did not cross the seam — a governed script can carry an
92
+ * arbitrary number of unrecognized fields, and a per-field notice would turn one authoring mistake into a
93
+ * flood. Not de-duplicated across builds: each spawn is a distinct fact about a distinct spec. */
94
+ export declare const STRIPPED_KEYS_NOTICE_CODE = "workflow.governance_key_stripped";
50
95
  /** Per-child workflow ceilings the engine forces onto every spawned agent (design/98 §D.6), independent of
51
96
  * what the script asks for. The child's effective limits = min(script, baseline, these). */
52
97
  export interface WorkflowChildCaps {
@@ -91,5 +136,10 @@ export declare function resolveModelName(name: string, allowlist: string[] | und
91
136
  * disclosure hook the caller can wire to a log/event channel (see `buildWorkflowPrimitives`, which logs it
92
137
  * onto the run's log stream so the LLM-authored script's caller can see requested→applied per field instead
93
138
  * of the child silently running under different limits than the script wrote).
139
+ *
140
+ * `onNotice` is the deployment's structured notice sink (`RunnerDeps.onNotice`): step 1's strip is announced
141
+ * through it as one aggregated {@link STRIPPED_KEYS_NOTICE_CODE} notice (see {@link WHITELIST_KEYS}). An
142
+ * absent sink keeps the historic loudness (`console.warn`); the delivery itself is swallow-guarded, so no
143
+ * sink can turn an announcement into a failed spawn.
94
144
  */
95
- export declare function buildGovernedChildSpec(scriptSpec: unknown, baseline: WorkflowGovernanceBaseline, models: Record<string, Model> | undefined, caps?: WorkflowChildCaps, onResourceClamp?: (notes: ResourceClampNote[]) => void): TaskSpec;
145
+ export declare function buildGovernedChildSpec(scriptSpec: unknown, baseline: WorkflowGovernanceBaseline, models: Record<string, Model> | undefined, caps?: WorkflowChildCaps, onResourceClamp?: (notes: ResourceClampNote[]) => void, onNotice?: (n: EngineNotice) => void): TaskSpec;
@@ -1,5 +1,7 @@
1
+ import { deliverEngineNotice } from "../core/types.js";
1
2
  import { tightenTaskSpec } from "../core/tighten-task-spec.js";
2
3
  import { sanitizeUntrustedText } from "../core/untrusted-text.js";
4
+ import { compileReadDeny } from "../tools/fs/read-deny.js";
3
5
  import { WorkflowScriptError } from "./workflow-meta.js";
4
6
  export class WorkflowModelNotAllowedError extends Error {
5
7
  modelName;
@@ -17,7 +19,43 @@ export const WHITELIST_KEYS = [
17
19
  "systemPrompt",
18
20
  "images",
19
21
  "limits",
22
+ "readFace",
23
+ "readDenyPatterns",
20
24
  ];
25
+ const WHITELIST_KEY_SET = new Set(WHITELIST_KEYS);
26
+ export const STRIPPED_KEYS_NOTICE_CODE = "workflow.governance_key_stripped";
27
+ const MAX_STRIPPED_KEYS_ANNOUNCED = 20;
28
+ const MAX_STRIPPED_KEY_CHARS = 64;
29
+ const UNSAFE_KEY_CODE_POINT = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u;
30
+ function renderStrippedKey(key) {
31
+ let out = "";
32
+ let seen = 0;
33
+ for (const point of key) {
34
+ if (seen === MAX_STRIPPED_KEY_CHARS)
35
+ return `${out}…`;
36
+ out += UNSAFE_KEY_CODE_POINT.test(point) ? "?" : point;
37
+ seen++;
38
+ }
39
+ return out;
40
+ }
41
+ function emitStrippedKeysNotice(survey, onNotice) {
42
+ const shown = survey.sample.map((s) => ({ key: renderStrippedKey(s.key), reason: s.reason }));
43
+ const omitted = survey.total - shown.length;
44
+ const list = shown.map((s) => JSON.stringify(s.key)).join(", ") + (omitted > 0 ? `, +${omitted} more` : "");
45
+ const message = `workflow governance: ${survey.total} field(s) of an agent spec were NOT applied to the child (${list}). ` +
46
+ `A governed workflow script may set only: ${WHITELIST_KEYS.join(", ")} — and \`readFace\` only as "roots" ` +
47
+ `(the containment-tightening direction; "open" widens and is never taken from a script). Every other field ` +
48
+ `of the child comes from the deployment baseline.`;
49
+ try {
50
+ deliverEngineNotice(onNotice, {
51
+ code: STRIPPED_KEYS_NOTICE_CODE,
52
+ message,
53
+ detail: { total: survey.total, stripped: shown, ...(omitted > 0 ? { omitted } : {}) },
54
+ });
55
+ }
56
+ catch {
57
+ }
58
+ }
21
59
  const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
22
60
  export function resolveModelName(name, allowlist, models) {
23
61
  if (!allowlist || allowlist.length === 0) {
@@ -49,49 +87,121 @@ function pickImageInput(el, i) {
49
87
  }
50
88
  const o = el;
51
89
  if ("url" in o) {
52
- if (typeof o.url !== "string" || o.url.length === 0) {
90
+ const url = o.url;
91
+ if (typeof url !== "string" || url.length === 0) {
53
92
  throw new WorkflowScriptError(`${at}.url must be a non-empty string`);
54
93
  }
55
- return { url: o.url };
94
+ return { url };
56
95
  }
57
- if (typeof o.data !== "string") {
96
+ const data = o.data;
97
+ const mimeType = o.mimeType;
98
+ if (typeof data !== "string") {
58
99
  throw new WorkflowScriptError(`${at}.data must be a base64 string (or supply { url } instead)`);
59
100
  }
60
- if (typeof o.mimeType !== "string" || o.mimeType.length === 0) {
101
+ if (typeof mimeType !== "string" || mimeType.length === 0) {
61
102
  throw new WorkflowScriptError(`${at}.mimeType must be a non-empty string, e.g. "image/png"`);
62
103
  }
63
- return { data: o.data, mimeType: o.mimeType };
104
+ return { data, mimeType };
105
+ }
106
+ const MAX_SCRIPT_DENY_ENTRIES = 16;
107
+ const MAX_SCRIPT_DENY_PATTERN_CHARS = 256;
108
+ const MAX_SCRIPT_DENY_WILDCARDS_PER_SEGMENT = 1;
109
+ const MAX_SCRIPT_IMAGES = 32;
110
+ function assertScriptDenyPatternBounded(pattern, at) {
111
+ if (pattern.length > MAX_SCRIPT_DENY_PATTERN_CHARS) {
112
+ throw new WorkflowScriptError(`${at} is ${pattern.length} characters — a workflow script's deny pattern is limited to ${MAX_SCRIPT_DENY_PATTERN_CHARS} (the matcher runs on every read of the child's run).`);
113
+ }
114
+ for (const segment of pattern.split("/")) {
115
+ const wildcards = segment.split("*").length - 1;
116
+ if (wildcards > MAX_SCRIPT_DENY_WILDCARDS_PER_SEGMENT) {
117
+ throw new WorkflowScriptError(`${at} has a path segment with ${wildcards} \`*\` wildcards — a workflow script's deny pattern allows at most ` +
118
+ `${MAX_SCRIPT_DENY_WILDCARDS_PER_SEGMENT} per path segment (the same expressiveness the engine's built-in deny table ` +
119
+ `uses). Each \`*\` compiles to a greedy match, and more than one in a single segment makes the judgment cost grow ` +
120
+ `superlinearly with the path length, on every read the child performs. Write the intent as separate entries ` +
121
+ `(the set is a union, so more entries deny strictly more) or anchor it with literal segments.`);
122
+ }
123
+ }
124
+ }
125
+ function mapUntrustedArray(value, at, atMost, why, build) {
126
+ const declared = value.length;
127
+ if (typeof declared !== "number" || !Number.isSafeInteger(declared) || declared < 0) {
128
+ throw new WorkflowScriptError(`${at} does not report a valid array length.`);
129
+ }
130
+ if (declared > atMost) {
131
+ throw new WorkflowScriptError(`${at} carries ${declared} entries — a workflow script may set at most ${atMost}. ${why}`);
132
+ }
133
+ const out = [];
134
+ for (let i = 0; i < declared; i++)
135
+ out.push(build(value[i], i));
136
+ return out;
137
+ }
138
+ function pickReadDenyEntry(el, i) {
139
+ const at = `agent(spec): \`readDenyPatterns[${i}]\``;
140
+ if (typeof el === "string") {
141
+ if (el.length === 0)
142
+ throw new WorkflowScriptError(`${at} must be a non-empty pattern string`);
143
+ assertScriptDenyPatternBounded(el, at);
144
+ return el;
145
+ }
146
+ if (typeof el !== "object" || el === null || Array.isArray(el)) {
147
+ throw new WorkflowScriptError(`${at} must be a pattern string or an object { pattern, caseSensitive? }`);
148
+ }
149
+ const o = el;
150
+ const pattern = o.pattern;
151
+ const caseSensitive = o.caseSensitive;
152
+ if (typeof pattern !== "string" || pattern.length === 0) {
153
+ throw new WorkflowScriptError(`${at}.pattern must be a non-empty string, e.g. ".ssh" or "secrets/*.json"`);
154
+ }
155
+ if (caseSensitive !== undefined && typeof caseSensitive !== "boolean") {
156
+ throw new WorkflowScriptError(`${at}.caseSensitive must be a boolean`);
157
+ }
158
+ assertScriptDenyPatternBounded(pattern, at);
159
+ return caseSensitive === undefined ? { pattern } : { pattern, caseSensitive };
64
160
  }
65
161
  function pickWhitelist(scriptSpec) {
66
162
  if (typeof scriptSpec !== "object" || scriptSpec === null || Array.isArray(scriptSpec)) {
67
163
  throw new WorkflowScriptError("agent(spec): spec must be an object with at least an `objective` string");
68
164
  }
69
165
  const s = scriptSpec;
70
- if (typeof s.objective !== "string" || s.objective.length === 0) {
166
+ const stripped = { total: 0, sample: [] };
167
+ for (const k of Object.keys(s)) {
168
+ if (WHITELIST_KEY_SET.has(k))
169
+ continue;
170
+ stripped.total++;
171
+ if (stripped.sample.length < MAX_STRIPPED_KEYS_ANNOUNCED)
172
+ stripped.sample.push({ key: k, reason: "not_whitelisted" });
173
+ }
174
+ const objective = s.objective;
175
+ const thinking = s.thinking;
176
+ const systemPrompt = s.systemPrompt;
177
+ const modelNameRaw = s.modelName;
178
+ if (typeof objective !== "string" || objective.length === 0) {
71
179
  throw new WorkflowScriptError("agent(spec): `objective` is required and must be a non-empty string");
72
180
  }
73
- const safe = { objective: s.objective };
74
- if (s.thinking !== undefined) {
75
- if (typeof s.thinking !== "string" || !VALID_THINKING.has(s.thinking)) {
181
+ const safe = { objective };
182
+ if (thinking !== undefined) {
183
+ if (typeof thinking !== "string" || !VALID_THINKING.has(thinking)) {
76
184
  throw new WorkflowScriptError(`agent(spec): invalid \`thinking\` (must be one of ${[...VALID_THINKING].join(", ")})`);
77
185
  }
78
- safe.thinking = s.thinking;
186
+ safe.thinking = thinking;
79
187
  }
80
- if (s.systemPrompt !== undefined) {
81
- if (typeof s.systemPrompt !== "string")
188
+ if (systemPrompt !== undefined) {
189
+ if (typeof systemPrompt !== "string")
82
190
  throw new WorkflowScriptError("agent(spec): `systemPrompt` must be a string");
83
- safe.systemPrompt = `[workflow-script-authored persona — task guidance, not engine authority]\n${sanitizeUntrustedText(s.systemPrompt)}`;
191
+ safe.systemPrompt = `[workflow-script-authored persona — task guidance, not engine authority]\n${sanitizeUntrustedText(systemPrompt)}`;
84
192
  }
85
- if (s.images !== undefined) {
86
- if (!Array.isArray(s.images))
193
+ const images = s.images;
194
+ if (images !== undefined) {
195
+ if (!Array.isArray(images))
87
196
  throw new WorkflowScriptError("agent(spec): `images` must be an array");
88
- safe.images = s.images.map(pickImageInput);
197
+ safe.images = mapUntrustedArray(images, "agent(spec): `images`", MAX_SCRIPT_IMAGES, "Each one is decoded and shipped on every request of the child's turn.", pickImageInput);
89
198
  }
90
- if (s.limits !== undefined) {
91
- if (typeof s.limits !== "object" || s.limits === null || Array.isArray(s.limits)) {
199
+ const limitsRaw = s.limits;
200
+ if (limitsRaw !== undefined) {
201
+ if (typeof limitsRaw !== "object" || limitsRaw === null || Array.isArray(limitsRaw)) {
92
202
  throw new WorkflowScriptError("agent(spec): `limits` must be an object { maxTurns?, maxWalltimeMs?, maxTokens?, maxCostUsd? }");
93
203
  }
94
- const l = s.limits;
204
+ const l = limitsRaw;
95
205
  const limits = {};
96
206
  const readAxis = (field, what) => {
97
207
  const raw = l[field];
@@ -109,13 +219,38 @@ function pickWhitelist(scriptSpec) {
109
219
  readAxis("maxCostUsd", "spend ceiling");
110
220
  safe.limits = limits;
111
221
  }
222
+ if (s.readFace !== undefined) {
223
+ if (s.readFace === "roots") {
224
+ safe.readFace = "roots";
225
+ }
226
+ else {
227
+ stripped.total++;
228
+ stripped.sample.unshift({ key: "readFace", reason: "not_a_tightening_value" });
229
+ if (stripped.sample.length > MAX_STRIPPED_KEYS_ANNOUNCED)
230
+ stripped.sample.pop();
231
+ }
232
+ }
233
+ const readDenyPatternsRaw = s.readDenyPatterns;
234
+ if (readDenyPatternsRaw !== undefined) {
235
+ if (!Array.isArray(readDenyPatternsRaw)) {
236
+ throw new WorkflowScriptError('agent(spec): `readDenyPatterns` must be an array of deny entries (a "/"-separated segment run such as ".ssh", or { pattern, caseSensitive? })');
237
+ }
238
+ const entries = mapUntrustedArray(readDenyPatternsRaw, "agent(spec): `readDenyPatterns`", MAX_SCRIPT_DENY_ENTRIES, "Every entry is judged against every path the child reads, for the child's whole run; the built-in and deployment entries are always in force on top of these.", pickReadDenyEntry);
239
+ try {
240
+ compileReadDeny(entries, "agent(spec).readDenyPatterns");
241
+ }
242
+ catch (err) {
243
+ throw new WorkflowScriptError(err instanceof Error ? err.message : String(err));
244
+ }
245
+ safe.readDenyPatterns = entries;
246
+ }
112
247
  let modelName;
113
- if (s.modelName !== undefined) {
114
- if (typeof s.modelName !== "string")
248
+ if (modelNameRaw !== undefined) {
249
+ if (typeof modelNameRaw !== "string")
115
250
  throw new WorkflowScriptError("agent(spec): `modelName` must be a string (a model NAME, never a Model object)");
116
- modelName = s.modelName;
251
+ modelName = modelNameRaw;
117
252
  }
118
- return { safe, modelName };
253
+ return { safe, modelName, stripped };
119
254
  }
120
255
  function clampResourceLimits(safe, base, caps) {
121
256
  const trustedCandidates = [
@@ -172,8 +307,10 @@ function clampResourceLimits(safe, base, caps) {
172
307
  safe.limits = rebuilt;
173
308
  return notes;
174
309
  }
175
- export function buildGovernedChildSpec(scriptSpec, baseline, models, caps, onResourceClamp) {
176
- const { safe, modelName } = pickWhitelist(scriptSpec);
310
+ export function buildGovernedChildSpec(scriptSpec, baseline, models, caps, onResourceClamp, onNotice) {
311
+ const { safe, modelName, stripped } = pickWhitelist(scriptSpec);
312
+ if (stripped.total > 0)
313
+ emitStrippedKeysNotice(stripped, onNotice);
177
314
  if (modelName !== undefined) {
178
315
  safe.model = resolveModelName(modelName, baseline.workflowModelAllowlist, models);
179
316
  }
@@ -22,6 +22,11 @@ export interface WorkflowGovernance {
22
22
  baseline: WorkflowGovernanceBaseline;
23
23
  models?: Record<string, Model>;
24
24
  caps?: WorkflowChildCaps;
25
+ /** The deployment's structured notice sink (`RunnerDeps.onNotice`), threaded here because the governed
26
+ * build is the only place that can report which spec fields did NOT reach the child (see
27
+ * `buildGovernedChildSpec`'s `onNotice` param). Only meaningful in governed mode — the trusted-dev lane
28
+ * strips nothing. Absent ⇒ the notice falls back to `console.warn`. */
29
+ onNotice?: (n: import("../core/types.js").EngineNotice) => void;
25
30
  }
26
31
  /**
27
32
  * Build the flat {@link WorkflowPrimitives} a {@link WorkflowScriptRunner} runs the script against. The
@@ -30,4 +35,13 @@ export interface WorkflowGovernance {
30
35
  export declare function buildWorkflowPrimitives(ctx: WorkflowRunContext, governance?: WorkflowGovernance, onAgentSpawn?: (handle: WorkflowAgentHandle) => void, parentThinking?: () => TaskSpec["thinking"], parentPrincipal?: string,
31
36
  /** ruled 2026-08-04 — the host run set `TaskSpec.checkpointStore: null` (the per-run durable off
32
37
  * switch). Every agent this workflow spawns inherits it; see the injection below. */
33
- parentCheckpointStoreDisabled?: boolean): WorkflowPrimitives;
38
+ parentCheckpointStoreDisabled?: boolean,
39
+ /** 5.30 merge-rescan (design/199 parity gap) — the host run's RESOLVED read-face containment
40
+ * (post deps/org/mount resolution, not the raw spec value). `TaskSpec.readFace`'s own JSDoc
41
+ * promises "roots pins this task, and via the tighten-only clamp, its whole delegation subtree" —
42
+ * a workflow child is that subtree; the Task/SendMessage delegation lane already carries this,
43
+ * the workflow lane was the one gap. Lazy (read at spawn time, after prepare has resolved it). */
44
+ parentReadFace?: () => TaskSpec["readFace"],
45
+ /** Twin of the above for the deny-set additions (built-ins always apply; these are the extra
46
+ * entries the host's own deployment/task layers stacked on). */
47
+ parentReadDenyPatterns?: () => TaskSpec["readDenyPatterns"]): WorkflowPrimitives;