@sema-agent/core 2.12.0 → 3.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 (115) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +1 -0
  2. package/dist/agents/agent-transcript-tool.js +1 -1
  3. package/dist/agents/cascade.d.ts +4 -0
  4. package/dist/agents/cascade.js +54 -9
  5. package/dist/agents/repair-loop.d.ts +2 -0
  6. package/dist/agents/repair-loop.js +101 -2
  7. package/dist/agents/retain-ledger.d.ts +18 -2
  8. package/dist/agents/retain-ledger.js +37 -8
  9. package/dist/agents/roster-store.d.ts +4 -0
  10. package/dist/agents/roster-store.js +22 -1
  11. package/dist/agents/teacher.js +4 -2
  12. package/dist/agents/team.js +7 -4
  13. package/dist/agents/verify.d.ts +15 -2
  14. package/dist/agents/verify.js +36 -1
  15. package/dist/bin/sema-tb.js +2 -2
  16. package/dist/brain/circuit-breaker.d.ts +1 -0
  17. package/dist/brain/circuit-breaker.js +26 -9
  18. package/dist/brain/degrading.js +13 -1
  19. package/dist/brain/failover.js +2 -0
  20. package/dist/brain/repetition.js +10 -1
  21. package/dist/brain/routing.js +5 -1
  22. package/dist/brain/status-sink.js +5 -1
  23. package/dist/brain/stream-engine.js +4 -1
  24. package/dist/brain/tool-call-repair.js +32 -14
  25. package/dist/core/auto-compaction.js +17 -7
  26. package/dist/core/auto-promote.js +2 -1
  27. package/dist/core/checkpoint-store.d.ts +1 -0
  28. package/dist/core/checkpoint-store.js +28 -10
  29. package/dist/core/consolidate-scope.js +4 -3
  30. package/dist/core/context-edit.js +3 -0
  31. package/dist/core/context-guard.js +3 -0
  32. package/dist/core/git-worktree-env.d.ts +6 -1
  33. package/dist/core/git-worktree-env.js +17 -1
  34. package/dist/core/hooks.d.ts +1 -0
  35. package/dist/core/hooks.js +40 -2
  36. package/dist/core/lsp-diagnostics.js +13 -7
  37. package/dist/core/lsp-protocol.d.ts +1 -1
  38. package/dist/core/lsp-protocol.js +20 -8
  39. package/dist/core/lsp-session.d.ts +12 -2
  40. package/dist/core/lsp-session.js +114 -49
  41. package/dist/core/lsp.d.ts +13 -1
  42. package/dist/core/lsp.js +64 -9
  43. package/dist/core/mcp.d.ts +1 -0
  44. package/dist/core/mcp.js +4 -3
  45. package/dist/core/memory-engine/data-plane.js +4 -0
  46. package/dist/core/memory-engine/engine.d.ts +2 -1
  47. package/dist/core/memory-engine/engine.js +82 -7
  48. package/dist/core/memory-engine/file-backend.d.ts +1 -0
  49. package/dist/core/memory-engine/file-backend.js +13 -3
  50. package/dist/core/memory-engine/layout.js +12 -1
  51. package/dist/core/memory.js +3 -0
  52. package/dist/core/permission-rules.d.ts +2 -1
  53. package/dist/core/permission-rules.js +24 -3
  54. package/dist/core/runner/active-skill-scope.js +34 -6
  55. package/dist/core/runner/assemble-result.d.ts +1 -1
  56. package/dist/core/runner/assemble-result.js +0 -1
  57. package/dist/core/runner/prepare-task.js +23 -3
  58. package/dist/core/runner/runtask.js +48 -30
  59. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  60. package/dist/core/runner/tool-disclosure.js +26 -7
  61. package/dist/core/runner/usage-accounting.d.ts +6 -0
  62. package/dist/core/runner/usage-accounting.js +9 -8
  63. package/dist/core/secret-env.js +5 -1
  64. package/dist/core/sensitive-path-policy.js +9 -3
  65. package/dist/core/session-store.js +15 -4
  66. package/dist/core/skill-tool-specifier.d.ts +8 -0
  67. package/dist/core/skill-tool-specifier.js +58 -0
  68. package/dist/core/skills-directory.d.ts +1 -1
  69. package/dist/core/skills-directory.js +16 -4
  70. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  71. package/dist/core/task-registry-monitor.js +7 -1
  72. package/dist/core/tool-policy.js +65 -17
  73. package/dist/core/trace.d.ts +1 -0
  74. package/dist/core/types.d.ts +3 -5
  75. package/dist/core/with-retry.js +0 -1
  76. package/dist/engine/compaction/compaction.d.ts +1 -0
  77. package/dist/engine/compaction/compaction.js +14 -5
  78. package/dist/engine/lsp/frame-decoder.js +6 -3
  79. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  80. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  81. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  82. package/dist/index.d.ts +1 -1
  83. package/dist/internal/harness.d.ts +1 -1
  84. package/dist/internal/harness.js +1 -1
  85. package/dist/internal/llm.d.ts +1 -1
  86. package/dist/orchestration/workflow-governance.js +43 -9
  87. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  88. package/dist/orchestration/workflow-script-store.js +34 -4
  89. package/dist/orchestration/workflow.js +10 -6
  90. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  91. package/dist/prompt-assembly/artifact-store.js +39 -24
  92. package/dist/prompt-assembly/assemble.js +38 -7
  93. package/dist/prompt-assembly/epoch.js +19 -2
  94. package/dist/prompt-assembly/event-registry.js +3 -3
  95. package/dist/prompt-assembly/explain.js +1 -1
  96. package/dist/prompt-assembly/tool-catalog.js +16 -1
  97. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  98. package/dist/stores/file/fs-atomic.d.ts +3 -0
  99. package/dist/stores/file/fs-atomic.js +53 -8
  100. package/dist/stores/file/index.d.ts +1 -0
  101. package/dist/stores/file/index.js +7 -0
  102. package/dist/stores/file/session-store.d.ts +2 -0
  103. package/dist/stores/file/session-store.js +28 -1
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  105. package/dist/tools/fs/bash-readonly-classifier.js +57 -8
  106. package/dist/tools/fs/fs-bash.d.ts +1 -1
  107. package/dist/tools/fs/fs-bash.js +93 -5
  108. package/dist/tools/loop-tick.js +1 -1
  109. package/dist/tools/monitor.js +55 -9
  110. package/dist/tools/scheduler-tools.js +9 -3
  111. package/dist/tools/web.d.ts +8 -2
  112. package/dist/tools/web.js +46 -17
  113. package/dist/tools/worktree.d.ts +2 -0
  114. package/dist/tools/worktree.js +233 -143
  115. package/package.json +5 -2
