@sema-agent/core 2.13.0 → 4.0.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 (184) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +6 -1
  3. package/dist/agents/cascade.js +76 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.d.ts +2 -0
  9. package/dist/agents/repair-loop.js +110 -3
  10. package/dist/agents/retain-ledger.d.ts +20 -2
  11. package/dist/agents/retain-ledger.js +38 -8
  12. package/dist/agents/roster-store.d.ts +4 -0
  13. package/dist/agents/roster-store.js +20 -1
  14. package/dist/agents/subagent.js +66 -64
  15. package/dist/agents/teacher.d.ts +1 -9
  16. package/dist/agents/teacher.js +23 -24
  17. package/dist/agents/team.js +7 -4
  18. package/dist/agents/verify.d.ts +15 -2
  19. package/dist/agents/verify.js +39 -3
  20. package/dist/bin/sema-tb.js +2 -2
  21. package/dist/brain/anthropic.js +24 -21
  22. package/dist/brain/circuit-breaker.d.ts +1 -0
  23. package/dist/brain/circuit-breaker.js +26 -9
  24. package/dist/brain/degrading.js +13 -1
  25. package/dist/brain/failover.js +2 -0
  26. package/dist/brain/openai.js +24 -21
  27. package/dist/brain/repetition.js +10 -1
  28. package/dist/brain/routing.js +5 -1
  29. package/dist/brain/status-sink.js +5 -1
  30. package/dist/brain/stream-engine.js +5 -2
  31. package/dist/brain/stream-shared.d.ts +1 -0
  32. package/dist/brain/stream-shared.js +14 -0
  33. package/dist/brain/terminal-cause.d.ts +4 -0
  34. package/dist/brain/terminal-cause.js +12 -0
  35. package/dist/brain/tool-call-id.d.ts +1 -0
  36. package/dist/brain/tool-call-id.js +3 -0
  37. package/dist/brain/tool-call-repair.js +34 -15
  38. package/dist/core/auto-compaction.js +33 -32
  39. package/dist/core/background-agent-store.d.ts +22 -22
  40. package/dist/core/background-agent-store.js +19 -23
  41. package/dist/core/checkpoint-store.d.ts +7 -13
  42. package/dist/core/checkpoint-store.js +39 -15
  43. package/dist/core/consolidate-scope.js +4 -3
  44. package/dist/core/context-edit.js +3 -0
  45. package/dist/core/context-guard.js +3 -0
  46. package/dist/core/file-snapshot-store.d.ts +8 -0
  47. package/dist/core/file-snapshot-store.js +4 -3
  48. package/dist/core/git-worktree-env.d.ts +6 -1
  49. package/dist/core/git-worktree-env.js +12 -1
  50. package/dist/core/hooks.d.ts +4 -1
  51. package/dist/core/hooks.js +35 -7
  52. package/dist/core/lsp-diagnostics.js +13 -7
  53. package/dist/core/lsp-protocol.d.ts +1 -1
  54. package/dist/core/lsp-protocol.js +20 -8
  55. package/dist/core/lsp-session.d.ts +12 -2
  56. package/dist/core/lsp-session.js +114 -49
  57. package/dist/core/lsp.d.ts +13 -1
  58. package/dist/core/lsp.js +65 -9
  59. package/dist/core/mailbox-store.d.ts +7 -10
  60. package/dist/core/mcp.d.ts +7 -10
  61. package/dist/core/mcp.js +7 -3
  62. package/dist/core/memory-engine/data-plane.js +4 -0
  63. package/dist/core/memory-engine/engine.d.ts +2 -2
  64. package/dist/core/memory-engine/engine.js +96 -31
  65. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  66. package/dist/core/memory-engine/file-backend.js +38 -39
  67. package/dist/core/memory-engine/layout.d.ts +8 -0
  68. package/dist/core/memory-engine/layout.js +71 -4
  69. package/dist/core/memory-engine/scope-contract.js +9 -3
  70. package/dist/core/memory-engine/types.d.ts +1 -1
  71. package/dist/core/memory.js +3 -0
  72. package/dist/core/permission-rules.d.ts +2 -1
  73. package/dist/core/permission-rules.js +24 -3
  74. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  75. package/dist/core/runner/active-skill-scope.js +1 -15
  76. package/dist/core/runner/assemble-result.d.ts +3 -8
  77. package/dist/core/runner/assemble-result.js +3 -5
  78. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  79. package/dist/core/runner/compaction-call-options.js +30 -0
  80. package/dist/core/runner/prepare-task.d.ts +6 -23
  81. package/dist/core/runner/prepare-task.js +31 -53
  82. package/dist/core/runner/runtask.d.ts +1 -0
  83. package/dist/core/runner/runtask.js +86 -178
  84. package/dist/core/runner/session-rule-policy.js +4 -1
  85. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  86. package/dist/core/runner/teardown-bounded.js +36 -0
  87. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  88. package/dist/core/runner/turn-attachments.d.ts +5 -15
  89. package/dist/core/runner/turn-attachments.js +1 -1
  90. package/dist/core/runner/usage-accounting.d.ts +6 -0
  91. package/dist/core/runner/usage-accounting.js +9 -8
  92. package/dist/core/safe-notify.d.ts +17 -0
  93. package/dist/core/safe-notify.js +59 -0
  94. package/dist/core/secret-env.js +5 -1
  95. package/dist/core/sensitive-path-policy.js +11 -4
  96. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  97. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  98. package/dist/core/stub-env.d.ts +2 -6
  99. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  100. package/dist/core/surrogate-safe-slice.js +18 -0
  101. package/dist/core/task-registry-monitor.js +15 -14
  102. package/dist/core/task-registry-shared.d.ts +22 -0
  103. package/dist/core/task-registry-shared.js +16 -0
  104. package/dist/core/task-registry.d.ts +1 -0
  105. package/dist/core/task-registry.js +40 -57
  106. package/dist/core/tool-errors.js +5 -4
  107. package/dist/core/tool-name-aliases.js +2 -1
  108. package/dist/core/tool-policy.js +72 -19
  109. package/dist/core/tool-result-store.d.ts +1 -0
  110. package/dist/core/tool-result-store.js +17 -1
  111. package/dist/core/trace.d.ts +3 -9
  112. package/dist/core/types.d.ts +16 -12
  113. package/dist/core/untrusted-egress.js +7 -5
  114. package/dist/core/workflow-journal-store.d.ts +9 -20
  115. package/dist/core/workflow-run-store.d.ts +11 -10
  116. package/dist/core/workflow-run-store.js +22 -16
  117. package/dist/engine/compaction/compaction.d.ts +1 -0
  118. package/dist/engine/compaction/compaction.js +14 -5
  119. package/dist/engine/compaction/utils.js +4 -2
  120. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  121. package/dist/engine/harness/agent-harness.d.ts +1 -0
  122. package/dist/engine/harness/agent-harness.js +13 -33
  123. package/dist/engine/harness/types.d.ts +29 -67
  124. package/dist/engine/llm/types.d.ts +1 -1
  125. package/dist/engine/loop/agent-loop.js +11 -4
  126. package/dist/engine/loop/types.d.ts +11 -9
  127. package/dist/engine/lsp/frame-decoder.js +6 -3
  128. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  129. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  130. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  131. package/dist/engine/session/memory-repo.d.ts +2 -6
  132. package/dist/engine/session/session.d.ts +5 -30
  133. package/dist/index.d.ts +3 -1
  134. package/dist/index.js +2 -1
  135. package/dist/internal/harness-types.d.ts +3 -1
  136. package/dist/internal/harness.d.ts +1 -1
  137. package/dist/internal/harness.js +1 -1
  138. package/dist/orchestration/run-spec.js +7 -4
  139. package/dist/orchestration/run-workflow-tool.js +3 -35
  140. package/dist/orchestration/workflow-governance.js +43 -9
  141. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  142. package/dist/orchestration/workflow-script-store.js +34 -4
  143. package/dist/orchestration/workflow.d.ts +3 -11
  144. package/dist/orchestration/workflow.js +164 -204
  145. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  146. package/dist/prompt-assembly/artifact-store.js +39 -24
  147. package/dist/prompt-assembly/assemble.js +38 -7
  148. package/dist/prompt-assembly/epoch.js +19 -2
  149. package/dist/prompt-assembly/event-registry.js +3 -3
  150. package/dist/prompt-assembly/explain.js +1 -1
  151. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  152. package/dist/prompt-assembly/tool-catalog.js +16 -1
  153. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  154. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  155. package/dist/stores/file/background-agent-store.d.ts +3 -12
  156. package/dist/stores/file/background-agent-store.js +3 -41
  157. package/dist/stores/file/checkpoint-store.js +6 -6
  158. package/dist/stores/file/fs-atomic.d.ts +3 -0
  159. package/dist/stores/file/fs-atomic.js +53 -8
  160. package/dist/stores/file/index.d.ts +1 -0
  161. package/dist/stores/file/index.js +7 -0
  162. package/dist/stores/file/mailbox-store.d.ts +2 -6
  163. package/dist/stores/file/session-policy-store.js +15 -1
  164. package/dist/stores/file/session-store.d.ts +4 -6
  165. package/dist/stores/file/session-store.js +28 -1
  166. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  167. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  168. package/dist/stores/file/workflow-run-store.js +4 -17
  169. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -0
  170. package/dist/tools/fs/bash-readonly-classifier.js +73 -21
  171. package/dist/tools/fs/fs-bash.d.ts +1 -1
  172. package/dist/tools/fs/fs-bash.js +407 -91
  173. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  174. package/dist/tools/fs/fs-shared.js +2 -2
  175. package/dist/tools/fs/notebook.d.ts +2 -5
  176. package/dist/tools/fs/safety.d.ts +3 -0
  177. package/dist/tools/fs/safety.js +43 -14
  178. package/dist/tools/fs/search.js +5 -5
  179. package/dist/tools/loop-tick.js +1 -1
  180. package/dist/tools/monitor.d.ts +1 -0
  181. package/dist/tools/monitor.js +57 -9
  182. package/dist/tools/worktree.d.ts +4 -7
  183. package/dist/tools/worktree.js +235 -143
  184. package/package.json +9 -2
