@relayflows/sdk 2.0.30 → 2.0.32

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 (347) hide show
  1. package/dist/agent-artifacts.d.ts.map +1 -1
  2. package/dist/agent-artifacts.js +2 -9
  3. package/dist/agent-artifacts.js.map +1 -1
  4. package/dist/agent-cwd.d.ts +69 -0
  5. package/dist/agent-cwd.d.ts.map +1 -0
  6. package/dist/agent-cwd.js +140 -0
  7. package/dist/agent-cwd.js.map +1 -0
  8. package/dist/artifact-scan-policy.d.ts +35 -0
  9. package/dist/artifact-scan-policy.d.ts.map +1 -0
  10. package/dist/artifact-scan-policy.js +49 -0
  11. package/dist/artifact-scan-policy.js.map +1 -0
  12. package/dist/authored-completion-record.d.ts +35 -0
  13. package/dist/authored-completion-record.d.ts.map +1 -0
  14. package/dist/authored-completion-record.js +92 -0
  15. package/dist/authored-completion-record.js.map +1 -0
  16. package/dist/authored-completion.d.ts +99 -0
  17. package/dist/authored-completion.d.ts.map +1 -0
  18. package/dist/authored-completion.js +184 -0
  19. package/dist/authored-completion.js.map +1 -0
  20. package/dist/authored-flow-error.d.ts +13 -1
  21. package/dist/authored-flow-error.d.ts.map +1 -1
  22. package/dist/authored-flow-error.js +12 -0
  23. package/dist/authored-flow-error.js.map +1 -1
  24. package/dist/authored-flow-executor.d.ts +7 -30
  25. package/dist/authored-flow-executor.d.ts.map +1 -1
  26. package/dist/authored-flow-executor.js +54 -47
  27. package/dist/authored-flow-executor.js.map +1 -1
  28. package/dist/authored-human.d.ts.map +1 -1
  29. package/dist/authored-human.js +1 -3
  30. package/dist/authored-human.js.map +1 -1
  31. package/dist/authored-node-entry.js +4 -1
  32. package/dist/authored-node-entry.js.map +1 -1
  33. package/dist/authored-node-runner.d.ts +9 -1
  34. package/dist/authored-node-runner.d.ts.map +1 -1
  35. package/dist/authored-node-runner.js +92 -17
  36. package/dist/authored-node-runner.js.map +1 -1
  37. package/dist/authored-preflight.d.ts +4 -0
  38. package/dist/authored-preflight.d.ts.map +1 -0
  39. package/dist/authored-preflight.js +50 -0
  40. package/dist/authored-preflight.js.map +1 -0
  41. package/dist/authored-root.d.ts +5 -0
  42. package/dist/authored-root.d.ts.map +1 -1
  43. package/dist/authored-root.js +20 -2
  44. package/dist/authored-root.js.map +1 -1
  45. package/dist/authored-step-output.d.ts +35 -10
  46. package/dist/authored-step-output.d.ts.map +1 -1
  47. package/dist/authored-step-output.js +108 -40
  48. package/dist/authored-step-output.js.map +1 -1
  49. package/dist/authored-verdict.d.ts +28 -0
  50. package/dist/authored-verdict.d.ts.map +1 -0
  51. package/dist/authored-verdict.js +79 -0
  52. package/dist/authored-verdict.js.map +1 -0
  53. package/dist/authored-worker-step.d.ts +5 -2
  54. package/dist/authored-worker-step.d.ts.map +1 -1
  55. package/dist/authored-worker-step.js +51 -13
  56. package/dist/authored-worker-step.js.map +1 -1
  57. package/dist/cli/check-worker-surface.d.ts +27 -0
  58. package/dist/cli/check-worker-surface.d.ts.map +1 -0
  59. package/dist/cli/check-worker-surface.js +63 -0
  60. package/dist/cli/check-worker-surface.js.map +1 -0
  61. package/dist/cli/check.d.ts +28 -3
  62. package/dist/cli/check.d.ts.map +1 -1
  63. package/dist/cli/check.js +27 -140
  64. package/dist/cli/check.js.map +1 -1
  65. package/dist/cli/cli-probe.d.ts +20 -0
  66. package/dist/cli/cli-probe.d.ts.map +1 -0
  67. package/dist/cli/cli-probe.js +166 -0
  68. package/dist/cli/cli-probe.js.map +1 -0
  69. package/dist/cli/cloud-format.d.ts +9 -0
  70. package/dist/cli/cloud-format.d.ts.map +1 -0
  71. package/dist/cli/cloud-format.js +77 -0
  72. package/dist/cli/cloud-format.js.map +1 -0
  73. package/dist/cli/cloud-live.d.ts +39 -0
  74. package/dist/cli/cloud-live.d.ts.map +1 -0
  75. package/dist/cli/cloud-live.js +325 -0
  76. package/dist/cli/cloud-live.js.map +1 -0
  77. package/dist/cli/cloud-mirror-session.d.ts +97 -0
  78. package/dist/cli/cloud-mirror-session.d.ts.map +1 -0
  79. package/dist/cli/cloud-mirror-session.js +264 -0
  80. package/dist/cli/cloud-mirror-session.js.map +1 -0
  81. package/dist/cli/cloud-read.d.ts +3 -7
  82. package/dist/cli/cloud-read.d.ts.map +1 -1
  83. package/dist/cli/cloud-read.js +14 -266
  84. package/dist/cli/cloud-read.js.map +1 -1
  85. package/dist/cli/cloud-refusal.d.ts +39 -0
  86. package/dist/cli/cloud-refusal.d.ts.map +1 -0
  87. package/dist/cli/cloud-refusal.js +95 -0
  88. package/dist/cli/cloud-refusal.js.map +1 -0
  89. package/dist/cli/cloud-status-view.d.ts +7 -0
  90. package/dist/cli/cloud-status-view.d.ts.map +1 -0
  91. package/dist/cli/cloud-status-view.js +214 -0
  92. package/dist/cli/cloud-status-view.js.map +1 -0
  93. package/dist/cli/direct-run.d.ts.map +1 -1
  94. package/dist/cli/direct-run.js +16 -2
  95. package/dist/cli/direct-run.js.map +1 -1
  96. package/dist/cli/hn-monitor.d.ts +0 -13
  97. package/dist/cli/hn-monitor.d.ts.map +1 -1
  98. package/dist/cli/hn-monitor.js +2 -1
  99. package/dist/cli/hn-monitor.js.map +1 -1
  100. package/dist/cli/local-agent-remedy.d.ts +80 -0
  101. package/dist/cli/local-agent-remedy.d.ts.map +1 -0
  102. package/dist/cli/local-agent-remedy.js +116 -0
  103. package/dist/cli/local-agent-remedy.js.map +1 -0
  104. package/dist/cli/observer-session.d.ts +41 -0
  105. package/dist/cli/observer-session.d.ts.map +1 -0
  106. package/dist/cli/observer-session.js +78 -0
  107. package/dist/cli/observer-session.js.map +1 -0
  108. package/dist/cli/run.d.ts +30 -2
  109. package/dist/cli/run.d.ts.map +1 -1
  110. package/dist/cli/run.js +186 -24
  111. package/dist/cli/run.js.map +1 -1
  112. package/dist/cli/status.d.ts +9 -0
  113. package/dist/cli/status.d.ts.map +1 -1
  114. package/dist/cli/status.js +31 -2
  115. package/dist/cli/status.js.map +1 -1
  116. package/dist/cli/step-evidence.d.ts +90 -0
  117. package/dist/cli/step-evidence.d.ts.map +1 -0
  118. package/dist/cli/step-evidence.js +259 -0
  119. package/dist/cli/step-evidence.js.map +1 -0
  120. package/dist/cli/step-failure.d.ts +14 -4
  121. package/dist/cli/step-failure.d.ts.map +1 -1
  122. package/dist/cli/step-failure.js +41 -76
  123. package/dist/cli/step-failure.js.map +1 -1
  124. package/dist/cli-commands.d.ts +12 -0
  125. package/dist/cli-commands.d.ts.map +1 -1
  126. package/dist/cli-commands.js +3 -0
  127. package/dist/cli-commands.js.map +1 -1
  128. package/dist/cli.d.ts +17 -0
  129. package/dist/cli.d.ts.map +1 -1
  130. package/dist/cli.js +134 -57
  131. package/dist/cli.js.map +1 -1
  132. package/dist/cloud-deploy.d.ts +1 -1
  133. package/dist/cloud-deploy.d.ts.map +1 -1
  134. package/dist/cloud-deploy.js +24 -8
  135. package/dist/cloud-deploy.js.map +1 -1
  136. package/dist/cloud-mirror-ledger.d.ts +40 -0
  137. package/dist/cloud-mirror-ledger.d.ts.map +1 -0
  138. package/dist/cloud-mirror-ledger.js +142 -0
  139. package/dist/cloud-mirror-ledger.js.map +1 -0
  140. package/dist/cloud-mirror-step.d.ts +168 -0
  141. package/dist/cloud-mirror-step.d.ts.map +1 -0
  142. package/dist/cloud-mirror-step.js +504 -0
  143. package/dist/cloud-mirror-step.js.map +1 -0
  144. package/dist/cloud-mirror-transport.d.ts +94 -0
  145. package/dist/cloud-mirror-transport.d.ts.map +1 -0
  146. package/dist/cloud-mirror-transport.js +163 -0
  147. package/dist/cloud-mirror-transport.js.map +1 -0
  148. package/dist/cloud-mirror.d.ts +132 -0
  149. package/dist/cloud-mirror.d.ts.map +1 -0
  150. package/dist/cloud-mirror.js +556 -0
  151. package/dist/cloud-mirror.js.map +1 -0
  152. package/dist/cloud-read.d.ts +15 -0
  153. package/dist/cloud-read.d.ts.map +1 -1
  154. package/dist/cloud-read.js +20 -1
  155. package/dist/cloud-read.js.map +1 -1
  156. package/dist/cloud-run-record.d.ts +27 -0
  157. package/dist/cloud-run-record.d.ts.map +1 -0
  158. package/dist/cloud-run-record.js +56 -0
  159. package/dist/cloud-run-record.js.map +1 -0
  160. package/dist/cloud-run.d.ts +2 -9
  161. package/dist/cloud-run.d.ts.map +1 -1
  162. package/dist/cloud-run.js +3 -20
  163. package/dist/cloud-run.js.map +1 -1
  164. package/dist/communication/local.d.ts.map +1 -1
  165. package/dist/communication/local.js +2 -1
  166. package/dist/communication/local.js.map +1 -1
  167. package/dist/communication/worker.d.ts +1 -1
  168. package/dist/communication/worker.d.ts.map +1 -1
  169. package/dist/communication/worker.js +13 -5
  170. package/dist/communication/worker.js.map +1 -1
  171. package/dist/compile.d.ts.map +1 -1
  172. package/dist/compile.js +28 -6
  173. package/dist/compile.js.map +1 -1
  174. package/dist/direct-input.d.ts.map +1 -1
  175. package/dist/direct-input.js +21 -1
  176. package/dist/direct-input.js.map +1 -1
  177. package/dist/failure-kinds.d.ts +96 -6
  178. package/dist/failure-kinds.d.ts.map +1 -1
  179. package/dist/failure-kinds.js +29 -2
  180. package/dist/failure-kinds.js.map +1 -1
  181. package/dist/flow-requirements.d.ts.map +1 -1
  182. package/dist/flow-requirements.js +24 -9
  183. package/dist/flow-requirements.js.map +1 -1
  184. package/dist/gate-contract.d.ts +7 -0
  185. package/dist/gate-contract.d.ts.map +1 -1
  186. package/dist/gate-contract.js +6 -1
  187. package/dist/gate-contract.js.map +1 -1
  188. package/dist/helper-preflight.d.ts.map +1 -1
  189. package/dist/helper-preflight.js +10 -3
  190. package/dist/helper-preflight.js.map +1 -1
  191. package/dist/helper-reference.d.ts +25 -0
  192. package/dist/helper-reference.d.ts.map +1 -0
  193. package/dist/helper-reference.js +129 -0
  194. package/dist/helper-reference.js.map +1 -0
  195. package/dist/hosted-extension-runtime.js +1 -1
  196. package/dist/index.d.ts +1 -1
  197. package/dist/index.d.ts.map +1 -1
  198. package/dist/index.js.map +1 -1
  199. package/dist/journal-client.d.ts +3 -1
  200. package/dist/journal-client.d.ts.map +1 -1
  201. package/dist/journal-client.js +17 -2
  202. package/dist/journal-client.js.map +1 -1
  203. package/dist/journal-projection.d.ts +16 -0
  204. package/dist/journal-projection.d.ts.map +1 -0
  205. package/dist/journal-projection.js +108 -0
  206. package/dist/journal-projection.js.map +1 -0
  207. package/dist/llm-worker.js +1 -1
  208. package/dist/llm-worker.js.map +1 -1
  209. package/dist/local-agent.d.ts +1 -1
  210. package/dist/local-agent.d.ts.map +1 -1
  211. package/dist/local-agent.js +4 -3
  212. package/dist/local-agent.js.map +1 -1
  213. package/dist/named-gate-lowering.js +35 -5
  214. package/dist/named-gate-lowering.js.map +1 -1
  215. package/dist/named-gate-preflight.d.ts +27 -0
  216. package/dist/named-gate-preflight.d.ts.map +1 -0
  217. package/dist/named-gate-preflight.js +47 -0
  218. package/dist/named-gate-preflight.js.map +1 -0
  219. package/dist/named-gates.d.ts +1 -1
  220. package/dist/named-gates.d.ts.map +1 -1
  221. package/dist/named-gates.js +3 -0
  222. package/dist/named-gates.js.map +1 -1
  223. package/dist/observer-link.d.ts +7 -0
  224. package/dist/observer-link.d.ts.map +1 -1
  225. package/dist/observer-link.js +3 -1
  226. package/dist/observer-link.js.map +1 -1
  227. package/dist/package-version.d.ts +10 -0
  228. package/dist/package-version.d.ts.map +1 -0
  229. package/dist/package-version.js +22 -0
  230. package/dist/package-version.js.map +1 -0
  231. package/dist/preflight.d.ts +14 -0
  232. package/dist/preflight.d.ts.map +1 -1
  233. package/dist/preflight.js +25 -3
  234. package/dist/preflight.js.map +1 -1
  235. package/dist/protocol.d.ts +2 -0
  236. package/dist/protocol.d.ts.map +1 -1
  237. package/dist/redact.d.ts +42 -0
  238. package/dist/redact.d.ts.map +1 -1
  239. package/dist/redact.js +128 -0
  240. package/dist/redact.js.map +1 -1
  241. package/dist/relay-cli.d.ts.map +1 -1
  242. package/dist/relay-cli.js +1 -21
  243. package/dist/relay-cli.js.map +1 -1
  244. package/dist/run-projection.d.ts +83 -0
  245. package/dist/run-projection.d.ts.map +1 -0
  246. package/dist/run-projection.js +199 -0
  247. package/dist/run-projection.js.map +1 -0
  248. package/dist/shell-word.d.ts +19 -0
  249. package/dist/shell-word.d.ts.map +1 -0
  250. package/dist/shell-word.js +23 -0
  251. package/dist/shell-word.js.map +1 -0
  252. package/dist/spec.d.ts +52 -3
  253. package/dist/spec.d.ts.map +1 -1
  254. package/dist/spec.js.map +1 -1
  255. package/dist/step-fields.d.ts +1 -1
  256. package/dist/step-fields.js +1 -1
  257. package/dist/step-fields.js.map +1 -1
  258. package/dist/steps-green.d.ts +27 -0
  259. package/dist/steps-green.d.ts.map +1 -0
  260. package/dist/steps-green.js +164 -0
  261. package/dist/steps-green.js.map +1 -0
  262. package/dist/validate.d.ts.map +1 -1
  263. package/dist/validate.js +39 -8
  264. package/dist/validate.js.map +1 -1
  265. package/dist/worker-lease.d.ts +8 -1
  266. package/dist/worker-lease.d.ts.map +1 -1
  267. package/dist/worker-lease.js +34 -4
  268. package/dist/worker-lease.js.map +1 -1
  269. package/dist/worker.d.ts +8 -0
  270. package/dist/worker.d.ts.map +1 -1
  271. package/dist/worker.js +17 -9
  272. package/dist/worker.js.map +1 -1
  273. package/package.json +3 -7
  274. package/src/agent-artifacts.ts +2 -10
  275. package/src/agent-cwd.ts +139 -0
  276. package/src/artifact-scan-policy.ts +51 -0
  277. package/src/authored-completion-record.ts +119 -0
  278. package/src/authored-completion.ts +203 -0
  279. package/src/authored-flow-error.ts +13 -1
  280. package/src/authored-flow-executor.ts +91 -58
  281. package/src/authored-human.ts +1 -4
  282. package/src/authored-node-entry.ts +4 -1
  283. package/src/authored-node-runner.ts +96 -22
  284. package/src/authored-preflight.ts +46 -0
  285. package/src/authored-root.ts +20 -2
  286. package/src/authored-step-output.ts +139 -42
  287. package/src/authored-verdict.ts +87 -0
  288. package/src/authored-worker-step.ts +63 -14
  289. package/src/cli/check-worker-surface.ts +66 -0
  290. package/src/cli/check.ts +59 -166
  291. package/src/cli/cli-probe.ts +194 -0
  292. package/src/cli/cloud-format.ts +82 -0
  293. package/src/cli/cloud-live.ts +366 -0
  294. package/src/cli/cloud-mirror-session.ts +329 -0
  295. package/src/cli/cloud-read.ts +13 -282
  296. package/src/cli/cloud-refusal.ts +113 -0
  297. package/src/cli/cloud-status-view.ts +213 -0
  298. package/src/cli/direct-run.ts +19 -2
  299. package/src/cli/hn-monitor.ts +2 -1
  300. package/src/cli/local-agent-remedy.ts +160 -0
  301. package/src/cli/observer-session.ts +107 -0
  302. package/src/cli/run.ts +233 -28
  303. package/src/cli/status.ts +59 -3
  304. package/src/cli/step-evidence.ts +310 -0
  305. package/src/cli/step-failure.ts +57 -81
  306. package/src/cli-commands.ts +3 -0
  307. package/src/cli.ts +155 -60
  308. package/src/cloud-deploy.ts +26 -8
  309. package/src/cloud-mirror-ledger.ts +161 -0
  310. package/src/cloud-mirror-step.ts +618 -0
  311. package/src/cloud-mirror-transport.ts +261 -0
  312. package/src/cloud-mirror.ts +633 -0
  313. package/src/cloud-read.ts +25 -1
  314. package/src/cloud-run-record.ts +64 -0
  315. package/src/cloud-run.ts +5 -24
  316. package/src/communication/local.ts +2 -1
  317. package/src/communication/worker.ts +13 -5
  318. package/src/compile.ts +28 -6
  319. package/src/direct-input.ts +22 -1
  320. package/src/failure-kinds.ts +104 -2
  321. package/src/flow-requirements.ts +24 -9
  322. package/src/gate-contract.ts +13 -1
  323. package/src/helper-preflight.ts +10 -3
  324. package/src/helper-reference.ts +130 -0
  325. package/src/hosted-extension-runtime.ts +1 -1
  326. package/src/index.ts +2 -0
  327. package/src/journal-client.ts +19 -2
  328. package/src/journal-projection.ts +106 -0
  329. package/src/llm-worker.ts +1 -1
  330. package/src/local-agent.ts +4 -2
  331. package/src/named-gate-lowering.ts +35 -5
  332. package/src/named-gate-preflight.ts +47 -0
  333. package/src/named-gates.ts +3 -0
  334. package/src/observer-link.ts +10 -1
  335. package/src/package-version.ts +23 -0
  336. package/src/preflight.ts +38 -7
  337. package/src/protocol.ts +2 -0
  338. package/src/redact.ts +111 -0
  339. package/src/relay-cli.ts +1 -24
  340. package/src/run-projection.ts +247 -0
  341. package/src/shell-word.ts +24 -0
  342. package/src/spec.ts +54 -3
  343. package/src/step-fields.ts +1 -1
  344. package/src/steps-green.ts +168 -0
  345. package/src/validate.ts +35 -8
  346. package/src/worker-lease.ts +35 -5
  347. package/src/worker.ts +27 -11