@@ -1,2 +1,2 @@
1
1
  export { createAssistantMessageEventStream, stripEngineMetadata } from "../engine/llm/index.js";
2
- export type { AnthropicMessagesCompat, AssistantMessage, AssistantMessageDiagnostic, AssistantMessageEvent, Context, DocumentContent, ImageContent, Message, Model, ResilienceOptions, SimpleStreamOptions, StallTimeouts, StopReason, StreamFn, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage, Usage, UserMessage, } from "../engine/llm/index.js";
2
+ export type { AnthropicMessagesCompat, AssistantMessage, AssistantMessageDiagnostic, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, Model, ResilienceOptions, SimpleStreamOptions, StallTimeouts, StopReason, StreamFn, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage, Usage, UserMessage, } from "../engine/llm/index.js";
@@ -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))
@@ -480,13 +480,15 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
480
480
  if (row !== undefined)
481
481
  row.sessionId = sessionId;
482
482
  };
483
- const bceTick = (callKey, e) => {
483
+ const bceTick = (callKey, e, fromSessionId) => {
484
484
  if (!bceSink)
485
485
  return;
486
486
  const id = waIdOf(callKey);
487
487
  const row = bceLive.get(id);
488
488
  if (row === undefined)
489
489
  return;
490
+ if (fromSessionId !== undefined && row.sessionId !== undefined && fromSessionId !== row.sessionId)
491
+ return;
490
492
  bceEmit({
491
493
  kind: "tick",
492
494
  taskId: id,
@@ -507,8 +509,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
507
509
  if (!bceSink)
508
510
  return;
509
511
  const id = waIdOf(callKey);
512
+ const row = bceLive.get(id);
510
513
  if (!bceLive.delete(id))
511
514
  return;
515
+ const coord = sessionId ?? row?.sessionId;
512
516
  bceEmit({
513
517
  kind: "terminal",
514
518
  taskId: id,
@@ -516,7 +520,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
516
520
  owner: runId,
517
521
  workflowRunId: runId,
518
522
  ...(scope !== undefined ? { scope } : {}),
519
- ...(sessionId !== undefined ? { sessionId, transcriptId: sessionId } : {}),
523
+ ...(coord !== undefined ? { sessionId: coord, transcriptId: coord } : {}),
520
524
  status,
521
525
  summary: boundedRedactedSummary(summary, 300),
522
526
  ...(stats !== undefined ? { usage: { tokens: stats.tokens, turns: stats.turns, ...(stats.costMicroUsd !== undefined ? { costMicroUsd: stats.costMicroUsd } : {}) } } : {}),
@@ -873,7 +877,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
873
877
  ? {
874
878
  onForwardEvent: (e) => {
875
879
  if (e.type === "task_progress")
876
- bceTick(callKey, e);
880
+ bceTick(callKey, e, attemptSessionId);
877
881
  try {
878
882
  baseInternals.onForwardEvent?.(e);
879
883
  }
@@ -927,7 +931,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
927
931
  const onTurnEndUsage = (usage) => {
928
932
  if (finalized)
929
933
  return;
930
- const turnTokens = usage ? usage.inputTokens + usage.outputTokens : 0;
934
+ const turnTokens = usage ? usage.totalInputTokens + usage.outputTokens : 0;
931
935
  const turnCost = usage?.costMicroUsd ?? 0;
932
936
  beatTokens += turnTokens;
933
937
  beatTurns += 1;
@@ -1181,7 +1185,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1181
1185
  ? {
1182
1186
  onForwardEvent: (e) => {
1183
1187
  if (e.type === "task_progress")
1184
- bceTick(callKey, e);
1188
+ bceTick(callKey, e, childSessionId);
1185
1189
  try {
1186
1190
  enrichedForwardS?.(e);
1187
1191
  }
@@ -1229,7 +1233,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1229
1233
  const onTurnEndUsage = (usage) => {
1230
1234
  if (finalized)
1231
1235
  return;
1232
- const turnTokens = usage ? usage.inputTokens + usage.outputTokens : 0;
1236
+ const turnTokens = usage ? usage.totalInputTokens + usage.outputTokens : 0;
1233
1237
  const turnCost = usage?.costMicroUsd ?? 0;
1234
1238
  beatTokens += turnTokens;
1235
1239
  beatTurns += 1;
@@ -58,6 +58,8 @@ export declare class CenterPromptSource implements PromptArtifactSource {
58
58
  private readonly engineVersion;
59
59
  private candidate;
60
60
  private state;
61
+ private tail;
62
+ private exclusive;
61
63
  constructor(opts: {
62
64
  store: PromptArtifactStore;
63
65
  stateStore?: PromptSourceStateStore;
@@ -1,6 +1,9 @@
1
1
  import { mkdirSync, promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { verifyPromptArtifact, } from "./artifact.js";
4
+ function normalizedEnvelope(parsed, artifact) {
5
+ return { ...parsed, artifact };
6
+ }
4
7
  export class MemoryPromptArtifactStore {
5
8
  byDigest = new Map();
6
9
  async get(artifactDigest, opts) {
@@ -17,7 +20,7 @@ export class MemoryPromptArtifactStore {
17
20
  const v = verifyPromptArtifact(parsed, opts);
18
21
  if (!v.ok || v.artifact.artifactDigest !== artifactDigest)
19
22
  return undefined;
20
- return parsed;
23
+ return normalizedEnvelope(parsed, v.artifact);
21
24
  }
22
25
  async put(envelope) {
23
26
  this.byDigest.set(envelope.artifact.artifactDigest, JSON.stringify(envelope));
@@ -54,7 +57,7 @@ export class FilePromptArtifactStore {
54
57
  const v = verifyPromptArtifact(parsed, opts);
55
58
  if (!v.ok || v.artifact.artifactDigest !== artifactDigest)
56
59
  return undefined;
57
- return parsed;
60
+ return normalizedEnvelope(parsed, v.artifact);
58
61
  }
59
62
  async put(envelope) {
60
63
  const file = this.fileFor(envelope.artifact.artifactDigest);
@@ -113,6 +116,12 @@ export class CenterPromptSource {
113
116
  engineVersion;
114
117
  candidate;
115
118
  state = "unavailable";
119
+ tail = Promise.resolve();
120
+ exclusive(fn) {
121
+ const run = this.tail.then(fn, fn);
122
+ this.tail = run.then(() => undefined, () => undefined);
123
+ return run;
124
+ }
116
125
  constructor(opts) {
117
126
  this.store = opts.store;
118
127
  this.stateStore = opts.stateStore ?? new MemoryPromptSourceStateStore();
@@ -128,20 +137,22 @@ export class CenterPromptSource {
128
137
  return env?.artifact;
129
138
  }
130
139
  async restore() {
131
- const state = await this.stateStore.load();
132
- if (state?.state === "disabled") {
133
- this.candidate = undefined;
134
- this.state = "disabled";
135
- return;
136
- }
137
- if (state?.state !== "active") {
138
- this.candidate = undefined;
139
- this.state = "unavailable";
140
- return;
141
- }
142
- const env = await this.store.get(state.artifactDigest, { engineVersion: this.engineVersion });
143
- this.candidate = env !== undefined ? { artifact: env.artifact, sourceRevision: state.sourceRevision } : undefined;
144
- this.state = env !== undefined ? "active" : "unavailable";
140
+ return this.exclusive(async () => {
141
+ const state = await this.stateStore.load();
142
+ if (state?.state === "disabled") {
143
+ this.candidate = undefined;
144
+ this.state = "disabled";
145
+ return;
146
+ }
147
+ if (state?.state !== "active") {
148
+ this.candidate = undefined;
149
+ this.state = "unavailable";
150
+ return;
151
+ }
152
+ const env = await this.store.get(state.artifactDigest, { engineVersion: this.engineVersion });
153
+ this.candidate = env !== undefined ? { artifact: env.artifact, sourceRevision: state.sourceRevision } : undefined;
154
+ this.state = env !== undefined ? "active" : "unavailable";
155
+ });
145
156
  }
146
157
  sourceState() {
147
158
  return this.state;
@@ -151,15 +162,19 @@ export class CenterPromptSource {
151
162
  if (!v.ok)
152
163
  return v;
153
164
  const envelope = raw;
154
- await this.store.put(envelope);
155
- await this.stateStore.save({ state: "active", artifactDigest: v.artifact.artifactDigest, sourceRevision: envelope.sourceRevision });
156
- this.candidate = { artifact: v.artifact, sourceRevision: envelope.sourceRevision };
157
- this.state = "active";
158
- return { ok: true, artifactDigest: v.artifact.artifactDigest };
165
+ return this.exclusive(async () => {
166
+ await this.store.put(envelope);
167
+ await this.stateStore.save({ state: "active", artifactDigest: v.artifact.artifactDigest, sourceRevision: envelope.sourceRevision });
168
+ this.candidate = { artifact: v.artifact, sourceRevision: envelope.sourceRevision };
169
+ this.state = "active";
170
+ return { ok: true, artifactDigest: v.artifact.artifactDigest };
171
+ });
159
172
  }
160
173
  async applyDisabled(sourceRevision) {
161
- await this.stateStore.save({ state: "disabled", sourceRevision });
162
- this.candidate = undefined;
163
- this.state = "disabled";
174
+ return this.exclusive(async () => {
175
+ await this.stateStore.save({ state: "disabled", sourceRevision });
176
+ this.candidate = undefined;
177
+ this.state = "disabled";
178
+ });
164
179
  }
165
180
  }
@@ -40,6 +40,7 @@ function mountDeclarations(basePack, decls, onWarn) {
40
40
  const counters = {};
41
41
  const extra = [];
42
42
  let replacesRoleBase = false;
43
+ let replacedRoleBaseBy;
43
44
  for (const d of decls) {
44
45
  if (!d.id || d.id.startsWith("core/") || d.id.length > 128 || !/^[a-z0-9][a-z0-9/._-]*$/.test(d.id)) {
45
46
  throw new Error(`prompt-provider stableBlocks: invalid section id ${JSON.stringify(d.id).slice(0, 140)} (must match ^[a-z0-9][a-z0-9/._-]*$, ≤128 chars, non-core/)`);
@@ -47,8 +48,10 @@ function mountDeclarations(basePack, decls, onWarn) {
47
48
  const base = SLOT_BASE[d.slot];
48
49
  if (base === undefined)
49
50
  throw new Error(`prompt-provider stableBlocks: unknown slot "${String(d.slot)}" ("${d.id}")`);
50
- if (d.slot === "identity")
51
+ if (d.slot === "identity") {
51
52
  replacesRoleBase = true;
53
+ replacedRoleBaseBy ??= d.id;
54
+ }
52
55
  const n = (counters[d.slot] = (counters[d.slot] ?? 0) + 1);
53
56
  const declaredContentHash = verifyDeclaredContentHash(d, onWarn);
54
57
  extra.push({
@@ -68,10 +71,21 @@ function mountDeclarations(basePack, decls, onWarn) {
68
71
  });
69
72
  }
70
73
  return {
71
- ...basePack,
72
- sections: [...basePack.sections.filter((s) => !(replacesRoleBase && s.id === "core/role.base")), ...extra],
74
+ pack: {
75
+ ...basePack,
76
+ sections: [...basePack.sections.filter((s) => !(replacesRoleBase && s.id === "core/role.base")), ...extra],
77
+ },
78
+ ...(replacedRoleBaseBy !== undefined && basePack.sections.some((s) => s.id === "core/role.base")
79
+ ? { replacedRoleBaseBy }
80
+ : {}),
73
81
  };
74
82
  }
83
+ function warnRoleBaseEviction(replacedRoleBaseBy, origin, onWarn) {
84
+ if (replacedRoleBaseBy === undefined || origin === "pack-default")
85
+ return;
86
+ const what = origin === "task-system-prompt" ? "the caller's per-task systemPrompt" : "the PromptProvider's own role text";
87
+ onWarn?.(`prompt-constitution: identity declaration "${replacedRoleBaseBy}" supplants the role base, so ${what} does NOT compose into this prompt (identity sections replace the role layer by design). Fold that text into the declaration, or use a scenario/behavior slot to keep the role base.`, "prompt-constitution");
88
+ }
75
89
  export function assemblePrompt(inputs) {
76
90
  const { provider, facts } = inputs;
77
91
  const onWarn = inputs.onWarn
@@ -93,6 +107,7 @@ export function assemblePrompt(inputs) {
93
107
  };
94
108
  let pack = SEMA_DEFAULT_PACK;
95
109
  let roleBase = inputs.userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
110
+ let roleBaseOrigin = inputs.userSystemPrompt !== undefined ? "task-system-prompt" : "pack-default";
96
111
  let constitution = "core";
97
112
  let mountedCenter = [];
98
113
  if (!inputs.isDefaultProvider && provider.stableBlocks) {
@@ -108,6 +123,10 @@ export function assemblePrompt(inputs) {
108
123
  if (inputs.centerDeclarations && inputs.centerDeclarations.length > 0) {
109
124
  onWarn?.("prompt-constitution: a center prompt artifact is adopted but the provider's assembled-identity pass-through owns the prompt — the center sections cannot mount on this migration path.", "prompt-constitution");
110
125
  }
126
+ const droppedDecls = decls.filter((d) => d !== assembledIdentity);
127
+ if (droppedDecls.length > 0) {
128
+ onWarn?.(`prompt-provider stableBlocks: the assembled-identity pass-through owns the prompt — ${droppedDecls.length} other declaration(s) do NOT compose this run: ${droppedDecls.map((d) => `"${d.id}" (${d.slot})`).join(", ")}. Declare ONLY the role layer in the identity block to let them mount.`, "prompt-constitution");
129
+ }
111
130
  const renderInputsPT = (date) => ({
112
131
  roleBase: assembledIdentity.text,
113
132
  facts,
@@ -153,29 +172,41 @@ export function assemblePrompt(inputs) {
153
172
  return true;
154
173
  });
155
174
  mountedCenter = centerDecls;
156
- pack = mountDeclarations(SEMA_DEFAULT_PACK, [...centerDecls, ...decls], onWarn);
175
+ const mountedSB = mountDeclarations(SEMA_DEFAULT_PACK, [...centerDecls, ...decls], onWarn);
176
+ pack = mountedSB.pack;
177
+ warnRoleBaseEviction(mountedSB.replacedRoleBaseBy, roleBaseOrigin, onWarn);
157
178
  }
158
179
  else if (!inputs.isDefaultProvider && provider.stableSystem) {
159
180
  const roleOut = provider.stableSystem(stableCtx);
160
181
  if (provider.replaceAll) {
161
182
  constitution = "replaced";
162
183
  roleBase = roleOut;
184
+ roleBaseOrigin = "provider";
163
185
  pack = subsetPack(SEMA_DEFAULT_PACK, { exclude: REPLACE_ALL_EXCLUDED, roleBaseLegacyId: "provider.replace_all" });
164
186
  }
165
187
  else if (roleOut.includes(CYBER_RISK) && roleOut.includes(URL_SAFETY) && roleOut.includes(HARNESS_SECTION_ANCHOR)) {
166
188
  constitution = "provider-assembled";
167
189
  roleBase = roleOut;
190
+ roleBaseOrigin = "provider";
168
191
  pack = subsetPack(SEMA_DEFAULT_PACK, { keep: OPAQUE_KEPT, roleBaseLegacyId: "provider.assembled" });
169
192
  onWarn?.("prompt-constitution: this PromptProvider returns an already-assembled prompt (constitution anchor found). Core now appends the constitution structurally — return ONLY the role base from stableSystem (or set replaceAll:true to own the whole base). Passed through un-doubled.", "prompt-constitution");
170
193
  }
171
194
  else {
172
195
  roleBase = roleOut;
196
+ roleBaseOrigin = "provider";
173
197
  }
174
198
  }
175
199
  if (inputs.centerDeclarations && inputs.centerDeclarations.length > 0 && !(!inputs.isDefaultProvider && provider.stableBlocks)) {
176
- const composedCenter = inputs.centerDeclarations;
177
- pack = mountDeclarations(pack, composedCenter, onWarn);
178
- mountedCenter = composedCenter;
200
+ if (constitution === "provider-assembled") {
201
+ onWarn?.("prompt-constitution: a center prompt artifact is adopted but this PromptProvider returned an already-assembled prompt — the pass-through owns the prompt and the center sections cannot mount on this migration path (an identity section would evict the provider text that carries the constitution). Return ONLY the role base from stableSystem to let center sections compose.", "prompt-constitution");
202
+ }
203
+ else {
204
+ const composedCenter = inputs.centerDeclarations;
205
+ const mountedC = mountDeclarations(pack, composedCenter, onWarn);
206
+ pack = mountedC.pack;
207
+ warnRoleBaseEviction(mountedC.replacedRoleBaseBy, roleBaseOrigin, onWarn);
208
+ mountedCenter = composedCenter;
209
+ }
179
210
  }
180
211
  const renderInputs = (date) => ({
181
212
  roleBase,