@@ -16,7 +16,17 @@ export class StdioLspTransport {
16
16
  for (const frame of this.decode(chunk))
17
17
  this.onMessage(frame);
18
18
  });
19
- child.stdin.on("error", () => { });
19
+ child.stdin.on("error", (e) => this.failAll(toError(e)));
20
+ const stdout = child.stdout;
21
+ if (typeof stdout.removeListener === "function" && typeof stdout.once === "function") {
22
+ for (const event of ["end", "close", "error"]) {
23
+ stdout.on(event, (arg) => {
24
+ if (Buffer.isBuffer(arg))
25
+ return;
26
+ this.failAll(new Error(`LSP server stdout ${event}`));
27
+ });
28
+ }
29
+ }
20
30
  child.on("exit", () => this.failAll(new Error("LSP server exited")));
21
31
  child.on("error", (e) => this.failAll(toError(e)));
22
32
  }
@@ -36,7 +46,8 @@ export class StdioLspTransport {
36
46
  try {
37
47
  this.send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `Method not found: ${String(msg.method)}` } });
38
48
  }
39
- catch {
49
+ catch (e) {
50
+ this.failAll(toError(e));
40
51
  }
41
52
  }
42
53
  return;
@@ -121,10 +132,7 @@ export class StdioLspTransport {
121
132
  this.send({ jsonrpc: "2.0", id, method, params });
122
133
  }
123
134
  catch (e) {
124
- if (this.pending.delete(id)) {
125
- cleanup();
126
- reject(brandLspFailure(e instanceof Error ? e : new Error(String(e)), "server_terminated"));
127
- }
135
+ this.failAll(brandLspFailure(e instanceof Error ? e : new Error(String(e)), "server_terminated"));
128
136
  }
129
137
  });
130
138
  }
