@hasna/workflows 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,6 +87,34 @@ run API includes `triggerRun`, `getRun`, `listNodes`, `events`, `cancelRun` and
87
87
  the graph, context and cycle budget. Reusing a key for a different request returns
88
88
  HTTP 409. Run context cannot contain the reserved `__wf` or `steps` fields.
89
89
 
90
+ A step's optional `cwd` names its working directory on the server executing the
91
+ workflow. It applies to shell commands and model lanes; prefer an absolute path.
92
+ Codex prompts keep their native repository, sandbox and approval defaults. Supply
93
+ `cwd` for work in an existing trusted repository. For a prompt that needs no file
94
+ changes and may run outside a repository, explicitly set `codexMode: "read-only"`:
95
+
96
+ ```ts
97
+ import { createWorkflowsClient, type WorkflowGraph } from "@hasna/workflows/sdk";
98
+
99
+ const workflows = createWorkflowsClient();
100
+ const graph: WorkflowGraph = {
101
+ name: "read-only-answer", version: "1",
102
+ nodes: [
103
+ { id: "start", type: "start", next: "answer" },
104
+ { id: "answer", type: "step", lane: "codex", codexMode: "read-only",
105
+ prompt: "Return the requested answer without changing files.", next: "end" },
106
+ { id: "end", type: "end" },
107
+ ],
108
+ };
109
+ await workflows.triggerRun(graph, {}, { idempotencyKey: "read-only-answer-1" });
110
+ ```
111
+
112
+ This explicit mode uses Codex's read-only sandbox, permits a nonrepository
113
+ directory and refuses approval escalation. It applies only to Codex prompt steps;
114
+ shell commands cannot select it. An empty Codex response fails the step even when
115
+ the native process exits successfully. Memoization distinguishes the working
116
+ directory, lane and execution mode.
117
+
90
118
  ## MCP
91
119
 
92
120
  Register `workflows-mcp` as a stdio command in Codex, Claude Code, OpenCode, Sumi,
@@ -113,6 +141,31 @@ Use durable storage for the data directory and one deployed executor; ephemeral
113
141
  container storage is unsuitable. Model lanes require the selected harness's
114
142
  native subscription on the executor. They do not inherit paid model API keys.
115
143
 
144
+ Set `HASNA_WORKFLOWS_ENABLED_LANES` to a comma-separated subset of
145
+ `claude,codex,cursor,grok` to restrict model execution. An explicitly empty value
146
+ permits command steps only; leaving the variable unset preserves all four lanes.
147
+ Lane inspection reports disabled lanes as `wired: false` with a configuration
148
+ reason. Trigger rejects a graph containing a disabled prompt lane before any
149
+ step runs, and resume leaves such a stored run unchanged. Existing run history
150
+ and event reads remain available. This admission setting does not grant model
151
+ authentication or authorize API billing.
152
+
153
+ For a deployment with a restored data directory, set
154
+ `HASNA_WORKFLOWS_DATA_IDENTITY` to its non-secret storage identity. After verifying
155
+ the restore, the operator provisions `.workflows-data-identity` containing that
156
+ identity followed by a newline. The directory must already contain readable and
157
+ writable `workflows.db` and `sessions/session.wal`. The server refuses missing,
158
+ mismatched or inaccessible state without creating a replacement. Readiness and
159
+ storage operations repeat the check. The server also holds a process-lifetime
160
+ SQLite mutex in `.server-lease.sqlite`, containing no tables, so an overlapping
161
+ replacement cannot open a second server over the same local journal. Keep this
162
+ file in place; never delete it to take a live process's lease. This mode requires
163
+ a local filesystem with reliable SQLite locking, not a network filesystem.
164
+
165
+ Journal append acknowledgements follow file and parent-directory flushes. The
166
+ journal and PostgreSQL are separate stores, not one atomic transaction; capture
167
+ and restore them together while execution is quiescent.
168
+
116
169
  When the server sits behind an authenticated gateway, set
117
170
  `HASNA_WORKFLOWS_ORIGIN_AUTH` to the gateway's scoped origin identity. It must be
118
171
  32 to 256 ASCII letters, digits, underscores or hyphens. All API routes, including
@@ -16,502 +16,7 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = import.meta.require;
18
18
 
