@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.
- package/CHANGELOG.md +141 -0
- package/dist/agents/send-message-tool.js +2 -0
- package/dist/agents/subagent.d.ts +2 -0
- package/dist/agents/subagent.js +6 -0
- package/dist/agents/teacher.js +2 -0
- package/dist/agents/verify.js +2 -0
- package/dist/core/auto-compaction.d.ts +5 -1
- package/dist/core/auto-compaction.js +10 -1
- package/dist/core/checkpoint-store.d.ts +51 -5
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/hooks.d.ts +12 -1
- package/dist/core/hooks.js +8 -2
- package/dist/core/permission-rules.js +2 -2
- package/dist/core/runner/prepare-task.d.ts +21 -5
- package/dist/core/runner/prepare-task.js +115 -26
- package/dist/core/runner/runtask.js +29 -3
- package/dist/core/runner/session-rule-policy.d.ts +3 -2
- package/dist/core/runner/tool-output-projection.js +1 -1
- package/dist/core/sensitive-path-policy.js +5 -16
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/tool-result-store-contract.js +26 -1
- package/dist/core/tighten-task-spec.js +18 -0
- package/dist/core/tool-policy.d.ts +20 -1
- package/dist/core/tool-policy.js +31 -4
- package/dist/core/tool-result-store.d.ts +6 -4
- package/dist/core/tool-result-store.js +3 -1
- package/dist/core/types.d.ts +63 -1
- package/dist/engine/harness/types.d.ts +10 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/orchestration/run-workflow-tool.d.ts +26 -0
- package/dist/orchestration/run-workflow-tool.js +7 -4
- package/dist/orchestration/workflow-governance.d.ts +53 -3
- package/dist/orchestration/workflow-governance.js +162 -25
- package/dist/orchestration/workflow-primitives.d.ts +15 -1
- package/dist/orchestration/workflow-primitives.js +13 -2
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/types.d.ts +4 -0
- package/dist/prompts/default.d.ts +14 -9
- package/dist/prompts/default.js +13 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +53 -4
- package/dist/tools/fs/bash-readonly-classifier.js +148 -16
- package/dist/tools/fs/fs-bash.d.ts +7 -0
- package/dist/tools/fs/fs-bash.js +8 -3
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.js +2 -2
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +11 -7
- package/dist/tools/fs/fs-search-tools.d.ts +4 -2
- package/dist/tools/fs/fs-search-tools.js +15 -8
- package/dist/tools/fs/fs-shared.d.ts +5 -1
- package/dist/tools/fs/fs-shared.js +8 -3
- package/dist/tools/fs/index.d.ts +18 -0
- package/dist/tools/fs/index.js +13 -2
- package/dist/tools/fs/read-deny.d.ts +110 -0
- package/dist/tools/fs/read-deny.js +159 -0
- package/dist/tools/fs/read-face.d.ts +49 -0
- package/dist/tools/fs/read-face.js +38 -0
- package/dist/tools/fs/repo-map.d.ts +3 -1
- package/dist/tools/fs/repo-map.js +11 -5
- package/dist/tools/fs/safety.d.ts +34 -11
- package/dist/tools/fs/safety.js +108 -8
- package/dist/tools/fs/search.d.ts +54 -5
- package/dist/tools/fs/search.js +107 -23
- 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
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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) =>
|
|
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)
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
-
...(
|
|
421
|
-
...(
|
|
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
|
|
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
|
-
|
|
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
|
|
94
|
+
return { url };
|
|
56
95
|
}
|
|
57
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
74
|
-
if (
|
|
75
|
-
if (typeof
|
|
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 =
|
|
186
|
+
safe.thinking = thinking;
|
|
79
187
|
}
|
|
80
|
-
if (
|
|
81
|
-
if (typeof
|
|
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(
|
|
191
|
+
safe.systemPrompt = `[workflow-script-authored persona — task guidance, not engine authority]\n${sanitizeUntrustedText(systemPrompt)}`;
|
|
84
192
|
}
|
|
85
|
-
|
|
86
|
-
|
|
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.
|
|
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
|
-
|
|
91
|
-
|
|
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 =
|
|
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 (
|
|
114
|
-
if (typeof
|
|
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 =
|
|
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
|
|
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;
|