@@ -0,0 +1,168 @@
1
+ // The read side of `onNonZero: 'record'`. A `steps_green` gate names earlier
2
+ // deterministic steps and asserts every one of them recorded exit code zero.
3
+ // The verdict comes from the journal — the recorded outcome each step already
4
+ // produced — so asserting it costs nothing and cannot disagree with what ran.
5
+
6
+ import { bindingDependencies } from './input-binding.js';
7
+ import type { StepSpec, StepsGreenGate, VerificationSpec } from './spec.js';
8
+
9
+ export const STEPS_GREEN_KEYS = ['type', 'ids'] as const;
10
+
11
+ export function isStepsGreenGate(gate: VerificationSpec | undefined): gate is StepsGreenGate {
12
+ return gate?.type === 'steps_green';
13
+ }
14
+
15
+ /** Shape errors for one gate, without reference to the rest of the flow. */
16
+ export function stepsGreenErrors(gate: Record<string, unknown>, at: string, stepType: string): string[] {
17
+ const errors: string[] = [];
18
+ if (stepType !== 'deterministic') {
19
+ errors.push(`${at}: gate_host_unsupported: steps_green is supported only on deterministic steps`);
20
+ }
21
+ const ids = gate['ids'];
22
+ if (!Array.isArray(ids) || ids.length === 0
23
+ || !ids.every(id => typeof id === 'string' && id.trim().length > 0)) {
24
+ errors.push(`${at}.ids: gate_ids_invalid: expected a non-empty array of step ids`);
25
+ return errors;
26
+ }
27
+ if (new Set(ids).size !== ids.length) {
28
+ errors.push(`${at}.ids: gate_ids_invalid: step ids must be unique`);
29
+ }
30
+ return errors;
31
+ }
32
+
33
+ /**
34
+ * Flow-level errors: the ids must resolve to earlier deterministic steps whose
35
+ * recorded outcome this gate can actually read. Refusing here is the whole
36
+ * point — a gate that silently reads nothing is the `|| true` failure mode
37
+ * this feature replaces.
38
+ */
39
+ export function stepsGreenReferenceErrors(steps: readonly unknown[]): string[] {
40
+ const errors: string[] = [];
41
+ const sources = new Map(steps.flatMap((step, index) =>
42
+ isObject(step) && typeof step['id'] === 'string' ? [[step['id'], { step, index }] as const] : []));
43
+ for (const [index, step] of steps.entries()) {
44
+ if (!isObject(step)) continue;
45
+ const gate = step['verification'];
46
+ if (!isObject(gate) || gate['type'] !== 'steps_green') continue;
47
+ const ids = gate['ids'];
48
+ if (!Array.isArray(ids)) continue;
49
+ const at = `spec.steps[${index}].verification.ids`;
50
+ for (const id of ids) {
51
+ if (typeof id !== 'string') continue;
52
+ const source = sources.get(id);
53
+ if (source === undefined) {
54
+ errors.push(`${at}: gate_source_unsupported: unknown step "${id}"`);
55
+ continue;
56
+ }
57
+ if (source.index >= index) {
58
+ errors.push(`${at}: gate_source_unsupported: source "${id}" must precede this step; forward and self references are not supported`);
59
+ continue;
60
+ }
61
+ if (source.step['type'] !== 'deterministic') {
62
+ errors.push(`${at}: gate_source_unsupported: source "${id}" is a ${String(source.step['type'])} step and records no exit code`);
63
+ continue;
64
+ }
65
+ // The gate binds the source's whole output envelope, which requires a
66
+ // declared output schema. Every source shape is reachable except this
67
+ // one: `output_contains` is the single gate the compiler cannot widen
68
+ // without dropping the author's own check.
69
+ const sourceGate = source.step['verification'];
70
+ if (isObject(sourceGate) && sourceGate['type'] === 'output_contains') {
71
+ errors.push(`${at}: gate_source_unsupported: source "${id}" declares an output_contains gate, which cannot also carry the output schema this gate reads`);
72
+ }
73
+ }
74
+ }
75
+ return errors;
76
+ }
77
+
78
+ /**
79
+ * Lower every `steps_green` gate into a generated deterministic gate step.
80
+ *
81
+ * The host keeps its own command and its own exit policy; the gate is a
82
+ * separate step, always fatal, that binds each named source's journaled output
83
+ * envelope and refuses any nonzero or malformed exit code. Dependents of the
84
+ * host gain a barrier on the gate, so "these steps were green" is established
85
+ * before anything downstream of the assertion runs.
86
+ *
87
+ * Runs BEFORE `lowerNamedGates`, which then adds each source's own named-gate
88
+ * barrier to the generated step and rewrites named-gate hosts to the permissive
89
+ * envelope schema this gate's bindings need.
90
+ */
91
+ export function lowerStepsGreenGates(steps: readonly StepSpec[]): StepSpec[] {
92
+ const used = new Set(steps.map(step => step.id));
93
+ const barriers = new Map<string, string>();
94
+ for (const step of steps) {
95
+ // Deterministic hosts only, which `validateSpec` already enforces; the
96
+ // narrowing here is what lets the rewrite below stay type-exact.
97
+ if (step.type !== 'deterministic' || !isStepsGreenGate(step.verification)) continue;
98
+ let id = `${step.id}.green`;
99
+ while (used.has(id)) id += '.green';
100
+ used.add(id);
101
+ barriers.set(step.id, id);
102
+ }
103
+ if (barriers.size === 0) return [...steps];
104
+
105
+ // Every source's envelope has to be bindable. A source that declares no
106
+ // gate, or the implicit exit_code gate, gets the permissive whole-output
107
+ // schema; its exit policy is untouched, so a recording source stays red.
108
+ const bound = new Set(steps.flatMap(step =>
109
+ isStepsGreenGate(step.verification) ? step.verification.ids : []));
110
+ const output: StepSpec[] = [];
111
+ for (const step of steps) {
112
+ const dependencies = [...new Set([...(step.dependsOn ?? []), ...bindingDependencies(step.input)])];
113
+ const dependsOn = [...new Set([...dependencies, ...dependencies.flatMap(id => barriers.get(id) ?? [])])];
114
+ let producer: StepSpec = dependsOn.length ? { ...step, dependsOn } : step;
115
+ if (bound.has(step.id) && producer.type === 'deterministic'
116
+ && (producer.verification === undefined || producer.verification.type === 'exit_code')) {
117
+ producer = { ...producer, verification: { type: 'json_schema', schema: true } };
118
+ }
119
+ const gate = step.verification;
120
+ if (producer.type !== 'deterministic' || !isStepsGreenGate(gate)) {
121
+ output.push(producer);
122
+ continue;
123
+ }
124
+ // The host's own gate becomes the implicit exit_code check. Its command
125
+ // still runs, and its own `onNonZero` still applies to its own exit. A
126
+ // host that is itself read by a later gate keeps the bindable envelope.
127
+ output.push({
128
+ ...producer,
129
+ verification: bound.has(step.id) ? { type: 'json_schema', schema: true } : { type: 'exit_code' },
130
+ });
131
+ const input = Object.fromEntries(gate.ids.map((id, index) => [String(index), { step: id }]));
132
+ output.push({
133
+ id: barriers.get(step.id)!, type: 'deterministic',
134
+ // A named source that is itself a gate host must have passed its own
135
+ // assertion too — depending on the source alone would read its command
136
+ // exit and let this barrier pass while the inner gate is still red.
137
+ dependsOn: [...new Set([step.id, ...gate.ids, ...gate.ids.flatMap(id => barriers.get(id) ?? [])])], input,
138
+ command: greenCommand(gate.ids),
139
+ verification: { type: 'exit_code' }, maxIterations: step.maxIterations ?? 1,
140
+ ...(step.requirements === undefined ? {} : { requirements: step.requirements }),
141
+ });
142
+ }
143
+ return output;
144
+ }
145
+
146
+ /**
147
+ * Compiler-owned Node command. Author-supplied ids are JSON literals and the
148
+ * recorded envelopes travel only through FLOWS_INPUT, never through shell text.
149
+ */
150
+ function greenCommand(ids: readonly string[]): string {
151
+ const script = `const input=JSON.parse(process.env.FLOWS_INPUT);
152
+ const ids=${JSON.stringify(ids)};
153
+ const red=[];
154
+ for(let i=0;i<ids.length;i++){
155
+ const envelope=input[String(i)];
156
+ if(envelope===null||typeof envelope!=='object'||Array.isArray(envelope)){red.push(ids[i]+': no recorded outcome');continue;}
157
+ const code=envelope.exit_code;
158
+ if(typeof code!=='number'||!Number.isInteger(code)){red.push(ids[i]+': recorded no integer exit_code');continue;}
159
+ if(code!==0)red.push(ids[i]+': exit code '+code);
160
+ }
161
+ if(red.length>0){process.stderr.write('steps_green: '+red.join('; ')+'\\n');process.exit(1);}
162
+ process.stdout.write('steps_green:pass');`;
163
+ return `node -e '${script.replaceAll("'", "'\\''")}'`;
164
+ }
165
+
166
+ function isObject(value: unknown): value is Record<string, unknown> {
167
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
168
+ }
package/src/validate.ts CHANGED
@@ -19,10 +19,12 @@ import type {
19
19
  import { SPEC_SCHEMA_VERSION } from './spec.js';
20
20
  import { validateOutputDeclaration } from './output-schema.js';
21
21
  import { modelNameError } from './model-name.js';
22
+ import { agentCwdDeclarationError, agentCwdTransportError } from './agent-cwd.js';
22
23
  import { unknownKeyErrors } from './unknown-keys.js';
23
24
  import { stepDependencyErrors } from './step-dependencies.js';
24
25
  import { inputBindingErrors } from './input-binding.js';
25
26
  import { NAMED_GATE_KEYS, namedGateErrors } from './named-gates.js';
27
+ import { STEPS_GREEN_KEYS, stepsGreenErrors, stepsGreenReferenceErrors } from './steps-green.js';
26
28
  import {
27
29
  AGENT_DECLARATION_FIELDS,
28
30
  FLOW_FIELDS,
@@ -74,6 +76,7 @@ const BUDGET_KEYS = ['maxTokensIn', 'maxTokensOut', 'maxDollars', 'maxTokens', '
74
76
  const VERIFICATION_KEYS: Record<string, readonly string[]> = {
75
77
  ...NAMED_GATE_KEYS,
76
78
  exit_code: ['type', 'expect'],
79
+ steps_green: STEPS_GREEN_KEYS,
77
80
  output_contains: ['type', 'value'],
78
81
  json_schema: ['type', 'schema'],
79
82
  };
@@ -195,6 +198,9 @@ class Validator {
195
198
  // Dependents must reference real step ids and form a DAG (no cycles).
196
199
  for (const error of stepDependencyErrors(steps, this.ids)) this.fail(error);
197
200
  for (const error of inputBindingErrors(steps)) this.fail(error);
201
+ // A steps_green gate that names nothing readable is the `|| true` failure
202
+ // it replaces, so its references are refused at the flow level, not lowered.
203
+ for (const error of stepsGreenReferenceErrors(steps)) this.fail(error);
198
204
  return this.result();
199
205
  }
200
206
 
@@ -443,10 +449,14 @@ class Validator {
443
449
  } catch (error) {
444
450
  this.fail(error instanceof Error ? error.message : `${at}.schema: expected JSON-compatible data`);
445
451
  }
452
+ // Below here are the gates the COMPILER lowers; above are the three the
453
+ // kernel evaluates itself. The enumeration follows the same split.
454
+ } else if (gate.type === 'steps_green') {
455
+ for (const error of stepsGreenErrors(v, at, stepType)) this.fail(error);
446
456
  } else if (Object.hasOwn(NAMED_GATE_KEYS, gate.type)) {
447
457
  for (const error of namedGateErrors(v, input, at)) this.fail(error);
448
458
  } else {
449
- this.fail(`${at}.type: unknown_gate_kind: expected exit_code | output_contains | json_schema | references_input | subprocess_gate | word_count_bounds | regex_match | artifact_exists`);
459
+ this.fail(`${at}.type: unknown_gate_kind: expected exit_code | output_contains | json_schema | steps_green | references_input | subprocess_gate | word_count_bounds | regex_match | artifact_exists`);
450
460
  }
451
461
  }
452
462
 
@@ -460,6 +470,12 @@ class Validator {
460
470
  if (st.timeoutMs !== undefined && !isPosInt(st.timeoutMs)) {
461
471
  this.fail(`${at}.timeoutMs: expected a positive integer`);
462
472
  }
473
+ // Only the two policies the kernel enum names. A third spelling would
474
+ // deserialize-fail in the kernel, so it is refused where the author can
475
+ // still read the error.
476
+ if (st.onNonZero !== undefined && st.onNonZero !== 'fail' && st.onNonZero !== 'record') {
477
+ this.fail(`${at}.onNonZero: expected fail | record`);
478
+ }
463
479
  }
464
480
 
465
481
  private validateLlm(st: LlmStepSpec, at: string): void {
@@ -486,13 +502,7 @@ class Validator {
486
502
  }
487
503
  this.validateCli(st.cli, at);
488
504
  this.validateModel(st.model, at);
489
- // The kernel refuses a relative cwd at run.start; refuse it here so `flows
490
- // check` and the run agree. Not resolved against the checking directory:
491
- // that would make the compiled spec (and its hash) depend on where the
492
- // check ran. `f.agent` resolves before compiling, so it never lands here.
493
- if (st.cwd !== undefined && (!isNonEmptyString(st.cwd) || !st.cwd.startsWith('/'))) {
494
- this.fail(`${at}.cwd: expected an absolute path (got ${JSON.stringify(st.cwd)})`);
495
- }
505
+ this.validateCwd(st, at);
496
506
  if (st.surfaces !== undefined) this.validateSurfaces(st.surfaces, `${at}.surfaces`);
497
507
  if (st.permissions !== undefined) this.validatePermissions(st.permissions, `${at}.permissions`);
498
508
  }
@@ -503,6 +513,23 @@ class Validator {
503
513
  }
504
514
  }