19
- // src/lanes/types.ts
20
- import { statSync } from "fs";
21
- function binaryOnPath(binary) {
22
- const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
23
- for (const entry of pathEntries) {
24
- try {
25
- const stat = statSync(`${entry}/${binary}`);
26
- if (stat.isFile())
27
- return true;
28
- } catch {}
29
- }
30
- return false;
31
- }
32
- function subscriptionEnvironment(overrides = {}) {
33
- const paid = /^(OPENAI_API_KEY|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|CODEX_API_KEY|CURSOR_API_KEY)$/;
34
- const sensitive = /(?:API[_-]?KEY|SECRET|PASSWORD|TOKEN|DATABASE_URL|CREDENTIAL)/i;
35
- const env = {};
36
- for (const [name, value] of Object.entries(process.env)) {
37
- if (value !== undefined && !paid.test(name) && (!sensitive.test(name) || name === "CLAUDE_CODE_OAUTH_TOKEN"))
38
- env[name] = value;
39
- }
40
- for (const [name, value] of Object.entries(overrides)) {
41
- if (paid.test(name) || sensitive.test(name))
42
- throw new Error(`lane environment field ${name} requires a supported scoped credential consumer`);
43
- env[name] = value;
44
- }
45
- return env;
46
- }
47
- function subscriptionSdkEnvironment(overrides = {}) {
48
- const safe = subscriptionEnvironment(overrides);
49
- return { ...Object.fromEntries(Object.keys(process.env).map((name) => [name, undefined])), ...safe };
50
- }
51
- function withTimeout(promise, timeoutMs, what, cancel) {
52
- return new Promise((resolve, reject) => {
53
- const timer = setTimeout(() => {
54
- cancel?.();
55
- reject(new LaneTimeoutError(what, timeoutMs));
56
- }, timeoutMs);
57
- promise.then((value) => {
58
- clearTimeout(timer);
59
- resolve(value);
60
- }, (err) => {
61
- clearTimeout(timer);
62
- reject(err);
63
- });
64
- });
65
- }
66
- async function spawnCli(cmd, opts) {
67
- const started = Date.now();
68
- const timeoutMs = opts.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
69
- let proc;
70
- try {
71
- proc = Bun.spawn({
72
- cmd,
73
- cwd: opts.cwd,
74
- env: subscriptionEnvironment(opts.env),
75
- stdin: "ignore",
76
- stdout: "pipe",
77
- stderr: "pipe",
78
- timeout: timeoutMs
79
- });
80
- } catch (err) {
81
- const message = String(err instanceof Error ? err.message : err);
82
- if (message.includes("ENOENT") || /not found/i.test(message)) {
83
- throw new LaneDependencyMissingError(`lane binary ${JSON.stringify(opts.binaryName)} not found on PATH; install it or provide ${opts.packageHint}`);
84
- }
85
- return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
86
- }
87
- const [stdout, stderr, exitCode] = await Promise.all([
88
- new Response(proc.stdout).text(),
89
- new Response(proc.stderr).text(),
90
- proc.exited
91
- ]);
92
- const output = stderr ? `${stdout}
93
- ${stderr}`.trim() : stdout.trim();
94
- return { ok: exitCode === 0, exitCode, output, durationMs: Date.now() - started };
95
- }
96
- var LANE_KINDS2, LaneDependencyMissingError, LaneAdapterShapeError, DEFAULT_LANE_TIMEOUT_MS = 120000, LaneTimeoutError;
97
- var init_types = __esm(() => {
98
- LANE_KINDS2 = ["claude", "codex", "cursor", "grok"];
99
- LaneDependencyMissingError = class LaneDependencyMissingError extends Error {
100
- constructor(message) {
101
- super(message);
102
- this.name = "LaneDependencyMissingError";
103
- }
104
- };
105
- LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
106
- constructor(message) {
107
- super(message);
108
- this.name = "LaneAdapterShapeError";
109
- }
110
- };
111
- LaneTimeoutError = class LaneTimeoutError extends Error {
112
- constructor(what, timeoutMs) {
113
- super(`${what} exceeded the ${timeoutMs}ms budget`);
114
- this.name = "LaneTimeoutError";
115
- }
116
- };
117
- });
118
-
119
- // src/lanes/claude.ts
120
- function createClaudeAdapter(deps = {}) {
121
- return {
122
- kind: "claude",
123
- async probe() {
124
- const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
125
- try {
126
- await loader();
127
- return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "sdk" };
128
- } catch {
129
- if (binaryOnPath(deps.cliPath ?? "claude")) {
130
- return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "cli" };
131
- }
132
- return {
133
- kind: "claude",
134
- sdk: "@anthropic-ai/claude-agent-sdk",
135
- wired: false,
136
- reason: "neither @anthropic-ai/claude-agent-sdk nor the claude CLI is available on this machine"
137
- };
138
- }
139
- },
140
- async run(job) {
141
- if (!job.prompt) {
142
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "claude lane requires a prompt" };
143
- }
144
- const started = Date.now();
145
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
146
- const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
147
- let sdk;
148
- try {
149
- sdk = await loader();
150
- } catch (err) {
151
- return runClaudeCli(job, deps.cliPath ?? "claude", started, timeoutMs);
152
- }
153
- const query = sdk.query;
154
- if (typeof query !== "function") {
155
- throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
156
- }
157
- const prompt = job.prompt;
158
- const abortController = new AbortController;
159
- try {
160
- const { output, isError } = await withTimeout((async () => {
161
- const generator = await query({
162
- prompt,
163
- options: { model: job.model, cwd: job.cwd, maxTurns: 32, abortController, env: subscriptionSdkEnvironment(job.env) }
164
- });
165
- let output2 = "";
166
- let isError2 = false;
167
- for await (const message of generator) {
168
- if (message.type === "assistant") {
169
- const content = message.message?.content;
170
- if (Array.isArray(content)) {
171
- for (const block of content) {
172
- if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
173
- output2 += block.text;
174
- }
175
- }
176
- }
177
- }
178
- if (message.type === "error")
179
- isError2 = true;
180
- }
181
- return { output: output2, isError: isError2 };
182
- })(), timeoutMs, "claude lane", () => abortController.abort());
183
- const durationMs = Date.now() - started;
184
- return { ok: !isError, exitCode: isError ? 1 : 0, output: output.trim(), durationMs };
185
- } catch (err) {
186
- const durationMs = Date.now() - started;
187
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
188
- }
189
- }
190
- };
191
- }
192
- async function runClaudeCli(job, cliPath, started, timeoutMs) {
193
- try {
194
- const result = await spawnCli([cliPath, "-p", job.prompt], {
195
- binaryName: cliPath,
196
- packageHint: "@anthropic-ai/claude-agent-sdk or the claude CLI",
197
- timeoutMs,
198
- env: job.env,
199
- cwd: job.cwd
200
- });
201
- return { ...result, durationMs: Date.now() - started };
202
- } catch (err) {
203
- if (err instanceof LaneDependencyMissingError) {
204
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
205
- }
206
- throw err;
207
- }
208
- }
209
- var init_claude = __esm(() => {
210
- init_types();
211
- });
212
-
213
- // src/lanes/codex.ts
214
- function createCodexAdapter(deps = {}) {
215
- return {
216
- kind: "codex",
217
- async probe() {
218
- const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
219
- try {
220
- await loader();
221
- return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "sdk" };
222
- } catch {
223
- if (binaryOnPath(deps.cliPath ?? "codex")) {
224
- return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "cli" };
225
- }
226
- return {
227
- kind: "codex",
228
- sdk: "@openai/codex-sdk",
229
- wired: false,
230
- reason: "neither @openai/codex-sdk nor the codex CLI is available on this machine"
231
- };
232
- }
233
- },
234
- async run(job) {
235
- if (!job.prompt) {
236
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "codex lane requires a prompt" };
237
- }
238
- const started = Date.now();
239
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
240
- const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
241
- let sdk;
242
- try {
243
- sdk = await loader();
244
- } catch {
245
- return runCodexCli(job, deps.cliPath ?? "codex", started, timeoutMs);
246
- }
247
- const CodexClass = sdk.Codex;
248
- if (typeof CodexClass !== "function") {
249
- throw new LaneAdapterShapeError("@openai/codex-sdk exports no Codex class");
250
- }
251
- const abortController = new AbortController;
252
- try {
253
- const client = await withTimeout(Promise.resolve(new CodexClass({
254
- config: { model: job.model },
255
- env: subscriptionEnvironment(job.env)
256
- })), timeoutMs, "codex lane");
257
- const thread = client.startThread?.({ model: job.model, workingDirectory: job.cwd });
258
- if (!thread || typeof thread.run !== "function") {
259
- throw new LaneAdapterShapeError("@openai/codex-sdk Codex.startThread() returned no run()");
260
- }
261
- const turn = await withTimeout(thread.run(job.prompt, { signal: abortController.signal }), timeoutMs, "codex lane", () => abortController.abort());
262
- const output = turn?.finalResponse ?? "";
263
- const durationMs = Date.now() - started;
264
- return { ok: true, exitCode: 0, output, durationMs };
265
- } catch (err) {
266
- const durationMs = Date.now() - started;
267
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
268
- }
269
- }
270
- };
271
- }
272
- async function runCodexCli(job, cliPath, started, timeoutMs) {
273
- try {
274
- const result = await spawnCli([cliPath, "exec", "--", job.prompt], {
275
- binaryName: cliPath,
276
- packageHint: "@openai/codex-sdk or the codex CLI",
277
- timeoutMs,
278
- env: job.env,
279
- cwd: job.cwd
280
- });
281
- return { ...result, durationMs: Date.now() - started };
282
- } catch (err) {
283
- if (err instanceof LaneDependencyMissingError) {
284
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
285
- }
286
- throw err;
287
- }
288
- }
289
- var init_codex = __esm(() => {
290
- init_types();
291
- });
292
-
293
- // src/lanes/cursor.ts
294
- function createCursorAdapter(deps = {}) {
295
- return {
296
- kind: "cursor",
297
- async probe() {
298
- const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
299
- try {
300
- await loader();
301
- return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "sdk (local mode)" };
302
- } catch {
303
- if (binaryOnPath(deps.cliPath ?? "cursor-agent")) {
304
- return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "cli" };
305
- }
306
- return {
307
- kind: "cursor",
308
- sdk: "@cursor/sdk",
309
- wired: false,
310
- reason: "neither @cursor/sdk nor the cursor-agent CLI is available on this machine"
311
- };
312
- }
313
- },
314
- async run(job) {
315
- if (!job.prompt) {
316
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
317
- }
318
- if (!deps.sdkLoader && binaryOnPath(deps.cliPath ?? "cursor-agent"))
319
- return runCursorCli(job, deps.cliPath ?? "cursor-agent", Date.now(), job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS);
320
- const started = Date.now();
321
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
322
- const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
323
- let sdk;
324
- try {
325
- sdk = await loader();
326
- } catch {
327
- return runCursorCli(job, deps.cliPath ?? "cursor-agent", started, timeoutMs);
328
- }
329
- let cancel;
330
- try {
331
- subscriptionEnvironment(job.env);
332
- if (!deps.sdkLoader && process.env.CURSOR_API_KEY)
333
- throw new Error("cursor lane refuses ambient API billing; use the native subscription CLI");
334
- const run = await withTimeout(runLocalAgent(sdk, job, (callback) => {
335
- cancel = callback;
336
- }), timeoutMs, "cursor lane", () => cancel?.());
337
- const durationMs = Date.now() - started;
338
- const status = run.status;
339
- const ok = status === "finished";
340
- return { ok, exitCode: ok ? 0 : 1, output: run.result, durationMs };
341
- } catch (err) {
342
- if (err instanceof LaneAdapterShapeError)
343
- throw err;
344
- const durationMs = Date.now() - started;
345
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
346
- }
347
- }
348
- };
349
- }
350
- async function runLocalAgent(sdk, job, onCancel) {
351
- const AgentClass = sdk.Agent;
352
- const CursorClass = sdk.Cursor;
353
- if (typeof AgentClass !== "function" && typeof CursorClass !== "function") {
354
- throw new LaneAdapterShapeError("@cursor/sdk exports no Agent/Cursor class");
355
- }
356
- const create = AgentClass.create;
357
- const agent = typeof create === "function" ? await create.call(AgentClass, { local: { cwd: job.cwd, enableAgentRetries: false } }) : typeof AgentClass === "function" ? new AgentClass : undefined;
358
- if (!agent || typeof agent.send !== "function") {
359
- throw new LaneAdapterShapeError("@cursor/sdk Agent has no send()");
360
- }
361
- let run = await agent.send(job.prompt, { model: job.model });
362
- if (run && typeof run === "object") {
363
- const handle = run;
364
- if (typeof handle.cancel === "function")
365
- onCancel(() => {
366
- handle.cancel().catch(() => {});
367
- });
368
- if (typeof handle.wait === "function")
369
- run = await handle.wait();
370
- }
371
- if (!run || typeof run !== "object") {
372
- throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
373
- }
374
- const candidate = run;
375
- if (typeof candidate.status !== "string" || !["finished", "error", "cancelled"].includes(candidate.status)) {
376
- throw new LaneAdapterShapeError(`@cursor/sdk Agent.send() returned a Run without a terminal status (got ${JSON.stringify(candidate.status)}; expected finished|error|cancelled)`);
377
- }
378
- if (typeof candidate.result !== "string") {
379
- throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
380
- }
381
- return { status: candidate.status, result: candidate.result };
382
- }
383
- async function runCursorCli(job, cliPath, started, timeoutMs) {
384
- try {
385
- const result = await spawnCli([cliPath, "--print", job.prompt, ...job.model ? ["--model", job.model] : []], {
386
- binaryName: cliPath,
387
- packageHint: "@cursor/sdk (local mode) or the cursor-agent CLI",
388
- timeoutMs,
389
- env: job.env,
390
- cwd: job.cwd
391
- });
392
- return { ...result, durationMs: Date.now() - started };
393
- } catch (err) {
394
- if (err instanceof LaneDependencyMissingError) {
395
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
396
- }
397
- throw err;
398
- }
399
- }
400
- var init_cursor = __esm(() => {
401
- init_types();
402
- });
403
-
404
- // src/lanes/grok.ts
405
- function createGrokAdapter(deps = {}) {
406
- return {
407
- kind: "grok",
408
- async probe() {
409
- const cliPath = deps.cliPath ?? "grok";
410
- if (binaryOnPath(cliPath)) {
411
- return { kind: "grok", sdk: "no npm SDK (measured 2026-08-25)", wired: true, via: "cli" };
412
- }
413
- return {
414
- kind: "grok",
415
- sdk: "no npm SDK (measured 2026-08-25)",
416
- wired: false,
417
- reason: `no xAI Grok SDK exists on npm (measured 2026-08-25) and the grok CLI is not on PATH`
418
- };
419
- },
420
- async run(job) {
421
- if (!job.prompt) {
422
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "grok lane requires a prompt" };
423
- }
424
- const started = Date.now();
425
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
426
- const cliPath = deps.cliPath ?? "grok";
427
- try {
428
- const result = await spawnCli([cliPath, job.prompt], {
429
- binaryName: cliPath,
430
- packageHint: "the grok CLI (no xAI Grok SDK exists on npm \u2014 measured 2026-08-25)",
431
- timeoutMs,
432
- env: job.env,
433
- cwd: job.cwd
434
- });
435
- return { ...result, durationMs: Date.now() - started };
436
- } catch (err) {
437
- if (err instanceof LaneDependencyMissingError) {
438
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
439
- }
440
- throw err;
441
- }
442
- }
443
- };
444
- }
445
- var init_grok = __esm(() => {
446
- init_types();
447
- });
448
-
449
- // src/lanes/index.ts
450
- var exports_lanes = {};
451
- __export(exports_lanes, {
452
- runLaneJob: () => runLaneJob,
453
- resolveLane: () => resolveLane,
454
- probeLane: () => probeLane,
455
- laneInventory: () => laneInventory,
456
- createGrokAdapter: () => createGrokAdapter,
457
- createCursorAdapter: () => createCursorAdapter,
458
- createCodexAdapter: () => createCodexAdapter,
459
- createClaudeAdapter: () => createClaudeAdapter,
460
- binaryOnPath: () => binaryOnPath,
461
- LaneDependencyMissingError: () => LaneDependencyMissingError,
462
- LaneAdapterShapeError: () => LaneAdapterShapeError,
463
- LANE_KINDS: () => LANE_KINDS2
464
- });
465
- function resolveLane(kind) {
466
- const adapter = adapters.get(kind);
467
- if (!adapter)
468
- throw new Error(`unknown lane ${kind} (expected one of ${LANE_KINDS2.join(", ")})`);
469
- return adapter;
470
- }
471
- async function runLaneJob(job) {
472
- return resolveLane(job.lane).run(job);
473
- }
474
- async function probeLane(kind) {
475
- return resolveLane(kind).probe();
476
- }
477
- async function laneInventory() {
478
- return Promise.all(LANE_KINDS2.map((kind) => probeLane(kind)));
479
- }
480
- var adapters;
481
- var init_lanes = __esm(() => {
482
- init_claude();
483
- init_codex();
484
- init_cursor();
485
- init_grok();
486
- init_types();
487
- init_types();
488
- init_claude();
489
- init_codex();
490
- init_cursor();
491
- init_grok();
492
- adapters = new Map([
493
- ["claude", createClaudeAdapter()],
494
- ["codex", createCodexAdapter()],
495
- ["cursor", createCursorAdapter()],
496
- ["grok", createGrokAdapter()]
497
- ]);
498
- });
499
-
500
19
  // src/expr.ts