@@ -134,7 +142,8 @@ export class StdioLspTransport {
134
142
  try {
135
143
  this.send({ jsonrpc: "2.0", method, params });
136
144
  }
137
- catch {
145
+ catch (e) {
146
+ this.failAll(toError(e));
138
147
  }
139
148
  }
140
149
  async close() {
@@ -1,4 +1,4 @@
1
- import { type Session, type SessionMetadata, type SessionRepo, type SessionTreeEntry } from "../harness/types.js";
1
+ import { type Session, type SessionForkOptions, type SessionMetadata, type SessionRepo, type SessionTreeEntry } from "../harness/types.js";
2
2
  export declare class InMemorySessionRepo implements SessionRepo<SessionMetadata, {
3
3
  id?: string;
4
4
  }> {
@@ -9,11 +9,7 @@ export declare class InMemorySessionRepo implements SessionRepo<SessionMetadata,
9
9
  open(metadata: SessionMetadata): Promise<Session>;
10
10
  list(): Promise<SessionMetadata[]>;
11
11
  delete(metadata: SessionMetadata): Promise<void>;
12
- fork(sourceMetadata: SessionMetadata, options: {
13
- entryId?: string;
14
- position?: "before" | "at";
15
- id?: string;
16
- }): Promise<Session>;
12
+ fork(sourceMetadata: SessionMetadata, options: SessionForkOptions): Promise<Session>;
17
13
  exportEntries(sessionId: string): Promise<SessionTreeEntry[]>;
18
14
  importEntries(sessionId: string, owner: string | undefined, entries: SessionTreeEntry[]): Promise<void>;
19
15
  }
@@ -1,7 +1,8 @@
1
1
  import type { ImageContent, TextContent } from "../llm/index.js";
2
2
  import type { AgentMessage } from "../loop/types.js";
3
- import type { Session, SessionContext, SessionMetadata, SessionStorage, SessionTreeEntry } from "../harness/types.js";
3
+ import type { Session, SessionContext, SessionMetadata, SessionStorage, SessionTreeEntry, WorkspaceState } from "../harness/types.js";
4
4
  import type { BuildContextOptions } from "../harness/types.js";
5
+ import type { PromptEpochDescriptor } from "../../prompt-assembly/epoch.js";
5
6
  export declare function buildSessionContext(pathEntries: SessionTreeEntry[], opts?: BuildContextOptions): SessionContext;
6
7
  export declare class StoredSession<TMetadata extends SessionMetadata = SessionMetadata> implements Session<TMetadata> {
7
8
  private storage;
@@ -19,40 +20,14 @@ export declare class StoredSession<TMetadata extends SessionMetadata = SessionMe
19
20
  appendMessage(message: AgentMessage): Promise<string>;
20
21
  appendThinkingLevelChange(thinkingLevel: string): Promise<string>;
21
22
  appendModelChange(provider: string, modelId: string): Promise<string>;
22
- appendPromptEpoch(descriptor: {
23
- epoch: number;
24
- artifactDigest: string;
25
- packId: string;
26
- assemblyApi: number;
27
- activatedBy: "session_start" | "compaction" | "legacy_migration";
28
- centerArtifactDigest?: string;
29
- sourceRevision?: string;
30
- }): Promise<string>;
23
+ appendPromptEpoch(descriptor: PromptEpochDescriptor): Promise<string>;
31
24
  appendAnnouncedListing(snapshot: {
32
25
  agents?: readonly string[];
33
26
  skills?: readonly string[];
34
27
  models?: readonly string[];
35
28
  }): Promise<string>;
36
- appendWorkspaceState(state: {
37
- taskRoot: string;
38
- handsCwd?: string;
39
- activeWorktree?: {
40
- worktreeDir: string;
41
- originalCwd: string;
42
- baseSha: string;
43
- entered?: boolean;
44
- };
45
- }): Promise<string>;
46
- getWorkspaceState(): Promise<{
47
- taskRoot: string;
48
- handsCwd?: string;
49
- activeWorktree?: {
50
- worktreeDir: string;
51
- originalCwd: string;
52
- baseSha: string;
53
- entered?: boolean;
54
- };
55
- } | undefined>;
29
+ appendWorkspaceState(state: WorkspaceState): Promise<string>;
30
+ getWorkspaceState(): Promise<WorkspaceState | undefined>;
56
31
  getAnnouncedListing(): Promise<{
57
32
  agents?: string[];
58
33
  skills?: string[];
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export { assembleCodeTools, type CodeToolsConfig, CODE_ROLE } from "./scenarios/
14
14
  export { capAggregateToolResults, AGGREGATE_TOOL_RESULT_BUDGET_CHARS, DEFAULT_BUDGET_EXEMPT_TOOLS, type AggregateBudgetOptions, } from "./core/tool-result-budget.js";
15
15
  export { capAggregateMediaBytes, AGGREGATE_MEDIA_BUDGET_BYTES, type MediaStripInfo } from "./core/media-byte-cap.js";
16
16
  export { createAskUserQuestionTool, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, type OnQuestion, type AskQuestion, type AskQuestionOption, type AskQuestionRequest, type QuestionAnswer, type QuestionAnswerItem, } from "./core/ask-question.js";
17
- export { createLspTool, gitCheckIgnoreFilter, LSP_OPERATIONS, LSP_FAILURE_BRAND, brandLspFailure, lspFailureOf, type LspNoneReason, type LspOperation, type LspServerManager, type LspSession, type LspResult, type LspLocation, type LspSymbolInfo, type LspRequestParams, type LspToolOptions, type LspTransport, type LspReadText, } from "./core/lsp.js";
17
+ export { createLspTool, gitCheckIgnoreFilter, LSP_OPERATIONS, LSP_FAILURE_BRAND, brandLspFailure, lspFailureOf, SharedAbortScope, settleOnAbort, type LspNoneReason, type LspOperation, type LspServerManager, type LspSession, type LspResult, type LspLocation, type LspSymbolInfo, type LspRequestParams, type LspToolOptions, type LspTransport, type LspReadText, } from "./core/lsp.js";
18
18
  export { buildRequest, parseResult, callHierarchyMethod, pathToUri } from "./core/lsp-protocol.js";
19
19
  export { TransportLspSession, type SessionWarmup } from "./core/lsp-session.js";
20
20
  export { LspDiagnosticsRegistry, formatDiagnosticsBlock, formatDiagnosticsSummary, type LspDiagnostic, type LspFileDiagnostics, } from "./core/lsp-diagnostics.js";
@@ -27,6 +27,7 @@ export { BrainError, classifyHttp, type BrainErrorCode } from "./brain/errors.js
27
27
  export { looksDegenerate, inspectDegenerate, trimDegenerateTail } from "./brain/repetition.js";
28
28
  export type { RepetitionEvent, RepetitionInspection } from "./brain/repetition.js";
29
29
  export { computeCostMicroUsd, modelCostToPricing, type ModelPricing, type TokenCounts, } from "./core/pricing.js";
30
+ export { cacheFamilyOf, promptTokensOf, uncachedInputTokensOf, type CacheFamily } from "./core/runner/usage-accounting.js";
30
31
  export { emitTrace, type TraceEvent, type TracerHook } from "./core/trace.js";
31
32
  export { InMemoryStrategyStore, type StrategyStore, type StoredStrategy } from "./core/strategy-store.js";
32
33
  export { createSqlTool, validateReadOnlySql, type SqlToolOptions } from "./tools/sql.js";
@@ -56,6 +57,7 @@ export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-e
56
57
  export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
57
58
  export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
58
59
  export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode } from "./internal/harness.js";
60
+ export type { ExecResult } from "./internal/harness.js";
59
61
  export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
60
62
  export { withRetry } from "./core/with-retry.js";
61
63
  export type { RetryPolicy, RetryResult } from "./core/with-retry.js";
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { assembleCodeTools, CODE_ROLE } from "./scenarios/full-body.js";
12
12
  export { capAggregateToolResults, AGGREGATE_TOOL_RESULT_BUDGET_CHARS, DEFAULT_BUDGET_EXEMPT_TOOLS, } from "./core/tool-result-budget.js";
13
13
  export { capAggregateMediaBytes, AGGREGATE_MEDIA_BUDGET_BYTES } from "./core/media-byte-cap.js";
14
14
  export { createAskUserQuestionTool, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, } from "./core/ask-question.js";
15
- export { createLspTool, gitCheckIgnoreFilter, LSP_OPERATIONS, LSP_FAILURE_BRAND, brandLspFailure, lspFailureOf, } from "./core/lsp.js";
15
+ export { createLspTool, gitCheckIgnoreFilter, LSP_OPERATIONS, LSP_FAILURE_BRAND, brandLspFailure, lspFailureOf, SharedAbortScope, settleOnAbort, } from "./core/lsp.js";
16
16
  export { buildRequest, parseResult, callHierarchyMethod, pathToUri } from "./core/lsp-protocol.js";
17
17
  export { TransportLspSession } from "./core/lsp-session.js";
18
18
  export { LspDiagnosticsRegistry, formatDiagnosticsBlock, formatDiagnosticsSummary, } from "./core/lsp-diagnostics.js";
@@ -24,6 +24,7 @@ export { selectModel, selectModelOrThrow } from "./core/select-model.js";
24
24
  export { BrainError, classifyHttp } from "./brain/errors.js";
25
25
  export { looksDegenerate, inspectDegenerate, trimDegenerateTail } from "./brain/repetition.js";
26
26
  export { computeCostMicroUsd, modelCostToPricing, } from "./core/pricing.js";
27
+ export { cacheFamilyOf, promptTokensOf, uncachedInputTokensOf } from "./core/runner/usage-accounting.js";
27
28
  export { emitTrace } from "./core/trace.js";
28
29
  export { InMemoryStrategyStore } from "./core/strategy-store.js";
29
30
  export { createSqlTool, validateReadOnlySql } from "./tools/sql.js";
@@ -3,5 +3,7 @@ export type { InvokedSkillRetention } from "../engine/compaction/utils.js";
3
3
  export type { AgentCoreRuntimeDeps } from "../engine/loop/runtime-deps.js";
4
4
  export type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode, } from "../engine/loop/types.js";
5
5
  export type { AgentHarnessEvent, CompactionSettings, ExecutionEnv, ExecutionErrorCode, FileErrorCode, FileInfo, Result, Session, SessionMetadata, SessionRepo, SessionStorage, SessionTreeEntry, Skill, } from "../engine/harness/types.js";
6
- export type { ExecutionEnvExecOptions } from "../engine/harness/types.js";
6
+ export type { ExecutionEnvExecOptions, ExecResult } from "../engine/harness/types.js";
7
7
  export type { SessionWriteOptions, CompactionEntry } from "../engine/harness/types.js";
8
+ export type { ActiveWorktreeSession, WorkspaceState } from "../engine/harness/types.js";
9
+ export type { SessionForkOptions } from "../engine/harness/types.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatFileOperations, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatFileOperations, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -4,8 +4,10 @@ import { isAbsolute, resolve, sep } from "node:path";
4
4
  import { combinePolicies } from "../core/tool-policy.js";
5
5
  import { delimitUntrusted } from "../core/untrusted-text.js";
6
6
  import { isDurablePause } from "../agents/suspend-guard.js";
7
+ import { isAbsolutePathForm, writeTargetPath } from "../tools/fs/safety.js";
8
+ import { canonicalToolName } from "../core/tool-name-aliases.js";
7
9
  import { captureBaseline, resolveFrozenPaths, restoreFrozenPaths, runOracle, snapshotFrozenPaths, specError, } from "../core/spec-contract.js";
8
- const WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
10
+ const WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"].map(canonicalToolName));
9
11
  const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
10
12
  const pathKey = (p) => {
11
13
  let n = p.replace(/\\/g, "/");
@@ -17,13 +19,14 @@ function frozenDenyPolicy(rootDir, frozenResolved) {
17
19
  const frozen = new Set(frozenResolved.map(pathKey));
18
20
  return {
19
21
  check(req) {
20
- if (!WRITE_TOOLS.has(req.toolName))
22
+ const toolName = canonicalToolName(req.toolName);
23
+ if (!WRITE_TOOLS.has(toolName))
21
24
  return { action: "allow" };
22
25
  const args = req.args;
23
- const raw = args?.file_path ?? args?.path ?? args?.notebook_path;
26
+ const raw = writeTargetPath(toolName, args);
24
27
  if (typeof raw !== "string" || raw.length === 0)
25
28
  return { action: "allow" };
26
- const absForm = raw.startsWith("/") || /^[A-Za-z]:[\\/]/.test(raw);
29
+ const absForm = isAbsolutePathForm(raw);
27
30
  const abs = pathKey(absForm ? raw : resolve(rootDir, raw));
28
31
  if (frozen.has(abs)) {
29
32
  return { action: "deny", reason: `${raw} is part of the frozen specification surface (read-only).` };
@@ -456,7 +456,7 @@ export async function createRunWorkflowTool(d) {
456
456
  },
457
457
  });
458
458
  }
459
- if (d.notifier) {
459
+ {
460
460
  let notified = false;
461
461
  const fire = (notification) => {
462
462
  if (notified)
@@ -467,6 +467,8 @@ export async function createRunWorkflowTool(d) {
467
467
  }
468
468
  catch {
469
469
  }
470
+ if (!d.notifier)
471
+ return;
470
472
  void Promise.resolve()
471
473
  .then(() => d.notifier.notify({
472
474
  runId,
@@ -507,40 +509,6 @@ export async function createRunWorkflowTool(d) {
507
509
  }))
508
510
  .catch(() => { });
509
511
  }
510
- else {
511
- let notified = false;
512
- const fire = (notification) => {
513
- if (notified)
514
- return;
515
- notified = true;
516
- try {
517
- d.taskNotification?.(notification);
518
- }
519
- catch {
520
- }
521
- };
522
- handle.done
523
- .then((res) => fire({
524
- task_id: runId,
525
- task_type: "workflow",
526
- ...(ctx.toolCallId !== undefined ? { toolUseId: ctx.toolCallId } : {}),
527
- status: "completed",
528
- summary: completionSummary(meta.name, res.run),
529
- result: boundedRedactedSummary(res.result, 4000),
530
- usage: workflowUsageBlock(res.run),
531
- diagnostics: workflowDiagnostics(runId, d.journalStore?.locator?.(runId, res.run.scope), res.run.resume?.journalEntries === 0),
532
- completionId: res.run.completionId,
533
- }), (err) => fire({
534
- task_id: runId,
535
- task_type: "workflow",
536
- ...(ctx.toolCallId !== undefined ? { toolUseId: ctx.toolCallId } : {}),
537
- status: "failed",
538
- summary: failureSummary(err, runId),
539
- diagnostics: workflowDiagnostics(runId, undefined),
540
- ...(completionIdFromRejection(err) !== undefined ? { completionId: completionIdFromRejection(err) } : {}),
541
- }))
542
- .catch(() => { });
543
- }
544
512
  return {
545
513
  details: { type: "workflow-run", status: "backgrounded", workflowRunId: runId },
546
514
  content: JSON.stringify({
@@ -37,6 +37,32 @@ function minDefined(...vals) {
37
37
  const xs = vals.filter((v) => typeof v === "number" && Number.isFinite(v));
38
38
  return xs.length ? Math.min(...xs) : undefined;
39
39
  }
40
+ function minPositiveDefined(...vals) {
41
+ const xs = vals.filter((v) => typeof v === "number" && Number.isFinite(v) && v > 0);
42
+ return xs.length ? Math.min(...xs) : undefined;
43
+ }
44
+ const NON_POSITIVE_LIMIT_MSG = (field, what) => `agent(spec): \`limits.${field}\` must be a POSITIVE number — a value of 0 or less DISABLES the ${what} ` +
45
+ `downstream (it is a sentinel, not a tighter ceiling), and a workflow script may only tighten a limit, never remove it`;
46
+ function pickImageInput(el, i) {
47
+ const at = `agent(spec): \`images[${i}]\``;
48
+ if (typeof el !== "object" || el === null || Array.isArray(el)) {
49
+ throw new WorkflowScriptError(`${at} must be an object — either { url } or { data, mimeType }`);
50
+ }
51
+ const o = el;
52
+ if ("url" in o) {
53
+ if (typeof o.url !== "string" || o.url.length === 0) {
54
+ throw new WorkflowScriptError(`${at}.url must be a non-empty string`);
55
+ }
56
+ return { url: o.url };
57
+ }
58
+ if (typeof o.data !== "string") {
59
+ throw new WorkflowScriptError(`${at}.data must be a base64 string (or supply { url } instead)`);
60
+ }
61
+ if (typeof o.mimeType !== "string" || o.mimeType.length === 0) {
62
+ throw new WorkflowScriptError(`${at}.mimeType must be a non-empty string, e.g. "image/png"`);
63
+ }
64
+ return { data: o.data, mimeType: o.mimeType };
65
+ }
40
66
  function pickWhitelist(scriptSpec) {
41
67
  if (typeof scriptSpec !== "object" || scriptSpec === null || Array.isArray(scriptSpec)) {
42
68
  throw new WorkflowScriptError("agent(spec): spec must be an object with at least an `objective` string");
@@ -60,7 +86,7 @@ function pickWhitelist(scriptSpec) {
60
86
  if (s.images !== undefined) {
61
87
  if (!Array.isArray(s.images))
62
88
  throw new WorkflowScriptError("agent(spec): `images` must be an array");
63
- safe.images = s.images;
89
+ safe.images = s.images.map(pickImageInput);
64
90
  }
65
91
  if (s.limits !== undefined) {
66
92
  if (typeof s.limits !== "object" || s.limits === null || Array.isArray(s.limits)) {
@@ -71,11 +97,15 @@ function pickWhitelist(scriptSpec) {
71
97
  if (l.maxTurns !== undefined) {
72
98
  if (typeof l.maxTurns !== "number" || !Number.isFinite(l.maxTurns))
73
99
  throw new WorkflowScriptError("agent(spec): `limits.maxTurns` must be a number");
100
+ if (l.maxTurns <= 0)
101
+ throw new WorkflowScriptError(NON_POSITIVE_LIMIT_MSG("maxTurns", "turn cap"));
74
102
  limits.maxTurns = l.maxTurns;
75
103
  }
76
104
  if (l.timeoutSec !== undefined) {
77
105
  if (typeof l.timeoutSec !== "number" || !Number.isFinite(l.timeoutSec))
78
106
  throw new WorkflowScriptError("agent(spec): `limits.timeoutSec` must be a number");
107
+ if (l.timeoutSec <= 0)
108
+ throw new WorkflowScriptError(NON_POSITIVE_LIMIT_MSG("timeoutSec", "wall-clock window"));
79
109
  limits.timeoutSec = l.timeoutSec;
80
110
  }
81
111
  safe.limits = limits;
@@ -116,20 +146,24 @@ function clampResourceLimits(safe, base, caps) {
116
146
  }
117
147
  const requestedTimeoutSec = safe.limits?.timeoutSec;
118
148
  const requestedMaxTurns = safe.limits?.maxTurns;
119
- const timeoutSec = minDefined(safe.limits?.timeoutSec, base.limits?.timeoutSec, caps?.perAgentTimeoutSec);
120
- const maxTurns = minDefined(safe.limits?.maxTurns, base.limits?.maxTurns, caps?.childMaxTurns);
149
+ const timeoutSec = minPositiveDefined(safe.limits?.timeoutSec, base.limits?.timeoutSec, caps?.perAgentTimeoutSec);
150
+ const maxTurns = minPositiveDefined(safe.limits?.maxTurns, base.limits?.maxTurns, caps?.childMaxTurns);
121
151
  if (timeoutSec !== undefined && timeoutSec !== requestedTimeoutSec) {
122
152
  notes.push({ field: "timeoutSec", requested: requestedTimeoutSec, applied: timeoutSec });
123
153
  }
124
154
  if (maxTurns !== undefined && maxTurns !== requestedMaxTurns) {
125
155
  notes.push({ field: "maxTurns", requested: requestedMaxTurns, applied: maxTurns });
126
156
  }
127
- if (timeoutSec !== undefined || maxTurns !== undefined) {
128
- safe.limits = {
129
- ...(maxTurns !== undefined ? { maxTurns } : {}),
130
- ...(timeoutSec !== undefined ? { timeoutSec } : {}),
131
- };
132
- }
157
+ const baseOtherLimits = { ...base.limits };
158
+ delete baseOtherLimits.maxTurns;
159
+ delete baseOtherLimits.timeoutSec;
160
+ const rebuilt = {
161
+ ...baseOtherLimits,
162
+ ...(maxTurns !== undefined ? { maxTurns } : {}),
163
+ ...(timeoutSec !== undefined ? { timeoutSec } : {}),
164
+ };
165
+ if (base.limits !== undefined || Object.keys(rebuilt).length > 0)
166
+ safe.limits = rebuilt;
133
167
  return notes;
134
168
  }
135
169
  export function buildGovernedChildSpec(scriptSpec, baseline, models, caps, onResourceClamp) {
@@ -1,20 +1,31 @@
1
1
  import assert from "node:assert/strict";
2
2
  const META = "export const meta = { name: 'conformance', description: 'sandbox escape probes' }\n";
3
+ const WITNESS_FN = "__probeWitness";
4
+ const WITNESS_PRELUDE = `function ${WITNESS_FN}(v) {\n` +
5
+ ` if (v === null || v === undefined) return null\n` +
6
+ ` var t = typeof v\n` +
7
+ ` if (t === 'function') return { hostCapabilityReached: true, via: 'function' }\n` +
8
+ ` if (t !== 'object') return null\n` +
9
+ ` var names = ['pid', 'env', 'version', 'mainModule', 'binding']\n` +
10
+ ` var hit = []\n` +
11
+ ` for (var i = 0; i < names.length; i++) { try { if (names[i] in v) hit.push(names[i]) } catch (err) { } }\n` +
12
+ ` return hit.length > 0 ? { hostCapabilityReached: true, via: 'object', keys: hit } : null\n` +
13
+ `}\n`;
3
14
  const ESCAPE_BODIES = [
4
- `return agent.constructor.constructor("return typeof process !== 'undefined' ? process : null")()`,
5
- `return log.constructor.constructor("return typeof process !== 'undefined' ? process : null")()`,
6
- `return budget.constructor.constructor("return typeof process !== 'undefined' ? process : null")()`,
7
- `return ({}).constructor.constructor("return typeof process !== 'undefined' ? process : null")()`,
8
- `return args.constructor.constructor("return typeof require !== 'undefined' ? require : null")()`,
9
- `const r = await agent({ objective: 'x' }); return (r ?? {}).constructor.constructor("return typeof require !== 'undefined' ? require : null")()`,
10
- `return (typeof process !== 'undefined' ? process : null)`,
11
- `return (typeof require !== 'undefined' ? require : null)`,
12
- `return (typeof globalThis !== 'undefined' && globalThis.process) ? globalThis.process : null`,
15
+ `return ${WITNESS_FN}(agent.constructor.constructor("return typeof process !== 'undefined' ? process : null")())`,
16
+ `return ${WITNESS_FN}(log.constructor.constructor("return typeof process !== 'undefined' ? process : null")())`,
17
+ `return ${WITNESS_FN}(budget.constructor.constructor("return typeof process !== 'undefined' ? process : null")())`,
18
+ `return ${WITNESS_FN}(({}).constructor.constructor("return typeof process !== 'undefined' ? process : null")())`,
19
+ `return ${WITNESS_FN}(args.constructor.constructor("return typeof require !== 'undefined' ? require : null")())`,
20
+ `const r = await agent({ objective: 'x' }); return ${WITNESS_FN}((r ?? {}).constructor.constructor("return typeof require !== 'undefined' ? require : null")())`,
21
+ `return ${WITNESS_FN}(typeof process !== 'undefined' ? process : null)`,
22
+ `return ${WITNESS_FN}(typeof require !== 'undefined' ? require : null)`,
23
+ `return ${WITNESS_FN}((typeof globalThis !== 'undefined' && globalThis.process) ? globalThis.process : null)`,
13
24
  ];
14
25
  const ERROR_PATH_BODIES = [
15
- `try { await agent({ objective: 'x' }); return null } catch (e) { return e.constructor.constructor("return typeof process !== 'undefined' ? process : null")() }`,
16
- `try { await parallel([]); return null } catch (e) { return e.constructor.constructor("return typeof require !== 'undefined' ? require : null")() }`,
17
- `try { await agent({}); return null } catch (e) { return (e && e.constructor && e.constructor.constructor) ? e.constructor.constructor("return (typeof globalThis !== 'undefined' && globalThis.process) ? globalThis.process : null")() : null }`,
26
+ `try { await agent({ objective: 'x' }); return null } catch (e) { return ${WITNESS_FN}(e.constructor.constructor("return typeof process !== 'undefined' ? process : null")()) }`,
27
+ `try { await parallel([]); return null } catch (e) { return ${WITNESS_FN}(e.constructor.constructor("return typeof require !== 'undefined' ? require : null")()) }`,
28
+ `try { await agent({}); return null } catch (e) { return ${WITNESS_FN}((e && e.constructor && e.constructor.constructor) ? e.constructor.constructor("return (typeof globalThis !== 'undefined' && globalThis.process) ? globalThis.process : null")() : null) }`,
18
29
  ];
19
30
  const POLLUTION_BODIES = [
20
31
  `return JSON.parse('{"__proto__":{"polluted":1}}')`,
@@ -24,9 +35,9 @@ const POLLUTION_BODIES = [
24
35
  `return [JSON.parse('{"__proto__":{"polluted":1}}')]`,
25
36
  ];
26
37
  const SIGNAL_BODIES = [
27
- `return (typeof signal !== 'undefined' && signal) ? signal.constructor.constructor("return typeof process !== 'undefined' ? process : null")() : null`,
28
- `return (typeof input !== 'undefined' && input && input.signal) ? input.signal.constructor.constructor("return typeof process !== 'undefined' ? process : null")() : null`,
29
- `try { return (typeof signal !== 'undefined' && signal) ? (signal.addEventListener ? signal.addEventListener.constructor("return typeof process !== 'undefined' ? process : null")() : null) : null } catch (e) { return null }`,
38
+ `return ${WITNESS_FN}((typeof signal !== 'undefined' && signal) ? signal.constructor.constructor("return typeof process !== 'undefined' ? process : null")() : null)`,
39
+ `return ${WITNESS_FN}((typeof input !== 'undefined' && input && input.signal) ? input.signal.constructor.constructor("return typeof process !== 'undefined' ? process : null")() : null)`,
40
+ `try { return ${WITNESS_FN}((typeof signal !== 'undefined' && signal) ? (signal.addEventListener ? signal.addEventListener.constructor("return typeof process !== 'undefined' ? process : null")() : null) : null) } catch (e) { return null }`,
30
41
  ];
31
42
  function benignPrimitives() {
32
43
  return {
@@ -84,6 +95,49 @@ function looksLikeHostCapability(v) {
84
95
  }
85
96
  return false;
86
97
  }
98
+ function witnessReportsHostCapability(v) {
99
+ return typeof v === "object" && v !== null && v.hostCapabilityReached === true;
100
+ }
101
+ function reachedHostCapability(v) {
102
+ return looksLikeHostCapability(v) || witnessReportsHostCapability(v);
103
+ }
104
+ const CANARY_MARKER = "workflow-conformance-canary";
105
+ let canarySeq = 0;
106
+ const CANARY_BODY = `const c = args.canary\n` +
107
+ `return { marker: '${CANARY_MARKER}', nonce: c.nonce, sum: c.x + c.y, joined: ['${CANARY_MARKER}', c.nonce].join(':') }`;
108
+ async function runLivenessCanary(runner) {
109
+ const nonce = `n${++canarySeq}`;
110
+ const scriptArgs = { canary: { nonce, x: 17, y: 25 } };
111
+ let result;
112
+ try {
113
+ const out = await runner.run({ scriptSource: META + CANARY_BODY, primitives: benignPrimitives(), scriptArgs });
114
+ result = out.result;
115
+ }
116
+ catch (err) {
117
+ return { ok: false, detail: `run() REJECTED with: ${err instanceof Error ? err.message : String(err)}` };
118
+ }
119
+ const expected = { marker: CANARY_MARKER, nonce, sum: 42, joined: `${CANARY_MARKER}:${nonce}` };
120
+ try {
121
+ assert.deepEqual(result, expected);
122
+ }
123
+ catch {
124
+ return { ok: false, detail: `run() returned ${JSON.stringify(result)} — expected ${JSON.stringify(expected)}` };
125
+ }
126
+ return { ok: true, detail: "" };
127
+ }
128
+ async function runProbeBody(runner, scriptSource, primitives, family, signal) {
129
+ try {
130
+ const out = await runner.run({ scriptSource, primitives, scriptArgs: { probe: true }, signal });
131
+ return { executed: true, result: out.result };
132
+ }
133
+ catch (err) {
134
+ const live = await runLivenessCanary(runner);
135
+ assert.ok(live.ok, `${family}: the probe threw AND the runner then failed the liveness canary — this runner is BROKEN, ` +
136
+ `not blocking, so nothing about its membrane was proven. probe: ${scriptSource.slice(META.length)} | ` +
137
+ `probe threw: ${err instanceof Error ? err.message : String(err)} | canary: ${live.detail}`);
138
+ return { executed: false };
139
+ }
140
+ }
87
141
  export async function assertWorkflowPrimitivesWiring(runner) {
88
142
  assert.equal(runner.safeForUntrustedScripts, true, "wiring conformance targets a HARD runner (safeForUntrustedScripts must be true)");
89
143
  const hostSeen = { phaseCalls: [], scopedBodyRan: false, logs: [] };
@@ -144,54 +198,34 @@ export async function assertWorkflowPrimitivesWiring(runner) {
144
198
  }
145
199
  export async function assertWorkflowSandboxConformance(runner) {
146
200
  assert.equal(runner.safeForUntrustedScripts, true, "conformance is only meaningful for a HARD runner (safeForUntrustedScripts must be true)");
201
+ const live = await runLivenessCanary(runner);
202
+ assert.ok(live.ok, `POSITIVE CONTROL FAILED — this runner did not execute the canary script and return its value, so the ` +
203
+ `escape battery below cannot certify anything about it (a runner that runs nothing produces the same ` +
204
+ `"nothing leaked" results as a perfect sandbox). ${live.detail}`);
147
205
  for (const escBody of ESCAPE_BODIES) {
148
- const scriptSource = META + escBody;
149
- let result;
150
- try {
151
- const out = await runner.run({ scriptSource, primitives: benignPrimitives(), scriptArgs: { probe: true } });
152
- result = out.result;
153
- }
154
- catch {
206
+ const out = await runProbeBody(runner, META + WITNESS_PRELUDE + escBody, benignPrimitives(), "sandbox escape");
207
+ if (!out.executed)
155
208
  continue;
156
- }
157
- assert.equal(looksLikeHostCapability(result), false, `sandbox escape REACHED a host capability via: ${escBody}`);
209
+ assert.equal(reachedHostCapability(out.result), false, `sandbox escape REACHED a host capability via: ${escBody}`);
158
210
  }
159
211
  for (const errBody of ERROR_PATH_BODIES) {
160
- const scriptSource = META + errBody;
161
- let result;
162
- try {
163
- const out = await runner.run({ scriptSource, primitives: throwingPrimitives(), scriptArgs: { probe: true } });
164
- result = out.result;
165
- }
166
- catch {
212
+ const out = await runProbeBody(runner, META + WITNESS_PRELUDE + errBody, throwingPrimitives(), "error-path");
213
+ if (!out.executed)
167
214
  continue;
168
- }
169
- assert.equal(looksLikeHostCapability(result), false, `error-path: a host-side primitive error leaked a host capability via: ${errBody}`);
215
+ assert.equal(reachedHostCapability(out.result), false, `error-path: a host-side primitive error leaked a host capability via: ${errBody}`);
170
216
  }
171
217
  for (const polBody of POLLUTION_BODIES) {
172
- const scriptSource = META + polBody;
173
- let result;
174
- try {
175
- const out = await runner.run({ scriptSource, primitives: benignPrimitives(), scriptArgs: { probe: true } });
176
- result = out.result;
177
- }
178
- catch {
218
+ const out = await runProbeBody(runner, META + polBody, benignPrimitives(), "prototype pollution");
219
+ if (!out.executed)
179
220
  continue;
180
- }
181
- assert.equal(graphHasInjectedProtoKey(result), false, `prototype pollution: the marshalled result carries an injected __proto__/constructor/prototype own key via: ${polBody}`);
221
+ assert.equal(graphHasInjectedProtoKey(out.result), false, `prototype pollution: the marshalled result carries an injected __proto__/constructor/prototype own key via: ${polBody}`);
182
222
  }
183
223
  const hostSignal = new AbortController().signal;
184
224
  for (const sigBody of SIGNAL_BODIES) {
185
- const scriptSource = META + sigBody;
186
- let result;
187
- try {
188
- const out = await runner.run({ scriptSource, primitives: benignPrimitives(), scriptArgs: { probe: true }, signal: hostSignal });
189
- result = out.result;
190
- }
191
- catch {
225
+ const out = await runProbeBody(runner, META + WITNESS_PRELUDE + sigBody, benignPrimitives(), "signal-path", hostSignal);
226
+ if (!out.executed)
192
227
  continue;
193
- }
194
- assert.equal(looksLikeHostCapability(result), false, `signal-path: the host AbortSignal leaked a host capability via: ${sigBody}`);
228
+ assert.equal(reachedHostCapability(out.result), false, `signal-path: the host AbortSignal leaked a host capability via: ${sigBody}`);
195
229
  }
196
230
  }
197
231
  const DETERMINISM_FORBIDDEN_BODIES = [
@@ -1,5 +1,5 @@
1
- import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
2
- import { join, resolve, sep } from "node:path";
1
+ import { mkdirSync, readFileSync, realpathSync, writeFileSync, existsSync } from "node:fs";
2
+ import { basename, dirname, join, resolve, sep } from "node:path";
3
3
  import { sanitizeScope } from "../stores/file/fs-atomic.js";
4
4
  function isPlainObject(v) {
5
5
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -30,6 +30,26 @@ export function normalizeStringArg(callArgs, defaultArgs, key = "topic") {
30
30
  function safeFileStem(id) {
31
31
  return /^[A-Za-z0-9._-]{1,120}$/.test(id) && !id.startsWith(".") ? id : undefined;
32
32
  }
33
+ function containedBy(root, p) {
34
+ return p === root || p.startsWith(root + sep);
35
+ }
36
+ function canonicalizeDeepestExisting(p) {
37
+ let cur = p;
38
+ const missingTail = [];
39
+ for (;;) {
40
+ try {
41
+ const real = realpathSync(cur);
42
+ return missingTail.length === 0 ? real : join(real, ...[...missingTail].reverse());
43
+ }
44
+ catch {
45
+ const parent = dirname(cur);
46
+ if (parent === cur)
47
+ return undefined;
48
+ missingTail.push(basename(cur));
49
+ cur = parent;
50
+ }
51
+ }
52
+ }
33
53
  export function createFileWorkflowScriptStore(dir) {
34
54
  const root = resolve(dir);
35
55
  const scopeDir = (scope) => join(root, sanitizeScope(scope));
@@ -48,10 +68,20 @@ export function createFileWorkflowScriptStore(dir) {
48
68
  load(scriptPath, scope) {
49
69
  const base = scopeDir(scope);
50
70
  const full = resolve(scriptPath);
51
- if (full !== base && !full.startsWith(base + sep)) {
71
+ if (!containedBy(base, full)) {
52
72
  throw new Error(`workflow script store: scriptPath is outside this scope's script partition`);
53
73
  }
54
- return readFileSync(full, "utf-8");
74
+ const physRoot = canonicalizeDeepestExisting(root);
75
+ const physBase = canonicalizeDeepestExisting(base);
76
+ const physFull = canonicalizeDeepestExisting(full);
77
+ const refusePhysical = () => {
78
+ throw new Error(`workflow script store: scriptPath is outside this scope's script partition after symlink resolution`);
79
+ };
80
+ if (physRoot !== undefined && physBase !== undefined && !containedBy(physRoot, physBase))
81
+ refusePhysical();
82
+ if (physBase !== undefined && physFull !== undefined && !containedBy(physBase, physFull))
83
+ refusePhysical();
84
+ return readFileSync(physFull ?? full, "utf-8");
55
85
  },
56
86
  resolveName(name) {
57
87
  if (/^wf_/i.test(name))