505
515
 
516
+ /**
517
+ * The declaration half of `cwd`: lexical, so an author sees the refusal from
518
+ * `flows check` rather than from a dispatch. Whether the directory exists and
519
+ * lies inside the run root is the worker's question (`agent-cwd.ts`), asked on
520
+ * the host that shares the agent's filesystem.
521
+ */
522
+ private validateCwd(st: AgentStepSpec, at: string): void {
523
+ if (st.cwd === undefined) return;
524
+ const problem = agentCwdDeclarationError(st.cwd);
525
+ if (problem !== undefined) {
526
+ this.fail(`${at}.cwd: ${problem}`);
527
+ return;
528
+ }
529
+ const unsupported = agentCwdTransportError(st.cwd, st.transport);
530
+ if (unsupported !== undefined) this.fail(`${at}.${unsupported}`);
531
+ }
532
+
506
533
  private validateModel(model: unknown, at: string, required = false): void {
507
534
  // Rejecting the empty string matters: it would reach the CLI as
508
535
  // RELAYFLOW_MODEL='', which reads as "declared, and declared as
@@ -1,6 +1,33 @@
1
- import type { JournalClient } from './journal-client.js';
1
+ import { JournalProtocolError, type JournalClient } from './journal-client.js';
2
2
  import type { StepDispatchEvent } from './protocol.js';
3
3
 
4
+ export class WorkerLeaseLostError extends Error {
5
+ constructor(readonly reason: 'already_expired' | 'renewal_expired' | 'completion_expired', message: string) {
6
+ super(message);
7
+ this.name = 'WorkerLeaseLostError';
8
+ }
9
+ }
10
+
11
+ /** No cause traversal: worker errors preserve their original identity. */
12
+ export function isLeaseLost(error: unknown): boolean {
13
+ return error instanceof WorkerLeaseLostError || (error instanceof JournalProtocolError
14
+ && (error.code === 'lease_conflict' || (error.code === 'run_terminal'
15
+ && ['step.heartbeat', 'step.complete', 'step.wait'].includes(error.verb ?? ''))));
16
+ }
17
+
18
+ export function onWorkerFailure(label: string, fatal: (error: unknown) => void) {
19
+ return (error: unknown, dispatch?: StepDispatchEvent): void => {
20
+ // Without the dispatch there is no attempt to hand back to the kernel: fail closed.
21
+ if (!isLeaseLost(error) || dispatch === undefined) { fatal(error); return; }
22
+ // The kernel owns this attempt's fate; its journal supplies the run outcome.
23
+ // stderr keeps this diagnostic out of structured reports on stdout.
24
+ process.emitWarning(
25
+ `${label}: run_id=${dispatch.run_id} step_id=${dispatch.step_id} attempt=${dispatch.attempt}: ${String(error)}`,
26
+ { code: 'FLOWS_WORKER_LEASE_LOST' },
27
+ );
28
+ };
29
+ }
30
+
4
31
  /** Hold the dispatched lease only while its subprocess is still ours to run. */
5
32
  export async function withWorkerLease<T>(
6
33
  client: JournalClient,
@@ -16,12 +43,15 @@ export async function withWorkerLease<T>(
16
43
  const fail = (error: unknown): void => { controller.abort(error); };
17
44
  const armExpiry = (deadline: number): number => {
18
45
  const remaining = deadline - Date.now();
19
- if (!Number.isFinite(remaining) || remaining <= 0) {
46
+ if (!Number.isFinite(remaining)) {
20
47
  throw new Error(`Agent lease is already expired for ${dispatch.run_id}/${dispatch.step_id}.`);
21
48
  }
49
+ if (remaining <= 0) {
50
+ throw new WorkerLeaseLostError('already_expired', `Agent lease is already expired for ${dispatch.run_id}/${dispatch.step_id}.`);
51
+ }
22
52
  latestDeadline = deadline;
23
53
  if (expiryTimer !== undefined) clearTimeout(expiryTimer);
24
- expiryTimer = setTimeout(() => fail(new Error(
54
+ expiryTimer = setTimeout(() => fail(new WorkerLeaseLostError('renewal_expired',
25
55
  `Agent lease expired before renewal for ${dispatch.run_id}/${dispatch.step_id}.`,
26
56
  )), remaining);
27
57
  return remaining;
@@ -34,7 +64,7 @@ export async function withWorkerLease<T>(
34
64
  // A response handled after local expiry cannot revive ownership, even
35
65
  // if its future deadline was issued before this event loop stalled.
36
66
  if (Date.now() >= latestDeadline) {
37
- throw new Error(`Agent lease expired before renewal for ${dispatch.run_id}/${dispatch.step_id}.`);
67
+ throw new WorkerLeaseLostError('renewal_expired', `Agent lease expired before renewal for ${dispatch.run_id}/${dispatch.step_id}.`);
38
68
  }
39
69
  const remaining = armExpiry(result.lease_deadline_ms);
40
70
  if (!stopped) {
@@ -57,7 +87,7 @@ export async function withWorkerLease<T>(
57
87
  // Timer callbacks can be delayed behind a resolved subprocess promise.
58
88
  // Check the clock itself before permitting step.complete.
59
89
  if (Date.now() >= latestDeadline) {
60
- throw new Error(`Agent lease expired before completion for ${dispatch.run_id}/${dispatch.step_id}.`);
90
+ throw new WorkerLeaseLostError('completion_expired', `Agent lease expired before completion for ${dispatch.run_id}/${dispatch.step_id}.`);
61
91
  }
62
92
  return result;
63
93
  } finally {
package/src/worker.ts CHANGED
@@ -6,6 +6,7 @@ import type { JournalClient } from './journal-client.js';
6
6
  import type { Pins, StepDispatchEvent } from './protocol.js';
7
7
  import type { KernelAgentStep } from './spec.js';
8
8
  import { runAgentCli } from './worker-cli.js';
9
+ import { agentStepCwd } from './agent-cwd.js';
9
10
  import { resolveCliModel } from './cli-adapter.js';
10
11
  import { withWorkerLease } from './worker-lease.js';
11
12
  import { workerInstruction } from './worker-input.js';
@@ -22,6 +23,14 @@ export interface AgentWorkerOptions {
22
23
  requiredStreams?: string[];
23
24
  dataDir?: string;
24
25
  onPtyReady?: (path: string) => void;
26
+ /**
27
+ * The run root a step's declared `cwd` is resolved against and held inside.
28
+ * Defaults to this process's working directory, which is what the run root
29
+ * is: `flows run` attaches the worker from the directory it was invoked in,
30
+ * and that directory is the uploaded tree on the Cloud path. Named here so
31
+ * the root a dispatch is measured against is a value, not an assumption.
32
+ */
33
+ runRoot?: string;
25
34
  }
26
35
 
27
36
  /**
@@ -95,7 +104,7 @@ export class AgentWorker extends EventEmitter {
95
104
  if (this.closing) return;
96
105
  if (dispatch.step_type !== 'agent') return;
97
106
  const running: Promise<void> = this.execute(dispatch).catch((error: unknown) => {
98
- this.emit('error', error);
107
+ this.emit('error', error, dispatch);
99
108
  });
100
109
  this.inFlight.add(running);
101
110
  void running.finally(() => { this.inFlight.delete(running); });
@@ -113,21 +122,28 @@ export class AgentWorker extends EventEmitter {
113
122
  if (communication) {
114
123
  if (!this.options.dataDir) throw new Error('Agent communication requires a worker data directory');
115
124
  const { completeCommunicationDispatch } = await import('./communication/worker.js');
116
- await completeCommunicationDispatch(this.client, dispatch, communication, this.options.dataDir);
125
+ await completeCommunicationDispatch(this.client, dispatch, communication, this.options.dataDir, this.options.runRoot);
117
126
  return;
118
127
  }
119
128
  let humanIntervention = false;
120
129
  const effectiveModel = typeof spec.cli === 'string' ? resolveCliModel(spec.cli, spec.model) : spec.model;
130
+ // Resolved here, before the lease, because this is the process that shares
131
+ // the agent's filesystem. A refusal completes the step the way a missing
132
+ // CLI does — journaled as `worker_error` with the reason — rather than
133
+ // spawning into a directory nobody established is inside the run root.
134
+ const cwd = agentStepCwd(spec, dispatch.step_id, this.options.runRoot);
121
135
  const completed: WorkerCliResult = await withWorkerLease(this.client, dispatch, signal =>
122
- typeof spec.cli === 'string' && typeof spec.instruction === 'string'
123
- ? runAgentCli(spec.cli, workerInstruction(spec.instruction, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'agent', this.options.dataDir === undefined ? undefined : {
124
- dataDir: this.options.dataDir, runId: dispatch.run_id, stepId: dispatch.step_id, attempt: dispatch.attempt,
125
- onReady: this.options.onPtyReady, onDrive: () => { humanIntervention = true; },
126
- }, typeof spec.cwd === 'string' ? spec.cwd : undefined,
127
- spec.transport === 'relay' ? 'relay' : 'direct',
128
- { runId: dispatch.run_id, stepId: dispatch.step_id, idempotencyKey: dispatch.idempotency_key,
129
- dataDir: this.options.dataDir, resultSchema: spec.verification?.json_schema })
130
- : Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'agent step has no declared CLI' }));
136
+ 'refusal' in cwd
137
+ ? Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: cwd.refusal })
138
+ : typeof spec.cli === 'string' && typeof spec.instruction === 'string'
139
+ ? runAgentCli(spec.cli, workerInstruction(spec.instruction, dispatch), dispatch.wake_context, effectiveModel, undefined, signal, 'agent', this.options.dataDir === undefined ? undefined : {
140
+ dataDir: this.options.dataDir, runId: dispatch.run_id, stepId: dispatch.step_id, attempt: dispatch.attempt,
141
+ onReady: this.options.onPtyReady, onDrive: () => { humanIntervention = true; },
142
+ }, cwd.directory ?? (spec.transport === 'relay' ? undefined : this.options.runRoot),
143
+ spec.transport === 'relay' ? 'relay' : 'direct',
144
+ { runId: dispatch.run_id, stepId: dispatch.step_id, idempotencyKey: dispatch.idempotency_key,
145
+ dataDir: this.options.dataDir, resultSchema: spec.verification?.json_schema })
146
+ : Promise.resolve({ exit_code: null, stdout_tail: '', stderr_tail: 'agent step has no declared CLI' }));
131
147
  const { result, usage } = workerSpend(completed, effectiveModel);
132
148
  const completionReason = result.exit_code === 0 ? 'success' : 'worker_error';
133
149