501
- class ExprSyntaxError extends Error {
502
- constructor(message) {
503
- super(message);
504
- this.name = "ExprSyntaxError";
505
- }
506
- }
507
-
508
- class ExprEvalError extends Error {
509
- constructor(message) {
510
- super(message);
511
- this.name = "ExprEvalError";
512
- }
513
- }
514
- var COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
515
20
  function tokenize(input) {
516
21
  const tokens = [];
517
22
  let i = 0;
@@ -763,11 +268,24 @@ function evaluateExpr(input, context) {
763
268
  }
764
269
  return value;
765
270
  }
271
+ var ExprSyntaxError, ExprEvalError, COMPARISON_OPS;
272
+ var init_expr = __esm(() => {
273
+ ExprSyntaxError = class ExprSyntaxError extends Error {
274
+ constructor(message) {
275
+ super(message);
276
+ this.name = "ExprSyntaxError";
277
+ }
278
+ };
279
+ ExprEvalError = class ExprEvalError extends Error {
280
+ constructor(message) {
281
+ super(message);
282
+ this.name = "ExprEvalError";
283
+ }
284
+ };
285
+ COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
286
+ });
766
287
 
767
288
  // src/graph.ts
768
- var GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
769
- var LANE_KINDS = ["claude", "codex", "cursor", "grok"];
770
- var NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
771
289
  function issue(issues, path, message) {
772
290
  issues.push({ path, message });
773
291
  }
@@ -985,6 +503,16 @@ function validateNode(graph, node, issues) {
985
503
  if (node.lane !== undefined && !LANE_KINDS.includes(node.lane)) {
986
504
  issue(issues, `nodes.${node.id}`, `unknown lane ${JSON.stringify(node.lane)} (expected one of ${LANE_KINDS.join(", ")})`);
987
505
  }
506
+ if (node.cwd !== undefined && (typeof node.cwd !== "string" || !node.cwd.trim() || node.cwd.includes("\x00"))) {
507
+ issue(issues, `nodes.${node.id}.cwd`, "cwd must be a non-empty path string without NUL characters on the executor");
508
+ }
509
+ if (node.codexMode !== undefined) {
510
+ if (!CODEX_EXECUTION_MODES.includes(node.codexMode)) {
511
+ issue(issues, `nodes.${node.id}.codexMode`, "codexMode must be repository or read-only");
512
+ } else if (node.lane !== "codex" || !node.prompt || node.command !== undefined) {
513
+ issue(issues, `nodes.${node.id}.codexMode`, "codexMode is supported only for a codex prompt step without command");
514
+ }
515
+ }
988
516
  if (node.maxRetries !== undefined && (!Number.isInteger(node.maxRetries) || node.maxRetries < 0)) {
989
517
  issue(issues, `nodes.${node.id}`, "maxRetries must be a non-negative integer");
990
518
  }
@@ -1062,9 +590,17 @@ function validateNode(graph, node, issues) {
1062
590
  break;
1063
591
  }
1064
592
  }
593
+ var GRAPH_NODE_TYPES, LANE_KINDS, CODEX_EXECUTION_MODES, NODE_ID_RE;
594
+ var init_graph = __esm(() => {
595
+ init_expr();
596
+ GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
597
+ LANE_KINDS = ["claude", "codex", "cursor", "grok"];
598
+ CODEX_EXECUTION_MODES = ["repository", "read-only"];
599
+ NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
600
+ });
1065
601
 
1066
602
  // src/agent-tools.ts
1067
- init_lanes();
603
+ init_graph();
1068
604
 
1069
605
  // src/tool-definitions.ts
1070
606
  function textResult(payload) {
@@ -1158,7 +694,7 @@ async function callWorkflowTool(service, name, args) {
1158
694
  limit: typeof args.limit === "number" ? args.limit : 100
1159
695
  }));
1160
696
  case "workflows_lanes_list":
1161
- return textResult(await laneInventory());
697
+ return textResult(await service.laneInventory());
1162
698
  default:
1163
699
  throw new Error("Unknown workflows tool");
1164
700
  }