@hasna/workflows 0.1.4 → 0.2.1

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/dist/mcp/index.js CHANGED
@@ -1,457 +1,170 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- var __defProp = Object.defineProperty;
4
- var __returnValue = (v) => v;
5
- function __exportSetter(name, newValue) {
6
- this[name] = __returnValue.bind(null, newValue);
7
- }
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, {
11
- get: all[name],
12
- enumerable: true,
13
- configurable: true,
14
- set: __exportSetter.bind(all, name)
15
- });
16
- };
17
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
- var __require = import.meta.require;
19
3
 
20
- // src/lanes/types.ts
21
- import { statSync as statSync2 } from "fs";
22
- function binaryOnPath(binary) {
23
- const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
24
- for (const entry of pathEntries) {
25
- try {
26
- const stat = statSync2(`${entry}/${binary}`);
27
- if (stat.isFile())
28
- return true;
29
- } catch {}
30
- }
31
- return false;
32
- }
33
- function withTimeout(promise, timeoutMs, what) {
34
- return new Promise((resolve, reject) => {
35
- const timer = setTimeout(() => {
36
- reject(new Error(`${what} exceeded the ${timeoutMs}ms budget`));
37
- }, timeoutMs);
38
- promise.then((value) => {
39
- clearTimeout(timer);
40
- resolve(value);
41
- }, (err) => {
42
- clearTimeout(timer);
43
- reject(err);
44
- });
45
- });
46
- }
47
- function spawnCli(cmd, opts) {
48
- const started = Date.now();
49
- const timeoutMs = opts.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
50
- let proc;
51
- try {
52
- proc = Bun.spawnSync({
53
- cmd,
54
- cwd: opts.cwd,
55
- env: { ...process.env, ...opts.env ?? {} },
56
- stdout: "pipe",
57
- stderr: "pipe",
58
- timeout: timeoutMs
59
- });
60
- } catch (err) {
61
- const message = String(err instanceof Error ? err.message : err);
62
- if (message.includes("ENOENT") || /not found/i.test(message)) {
63
- throw new LaneDependencyMissingError(`lane binary ${JSON.stringify(opts.binaryName)} not found on PATH; install it or provide ${opts.packageHint}`);
64
- }
65
- return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
66
- }
67
- const durationMs = Date.now() - started;
68
- const stdout = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
69
- const stderr = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
70
- const output = stderr ? `${stdout}
71
- ${stderr}`.trim() : stdout.trim();
72
- const exitCode = proc.exitCode ?? 1;
73
- return { ok: exitCode === 0, exitCode, output, durationMs };
74
- }
75
- var LANE_KINDS2, LaneDependencyMissingError, LaneAdapterShapeError, DEFAULT_LANE_TIMEOUT_MS = 120000;
76
- var init_types = __esm(() => {
77
- LANE_KINDS2 = ["claude", "codex", "cursor", "grok"];
78
- LaneDependencyMissingError = class LaneDependencyMissingError extends Error {
79
- constructor(message) {
80
- super(message);
81
- this.name = "LaneDependencyMissingError";
82
- }
83
- };
84
- LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
85
- constructor(message) {
86
- super(message);
87
- this.name = "LaneAdapterShapeError";
88
- }
89
- };
90
- });
4
+ // src/mcp/index.ts
5
+ import { createInterface } from "readline";
91
6
 
92
- // src/lanes/claude.ts
93
- function createClaudeAdapter(deps = {}) {
94
- return {
95
- kind: "claude",
96
- async probe() {
97
- const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
98
- try {
99
- await loader();
100
- return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "sdk" };
101
- } catch {
102
- if (binaryOnPath(deps.cliPath ?? "claude")) {
103
- return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "cli" };
104
- }
105
- return {
106
- kind: "claude",
107
- sdk: "@anthropic-ai/claude-agent-sdk",
108
- wired: false,
109
- reason: "neither @anthropic-ai/claude-agent-sdk nor the claude CLI is available on this machine"
110
- };
111
- }
112
- },
113
- async run(job) {
114
- if (!job.prompt) {
115
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "claude lane requires a prompt" };
116
- }
117
- const started = Date.now();
118
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
119
- const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
120
- let sdk;
121
- try {
122
- sdk = await loader();
123
- } catch (err) {
124
- return runClaudeCli(job, deps.cliPath ?? "claude", started, timeoutMs);
125
- }
126
- const query = sdk.query;
127
- if (typeof query !== "function") {
128
- throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
129
- }
130
- const prompt = job.prompt;
131
- try {
132
- const { output, isError } = await withTimeout((async () => {
133
- const generator = await query({
134
- prompt,
135
- options: { model: job.model, maxTurns: 32, abortSignal: undefined }
136
- });
137
- let output2 = "";
138
- let isError2 = false;
139
- for await (const message of generator) {
140
- if (message.type === "assistant") {
141
- const content = message.message?.content;
142
- if (Array.isArray(content)) {
143
- for (const block of content) {
144
- if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
145
- output2 += block.text;
146
- }
147
- }
148
- }
149
- }
150
- if (message.type === "error")
151
- isError2 = true;
152
- }
153
- return { output: output2, isError: isError2 };
154
- })(), timeoutMs, `claude lane (${job.prompt.slice(0, 60)})`);
155
- const durationMs = Date.now() - started;
156
- return { ok: !isError, exitCode: isError ? 1 : 0, output: output.trim(), durationMs };
157
- } catch (err) {
158
- const durationMs = Date.now() - started;
159
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
160
- }
161
- }
162
- };
163
- }
164
- function runClaudeCli(job, cliPath, started, timeoutMs) {
165
- try {
166
- const result = spawnCli([cliPath, "-p", job.prompt], {
167
- binaryName: cliPath,
168
- packageHint: "@anthropic-ai/claude-agent-sdk or the claude CLI",
169
- timeoutMs,
170
- env: job.env,
171
- cwd: job.cwd
172
- });
173
- return { ...result, durationMs: Date.now() - started };
174
- } catch (err) {
175
- if (err instanceof LaneDependencyMissingError) {
176
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
177
- }
178
- throw err;
179
- }
7
+ // src/tool-definitions.ts
8
+ function textResult(payload) {
9
+ return { content: [{ type: "text", text: JSON.stringify(payload) }], isError: false };
180
10
  }
181
- var init_claude = __esm(() => {
182
- init_types();
183
- });
184
-
185
- // src/lanes/codex.ts
186
- function createCodexAdapter(deps = {}) {
187
- return {
188
- kind: "codex",
189
- async probe() {
190
- const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
191
- try {
192
- await loader();
193
- return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "sdk" };
194
- } catch {
195
- if (binaryOnPath(deps.cliPath ?? "codex")) {
196
- return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "cli" };
197
- }
198
- return {
199
- kind: "codex",
200
- sdk: "@openai/codex-sdk",
201
- wired: false,
202
- reason: "neither @openai/codex-sdk nor the codex CLI is available on this machine"
203
- };
204
- }
11
+ function workflowsTools(_service) {
12
+ const tools = [
13
+ {
14
+ name: "workflows_version",
15
+ description: "Report the installed @hasna/workflows version.",
16
+ inputSchema: { type: "object", properties: {} }
205
17
  },
206
- async run(job) {
207
- if (!job.prompt) {
208
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "codex lane requires a prompt" };
209
- }
210
- const started = Date.now();
211
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
212
- const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
213
- let sdk;
214
- try {
215
- sdk = await loader();
216
- } catch {
217
- return runCodexCli(job, deps.cliPath ?? "codex", started, timeoutMs);
218
- }
219
- const CodexClass = sdk.Codex;
220
- if (typeof CodexClass !== "function") {
221
- throw new LaneAdapterShapeError("@openai/codex-sdk exports no Codex class");
18
+ {
19
+ name: "workflows_health",
20
+ description: "Report the workflows service health (service, version, pid, uptime).",
21
+ inputSchema: { type: "object", properties: {} }
22
+ },
23
+ {
24
+ name: "workflows_ready",
25
+ description: "Report the hosted workflows service readiness.",
26
+ inputSchema: { type: "object", properties: {} }
27
+ },
28
+ {
29
+ name: "workflows_validate",
30
+ description: "Validate a workflow graph (JSON object with name, version, nodes).",
31
+ inputSchema: {
32
+ type: "object",
33
+ properties: { graph: { type: "object", description: "the workflow graph" } },
34
+ required: ["graph"]
222
35
  }
223
- try {
224
- const client = await withTimeout(Promise.resolve(new CodexClass({
225
- config: { model: job.model },
226
- env: job.env
227
- })), timeoutMs, `codex lane (${job.prompt.slice(0, 60)})`);
228
- const thread = client.startThread?.();
229
- if (!thread || typeof thread.run !== "function") {
230
- throw new LaneAdapterShapeError("@openai/codex-sdk Codex.startThread() returned no run()");
231
- }
232
- const turn = await withTimeout(thread.run(job.prompt), timeoutMs, `codex lane (${job.prompt.slice(0, 60)})`);
233
- const output = turn?.finalResponse ?? "";
234
- const durationMs = Date.now() - started;
235
- return { ok: true, exitCode: 0, output, durationMs };
236
- } catch (err) {
237
- const durationMs = Date.now() - started;
238
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
36
+ },
37
+ {
38
+ name: "workflows_run",
39
+ description: "Run a workflow graph to a terminal state (bounded cycles; command steps execute; lane steps need the lane substrate).",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: {
43
+ graph: { type: "object", description: "the workflow graph" },
44
+ context: { type: "object", description: "optional run context" },
45
+ idempotencyKey: { type: "string", description: "stable request key" },
46
+ maxCycles: { type: "integer", minimum: 1, maximum: 2000 }
47
+ },
48
+ required: ["graph", "idempotencyKey"]
239
49
  }
240
- }
241
- };
242
- }
243
- function runCodexCli(job, cliPath, started, timeoutMs) {
244
- try {
245
- const result = spawnCli([cliPath, "exec", "--", job.prompt], {
246
- binaryName: cliPath,
247
- packageHint: "@openai/codex-sdk or the codex CLI",
248
- timeoutMs,
249
- env: job.env,
250
- cwd: job.cwd
251
- });
252
- return { ...result, durationMs: Date.now() - started };
253
- } catch (err) {
254
- if (err instanceof LaneDependencyMissingError) {
255
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
256
- }
257
- throw err;
258
- }
259
- }
260
- var init_codex = __esm(() => {
261
- init_types();
262
- });
263
-
264
- // src/lanes/cursor.ts
265
- function createCursorAdapter(deps = {}) {
266
- return {
267
- kind: "cursor",
268
- async probe() {
269
- const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
270
- try {
271
- await loader();
272
- return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "sdk (local mode)" };
273
- } catch {
274
- if (binaryOnPath(deps.cliPath ?? "cursor-agent")) {
275
- return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "cli" };
50
+ },
51
+ {
52
+ name: "workflows_runs_list",
53
+ description: "List recent runs from the configured hosted service.",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {
57
+ status: { type: "string", description: "optional status filter" },
58
+ limit: { type: "number", description: "max rows (default 100)" }
276
59
  }
277
- return {
278
- kind: "cursor",
279
- sdk: "@cursor/sdk",
280
- wired: false,
281
- reason: "neither @cursor/sdk nor the cursor-agent CLI is available on this machine"
282
- };
283
60
  }
284
61
  },
285
- async run(job) {
286
- if (!job.prompt) {
287
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
288
- }
289
- const started = Date.now();
290
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
291
- const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
292
- let sdk;
293
- try {
294
- sdk = await loader();
295
- } catch {
296
- return runCursorCli(job, deps.cliPath ?? "cursor-agent", started, timeoutMs);
297
- }
298
- try {
299
- const run = await withTimeout(runLocalAgent(sdk, job), timeoutMs, `cursor lane (${job.prompt.slice(0, 60)})`);
300
- const durationMs = Date.now() - started;
301
- const status = run.status;
302
- const ok = status === "finished";
303
- return { ok, exitCode: ok ? 0 : 1, output: run.result, durationMs };
304
- } catch (err) {
305
- if (err instanceof LaneAdapterShapeError)
306
- throw err;
307
- const durationMs = Date.now() - started;
308
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
309
- }
62
+ {
63
+ name: "workflows_lanes_list",
64
+ description: "List the four lane adapters with their wired-vs-not-ready-with-reason registry shape.",
65
+ inputSchema: { type: "object", properties: {} }
310
66
  }
311
- };
312
- }
313
- async function runLocalAgent(sdk, job) {
314
- const AgentClass = sdk.Agent;
315
- const CursorClass = sdk.Cursor;
316
- if (typeof AgentClass !== "function" && typeof CursorClass !== "function") {
317
- throw new LaneAdapterShapeError("@cursor/sdk exports no Agent/Cursor class");
318
- }
319
- const agent = typeof AgentClass === "function" ? new AgentClass : undefined;
320
- if (!agent || typeof agent.send !== "function") {
321
- throw new LaneAdapterShapeError("@cursor/sdk Agent has no send()");
322
- }
323
- const run = await agent.send(job.prompt, { model: job.model });
324
- if (!run || typeof run !== "object") {
325
- throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
326
- }
327
- const candidate = run;
328
- if (typeof candidate.status !== "string" || !["finished", "error", "cancelled"].includes(candidate.status)) {
329
- throw new LaneAdapterShapeError(`@cursor/sdk Agent.send() returned a Run without a terminal status (got ${JSON.stringify(candidate.status)}; expected finished|error|cancelled)`);
330
- }
331
- if (typeof candidate.result !== "string") {
332
- throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
333
- }
334
- return { status: candidate.status, result: candidate.result };
67
+ ];
68
+ return tools.map((tool) => ({ ...tool, inputSchema: { ...tool.inputSchema, properties: {
69
+ ...tool.inputSchema.properties,
70
+ agent: { type: "string", description: "calling agent identity" }
71
+ } } }));
335
72
  }
336
- function runCursorCli(job, cliPath, started, timeoutMs) {
337
- try {
338
- const result = spawnCli([cliPath, "run", job.prompt], {
339
- binaryName: cliPath,
340
- packageHint: "@cursor/sdk (local mode) or the cursor-agent CLI",
341
- timeoutMs,
342
- env: job.env,
343
- cwd: job.cwd
344
- });
345
- return { ...result, durationMs: Date.now() - started };
346
- } catch (err) {
347
- if (err instanceof LaneDependencyMissingError) {
348
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
73
+ // package.json
74
+ var package_default = {
75
+ name: "@hasna/workflows",
76
+ version: "0.2.1",
77
+ description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
78
+ type: "module",
79
+ main: "./dist/index.js",
80
+ types: "./types/index.d.ts",
81
+ bin: {
82
+ workflows: "dist/cli/index.js",
83
+ "workflows-mcp": "dist/mcp/index.js",
84
+ "workflows-serve": "dist/serve/index.js"
85
+ },
86
+ exports: {
87
+ ".": {
88
+ types: "./types/index.d.ts",
89
+ import: "./dist/index.js"
90
+ },
91
+ "./sdk": {
92
+ types: "./types/sdk.d.ts",
93
+ import: "./dist/sdk.js"
94
+ },
95
+ "./cli": {
96
+ types: "./types/cli/index.d.ts",
97
+ import: "./dist/cli/index.js"
349
98
  }
350
- throw err;
99
+ },
100
+ files: [
101
+ "dist",
102
+ "types",
103
+ "README.md",
104
+ "LICENSE",
105
+ "hasna.contract.json"
106
+ ],
107
+ scripts: {
108
+ build: "bun run check:sdk && bun run build:runtime && bun run build:types",
109
+ "generate:sdk": "bun scripts/generate-sdk.ts --write",
110
+ "check:sdk": "bun scripts/generate-sdk.ts",
111
+ "test:artifacts": "bun scripts/artifact-acceptance.ts",
112
+ "build:runtime": "bun build src/cli/index.ts --outdir dist/cli --target bun && bun build src/mcp/index.ts --outdir dist/mcp --target bun && bun build src/serve/index.ts src/serve/admin.ts --outdir dist/serve --target bun --packages external && bun build src/index.ts src/service.ts src/handlers.ts src/agent-tools.ts src/types.ts --root src --outdir dist --target bun --packages external && bun build src/sdk.ts --outdir dist --target node",
113
+ "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --outDir types",
114
+ "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
115
+ "contract:check": "bun scripts/contracts-cli.mjs repo-conformance .",
116
+ typecheck: "tsc --noEmit",
117
+ test: "bun test",
118
+ "test:pg": 'WORKFLOWS_TEST_DATABASE_URL="${WORKFLOWS_TEST_DATABASE_URL:?point it at a fresh test Postgres database}" WORKFLOWS_REQUIRE_POSTGRES=1 bun test src/db/postgres-store.test.ts src/db/postgres-http.test.ts src/db/postgres-fencing.test.ts',
119
+ prepack: "bun run build && bun run artifact-scan",
120
+ prepublishOnly: "bun run test && bun run build"
121
+ },
122
+ keywords: [
123
+ "workflows",
124
+ "graph",
125
+ "automation",
126
+ "cli",
127
+ "typescript",
128
+ "bun",
129
+ "hasna"
130
+ ],
131
+ publishConfig: {
132
+ registry: "https://registry.npmjs.org",
133
+ access: "public"
134
+ },
135
+ repository: {
136
+ type: "git",
137
+ url: "https://github.com/hasna/apps.git",
138
+ directory: "apps/workflows"
139
+ },
140
+ homepage: "https://github.com/hasna/apps/tree/main/apps/workflows",
141
+ bugs: {
142
+ url: "https://github.com/hasna/apps/issues"
143
+ },
144
+ engines: {
145
+ bun: ">=1.0.0"
146
+ },
147
+ author: "Hasna",
148
+ license: "Apache-2.0",
149
+ dependencies: {
150
+ "@anthropic-ai/claude-agent-sdk": "0.3.234",
151
+ "@cursor/sdk": "^1.0.28",
152
+ "@hasna/contracts": "1.3.3",
153
+ "@openai/codex-sdk": "0.160.0",
154
+ commander: "^13.1.0",
155
+ pg: "^8.16.3"
156
+ },
157
+ devDependencies: {
158
+ "@types/bun": "1.3.14",
159
+ "@types/pg": "^8.15.6",
160
+ typescript: "^5.7.3"
351
161
  }
352
- }
353
- var init_cursor = __esm(() => {
354
- init_types();
355
- });
162
+ };
356
163
 
357
- // src/lanes/grok.ts
358
- function createGrokAdapter(deps = {}) {
359
- return {
360
- kind: "grok",
361
- async probe() {
362
- const cliPath = deps.cliPath ?? "grok";
363
- if (binaryOnPath(cliPath)) {
364
- return { kind: "grok", sdk: "no npm SDK (measured 2026-08-25)", wired: true, via: "cli" };
365
- }
366
- return {
367
- kind: "grok",
368
- sdk: "no npm SDK (measured 2026-08-25)",
369
- wired: false,
370
- reason: `no xAI Grok SDK exists on npm (measured 2026-08-25) and the grok CLI is not on PATH`
371
- };
372
- },
373
- async run(job) {
374
- if (!job.prompt) {
375
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "grok lane requires a prompt" };
376
- }
377
- const started = Date.now();
378
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
379
- const cliPath = deps.cliPath ?? "grok";
380
- try {
381
- const result = spawnCli([cliPath, job.prompt], {
382
- binaryName: cliPath,
383
- packageHint: "the grok CLI (no xAI Grok SDK exists on npm \u2014 measured 2026-08-25)",
384
- timeoutMs,
385
- env: job.env,
386
- cwd: job.cwd
387
- });
388
- return { ...result, durationMs: Date.now() - started };
389
- } catch (err) {
390
- if (err instanceof LaneDependencyMissingError) {
391
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
392
- }
393
- throw err;
394
- }
395
- }
396
- };
164
+ // src/version.ts
165
+ function packageVersion() {
166
+ return package_default.version;
397
167
  }
398
- var init_grok = __esm(() => {
399
- init_types();
400
- });
401
-
402
- // src/lanes/index.ts
403
- var exports_lanes = {};
404
- __export(exports_lanes, {
405
- runLaneJob: () => runLaneJob,
406
- resolveLane: () => resolveLane,
407
- probeLane: () => probeLane,
408
- laneInventory: () => laneInventory,
409
- createGrokAdapter: () => createGrokAdapter,
410
- createCursorAdapter: () => createCursorAdapter,
411
- createCodexAdapter: () => createCodexAdapter,
412
- createClaudeAdapter: () => createClaudeAdapter,
413
- binaryOnPath: () => binaryOnPath,
414
- LaneDependencyMissingError: () => LaneDependencyMissingError,
415
- LaneAdapterShapeError: () => LaneAdapterShapeError,
416
- LANE_KINDS: () => LANE_KINDS2
417
- });
418
- function resolveLane(kind) {
419
- const adapter = adapters.get(kind);
420
- if (!adapter)
421
- throw new Error(`unknown lane ${kind} (expected one of ${LANE_KINDS2.join(", ")})`);
422
- return adapter;
423
- }
424
- async function runLaneJob(job) {
425
- return resolveLane(job.lane).run(job);
426
- }
427
- async function probeLane(kind) {
428
- return resolveLane(kind).probe();
429
- }
430
- async function laneInventory() {
431
- return Promise.all(LANE_KINDS2.map((kind) => probeLane(kind)));
432
- }
433
- var adapters;
434
- var init_lanes = __esm(() => {
435
- init_claude();
436
- init_codex();
437
- init_cursor();
438
- init_grok();
439
- init_types();
440
- init_types();
441
- init_claude();
442
- init_codex();
443
- init_cursor();
444
- init_grok();
445
- adapters = new Map([
446
- ["claude", createClaudeAdapter()],
447
- ["codex", createCodexAdapter()],
448
- ["cursor", createCursorAdapter()],
449
- ["grok", createGrokAdapter()]
450
- ]);
451
- });
452
-
453
- // src/mcp/index.ts
454
- import { createInterface } from "readline";
455
168
 
456
169
  // src/expr.ts
457
170
  class ExprSyntaxError extends Error {
@@ -460,13 +173,6 @@ class ExprSyntaxError extends Error {
460
173
  this.name = "ExprSyntaxError";
461
174
  }
462
175
  }
463
-
464
- class ExprEvalError extends Error {
465
- constructor(message) {
466
- super(message);
467
- this.name = "ExprEvalError";
468
- }
469
- }
470
176
  var COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
471
177
  function tokenize(input) {
472
178
  const tokens = [];
@@ -636,93 +342,11 @@ class Parser {
636
342
  function parseExpr(input) {
637
343
  return new Parser(tokenize(input)).parse();
638
344
  }
639
- function resolvePath(segments, context) {
640
- let current = context;
641
- for (const segment of segments) {
642
- if (current === null || typeof current !== "object")
643
- return;
644
- const record = current;
645
- if (!(segment in record))
646
- return;
647
- current = record[segment];
648
- }
649
- if (typeof current === "number" || typeof current === "string" || typeof current === "boolean") {
650
- return current;
651
- }
652
- return;
653
- }
654
- function evalNode(node, context) {
655
- switch (node.kind) {
656
- case "literal":
657
- return node.value;
658
- case "path":
659
- return resolvePath(node.segments, context);
660
- case "comparison": {
661
- const left = evalNode(node.left, context);
662
- const right = evalNode(node.right, context);
663
- if (left === undefined || right === undefined) {
664
- if (node.op === "==")
665
- return left === undefined && right === undefined;
666
- if (node.op === "!=")
667
- return left !== right;
668
- throw new ExprEvalError("cannot order-compare an unknown path");
669
- }
670
- if (typeof left !== typeof right) {
671
- throw new ExprEvalError(`cannot compare ${JSON.stringify(left)} (${typeof left}) with ${JSON.stringify(right)} (${typeof right})`);
672
- }
673
- switch (node.op) {
674
- case "==":
675
- return left === right;
676
- case "!=":
677
- return left !== right;
678
- case "<":
679
- return left < right;
680
- case "<=":
681
- return left <= right;
682
- case ">":
683
- return left > right;
684
- case ">=":
685
- return left >= right;
686
- default:
687
- throw new ExprEvalError(`unknown operator ${node.op}`);
688
- }
689
- }
690
- case "and": {
691
- const left = evalNode(node.left, context);
692
- const right = evalNode(node.right, context);
693
- if (typeof left !== "boolean" || typeof right !== "boolean") {
694
- throw new ExprEvalError("and requires boolean operands");
695
- }
696
- return left && right;
697
- }
698
- case "or": {
699
- const left = evalNode(node.left, context);
700
- const right = evalNode(node.right, context);
701
- if (typeof left !== "boolean" || typeof right !== "boolean") {
702
- throw new ExprEvalError("or requires boolean operands");
703
- }
704
- return left || right;
705
- }
706
- case "not": {
707
- const operand = evalNode(node.operand, context);
708
- if (typeof operand !== "boolean")
709
- throw new ExprEvalError("not requires a boolean operand");
710
- return !operand;
711
- }
712
- }
713
- }
714
- function evaluateExpr(input, context) {
715
- const ast = parseExpr(input);
716
- const value = evalNode(ast, context);
717
- if (typeof value !== "boolean") {
718
- throw new ExprEvalError(`condition must evaluate to a boolean, got ${typeof value}`);
719
- }
720
- return value;
721
- }
722
345
 
723
346
  // src/graph.ts
724
347
  var GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
725
348
  var LANE_KINDS = ["claude", "codex", "cursor", "grok"];
349
+ var CODEX_EXECUTION_MODES = ["repository", "read-only"];
726
350
  var NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
727
351
  function issue(issues, path, message) {
728
352
  issues.push({ path, message });
@@ -820,6 +444,9 @@ function findCycles(graph) {
820
444
  }
821
445
  function validateGraph(graph) {
822
446
  const issues = [];
447
+ if (graph === null || typeof graph !== "object" || Array.isArray(graph)) {
448
+ return { ok: false, issues: [{ path: "graph", message: "graph must be an object" }] };
449
+ }
823
450
  if (typeof graph.name !== "string" || graph.name.trim() === "") {
824
451
  issue(issues, "name", "graph requires a non-empty name");
825
452
  }
@@ -842,6 +469,24 @@ function validateGraph(graph) {
842
469
  issue(issues, "nodes", "graph requires at least one node");
843
470
  return { ok: false, issues };
844
471
  }
472
+ for (let index = 0;index < graph.nodes.length; index++) {
473
+ const node = graph.nodes[index];
474
+ if (node === null || typeof node !== "object" || Array.isArray(node)) {
475
+ issue(issues, `nodes.${index}`, "node must be an object");
476
+ continue;
477
+ }
478
+ if (!GRAPH_NODE_TYPES.includes(node.type))
479
+ issue(issues, `nodes.${index}.type`, "type must be start, step, decision, while, or end");
480
+ for (const key of ["next", "then", "else", "prompt", "command"]) {
481
+ const value = node[key];
482
+ if (value !== undefined && (typeof value !== "string" || value.trim() === ""))
483
+ issue(issues, `nodes.${index}.${key}`, `${key} must be a non-empty string`);
484
+ }
485
+ if (node.type === "while" && (!Array.isArray(node.body) || node.body.some((id) => typeof id !== "string")))
486
+ issue(issues, `nodes.${index}.body`, "body must be an array of node ids");
487
+ }
488
+ if (issues.length)
489
+ return { ok: false, issues };
845
490
  const starts = graph.nodes.filter((n) => n.type === "start");
846
491
  if (starts.length === 0)
847
492
  issue(issues, "nodes", "graph requires exactly one start node (found none)");
@@ -903,6 +548,8 @@ function nodeEdges(node) {
903
548
  return node.next ? [{ kind: "next", target: node.next }] : [];
904
549
  case "end":
905
550
  return [];
551
+ default:
552
+ return [];
906
553
  }
907
554
  }
908
555
  function validateNode(graph, node, issues) {
@@ -918,8 +565,18 @@ function validateNode(graph, node, issues) {
918
565
  if (node.lane !== undefined && !LANE_KINDS.includes(node.lane)) {
919
566
  issue(issues, `nodes.${node.id}`, `unknown lane ${JSON.stringify(node.lane)} (expected one of ${LANE_KINDS.join(", ")})`);
920
567
  }
921
- if (node.maxRetries !== undefined && (!Number.isInteger(node.maxRetries) || node.maxRetries < 0)) {
922
- issue(issues, `nodes.${node.id}`, "maxRetries must be a non-negative integer");
568
+ if (node.cwd !== undefined && (typeof node.cwd !== "string" || !node.cwd.trim() || node.cwd.includes("\x00"))) {
569
+ issue(issues, `nodes.${node.id}.cwd`, "cwd must be a non-empty path string without NUL characters on the executor");
570
+ }
571
+ if (node.codexMode !== undefined) {
572
+ if (!CODEX_EXECUTION_MODES.includes(node.codexMode)) {
573
+ issue(issues, `nodes.${node.id}.codexMode`, "codexMode must be repository or read-only");
574
+ } else if (node.lane !== "codex" || !node.prompt || node.command !== undefined) {
575
+ issue(issues, `nodes.${node.id}.codexMode`, "codexMode is supported only for a codex prompt step without command");
576
+ }
577
+ }
578
+ if (node.maxRetries !== undefined && (!Number.isInteger(node.maxRetries) || node.maxRetries < 0)) {
579
+ issue(issues, `nodes.${node.id}`, "maxRetries must be a non-negative integer");
923
580
  }
924
581
  if (node.timeoutMs !== undefined && (!Number.isInteger(node.timeoutMs) || node.timeoutMs < 1)) {
925
582
  issue(issues, `nodes.${node.id}`, "timeoutMs must be a positive integer");
@@ -996,1296 +653,1602 @@ function validateNode(graph, node, issues) {
996
653
  }
997
654
  }
998
655
 
999
- // src/secrets.ts
1000
- class SecretsGateError extends Error {
1001
- constructor(message) {
1002
- super(message);
1003
- this.name = "SecretsGateError";
1004
- }
1005
- }
1006
- var p = (...parts) => parts.join("");
1007
- var VALUE_PATTERNS = [
1008
- { detector: "anthropic_api_key", re: new RegExp(p("sk-", "ant-") + "[A-Za-z0-9_-]{10,}") },
1009
- { detector: "openai_api_key", re: new RegExp(p("sk-", "proj-") + "[A-Za-z0-9_-]{10,}") },
1010
- { detector: "npm_token", re: /npm_[A-Za-z0-9]{20,}/ },
1011
- { detector: "github_token", re: /gh[pousr]_[A-Za-z0-9]{20,}/ },
1012
- { detector: "aws_access_key", re: /AKIA[0-9A-Z]{16}/ },
1013
- { detector: "google_api_key", re: /AIza[0-9A-Za-z_-]{20,}/ },
1014
- { detector: "xai_api_key", re: new RegExp(p("xai-") + "[A-Za-z0-9]{20,}") },
1015
- { detector: "codex_token", re: new RegExp(p("ctx7", "sk-") + "[A-Za-z0-9]{10,}") },
1016
- { detector: "private_key_block", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/ },
1017
- { detector: "bearer_token", re: /Bearer [A-Za-z0-9._~+/=-]{20,}/ }
1018
- ];
1019
- var CREDENTIAL_KEY_RE = /^(token|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret[_-]?key|refresh[_-]?token|session[_-]?token|bot[_-]?token)$/i;
1020
- function scanString(value, path, findings) {
1021
- for (const { detector, re } of VALUE_PATTERNS) {
1022
- if (re.test(value)) {
1023
- findings.push({ path, detector });
1024
- return;
1025
- }
1026
- }
656
+ // ../../../../workflows-codex-execution-repair-1/dependency-update-1/standalone/workflows/node_modules/@hasna/contracts/dist/client/transport.js
657
+ import { isIP } from "net";
658
+ import { spawnSync } from "child_process";
659
+ import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync2 } from "fs";
660
+ import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
661
+ import { hostname as osHostname } from "os";
662
+ import { isAbsolute, join as join2 } from "path";
663
+ import { createRequire } from "module";
664
+ import { fileURLToPath as fileURLToPath2 } from "url";
665
+ import { closeSync, fstatSync, lstatSync, openSync, readFileSync, realpathSync, statSync } from "fs";
666
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
667
+ import { basename, dirname, join, relative, sep } from "path";
668
+ import { fileURLToPath, pathToFileURL } from "url";
669
+ function envToken(name) {
670
+ return name.toUpperCase().replace(/-/g, "_");
671
+ }
672
+ function clientTransportEnvKeys(name) {
673
+ const envSegment = envToken(name);
674
+ return {
675
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
676
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
677
+ };
1027
678
  }
1028
- function isTrivial(value) {
1029
- const v = value.trim();
1030
- if (v.length === 0)
1031
- return true;
1032
- const upper = v.toUpperCase();
1033
- if (upper.startsWith("REPLACE") || upper.startsWith("CHANGE") || upper.startsWith("YOUR_") || upper.startsWith("EXAMPLE") || upper.startsWith("PLACEHOLDER"))
1034
- return true;
1035
- if (v.length < 8)
1036
- return true;
1037
- return false;
1038
- }
1039
- function scanForSecrets(value, path = "$") {
1040
- const findings = [];
1041
- if (value === null || value === undefined)
1042
- return findings;
1043
- if (typeof value === "string") {
1044
- scanString(value, path, findings);
1045
- return findings;
1046
- }
1047
- if (Array.isArray(value)) {
1048
- value.forEach((item, index) => {
1049
- findings.push(...scanForSecrets(item, `${path}[${index}]`));
1050
- });
1051
- return findings;
1052
- }
1053
- if (typeof value === "object") {
1054
- const record = value;
1055
- for (const [key, child] of Object.entries(record)) {
1056
- if (CREDENTIAL_KEY_RE.test(key) && typeof child === "string" && !isTrivial(child)) {
1057
- findings.push({ path: `${path}.${key}`, detector: "credential_key_field" });
1058
- } else {
1059
- findings.push(...scanForSecrets(child, `${path}.${key}`));
679
+ function credentialOverrideEnvKey(name) {
680
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
681
+ }
682
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
683
+ function credentialPointerEnvKey(name) {
684
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
685
+ }
686
+ function e(e2, n, r) {
687
+ throw new Error(r ? `No known conditions for "${n}" specifier in "${e2}" package` : `Missing "${n}" specifier in "${e2}" package`);
688
+ }
689
+ function n(n2, i, o, f) {
690
+ let s, u, l = r(n2, o), c = function(e2) {
691
+ let n3 = new Set(["default", ...e2.conditions || []]);
692
+ return e2.unsafe || n3.add(e2.require ? "require" : "import"), e2.unsafe || n3.add(e2.browser ? "browser" : "node"), n3;
693
+ }(f || {}), a = i[l];
694
+ if (a === undefined) {
695
+ let e2, n3, r, t;
696
+ for (t in i)
697
+ n3 && t.length < n3.length || (t[t.length - 1] === "/" && l.startsWith(t) ? (u = l.substring(t.length), n3 = t) : t.length > 1 && (r = t.indexOf("*", 1), ~r && (e2 = RegExp("^" + t.substring(0, r) + "(.*)" + t.substring(1 + r) + "$").exec(l), e2 && e2[1] && (u = e2[1], n3 = t))));
698
+ a = i[n3];
699
+ }
700
+ return a || e(n2, l), s = t(a, c), s || e(n2, l, 1), u && function(e2, n3) {
701
+ let r, t = 0, i2 = e2.length, o2 = /[*]/g, f2 = /[/]$/;
702
+ for (;t < i2; t++)
703
+ e2[t] = o2.test(r = e2[t]) ? r.replace(o2, n3) : f2.test(r) ? r + n3 : r;
704
+ }(s, u), s;
705
+ }
706
+ function r(e2, n2, r2) {
707
+ if (e2 === n2 || n2 === ".")
708
+ return ".";
709
+ let t = e2 + "/", i = t.length, o = n2.slice(0, i) === t, f = o ? n2.slice(i) : n2;
710
+ return f[0] === "#" ? f : o || !r2 ? f.slice(0, 2) === "./" ? f : "./" + f : f;
711
+ }
712
+ function t(e2, n2, r2) {
713
+ if (e2) {
714
+ if (typeof e2 == "string")
715
+ return r2 && r2.add(e2), [e2];
716
+ let i, o;
717
+ if (Array.isArray(e2)) {
718
+ for (o = r2 || new Set, i = 0;i < e2.length; i++)
719
+ t(e2[i], n2, o);
720
+ if (!r2 && o.size)
721
+ return [...o];
722
+ } else
723
+ for (i in e2)
724
+ if (n2.has(i))
725
+ return t(e2[i], n2, r2);
726
+ }
727
+ }
728
+ function o(e2, r2, t2) {
729
+ let i, o2 = e2.exports;
730
+ if (o2) {
731
+ if (typeof o2 == "string")
732
+ o2 = { ".": o2 };
733
+ else
734
+ for (i in o2) {
735
+ i[0] !== "." && (o2 = { ".": o2 });
736
+ break;
1060
737
  }
1061
- }
738
+ return n(e2.name, o2, r2 || ".", t2);
1062
739
  }
1063
- return findings;
1064
- }
1065
- function assertNoSecrets(value, what) {
1066
- const findings = scanForSecrets(value);
1067
- if (findings.length === 0)
1068
- return;
1069
- const detail = findings.map((f) => `${f.path} (${f.detector})`).join(", ");
1070
- throw new SecretsGateError(`secrets write-gate refused to persist ${what}: credential-shaped value at ${detail}`);
1071
740
  }
1072
-
1073
- // src/session.ts
1074
- import { createHash } from "crypto";
1075
- import { readFileSync, statSync } from "fs";
1076
- import { isAbsolute, join } from "path";
1077
- var DEFAULT_MAX_ATTEMPTS = 3;
1078
- function repairTornRuns(store, wal, opts = {}) {
1079
- const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
1080
- const replay = wal.replay();
1081
- const liveClaims = replay.liveClaims(opts.now);
1082
- const report = { interrupted: 0, requeued: 0, failed: 0 };
1083
- const running = store.listRuns({ status: "running", limit: 1000 });
1084
- for (const run of running) {
1085
- if (liveClaims.has(run.id))
1086
- continue;
1087
- report.interrupted++;
1088
- const nodes = store.listRunNodes(run.id);
1089
- if (run.attempts < maxAttempts) {
1090
- store.bumpAttempts(run.id);
1091
- store.setRunStatus(run.id, "interrupted", {
1092
- error: `torn run interrupted after claim loss (attempt ${run.attempts + 1}/${maxAttempts})`
1093
- });
1094
- for (const node of nodes) {
1095
- if (node.status === "running") {
1096
- store.setRunNodeStatus(node.id, "pending", { error: undefined });
1097
- }
1098
- }
1099
- report.requeued++;
1100
- } else {
1101
- store.setRunStatus(run.id, "failed", {
1102
- error: `torn run failed after ${maxAttempts} interrupted attempts`
1103
- });
1104
- for (const node of nodes) {
1105
- if (node.status === "running") {
1106
- store.setRunNodeStatus(node.id, "failed", { error: "run interrupted beyond retry budget" });
1107
- }
741
+ var PACKAGE = "@hasna/secrets";
742
+ var MAX_PACKAGE_BYTES = 1024 * 1024;
743
+ function entryPoint(directory) {
744
+ const root = realpathSync(directory);
745
+ const fd = openSync(join(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
746
+ let pkg;
747
+ try {
748
+ const stat = fstatSync(fd);
749
+ if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES)
750
+ throw new Error("Invalid Secrets package metadata");
751
+ pkg = JSON.parse(readFileSync(fd, "utf8"));
752
+ } finally {
753
+ closeSync(fd);
754
+ }
755
+ if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== PACKAGE) {
756
+ throw new Error("Invalid Secrets package identity");
757
+ }
758
+ const target = o(pkg, ".")?.[0];
759
+ if (typeof target !== "string" || !target.startsWith("./"))
760
+ throw new Error("Missing Secrets import export");
761
+ const parts = target.slice(2).split("/");
762
+ if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
763
+ throw new Error("Invalid Secrets import export");
764
+ }
765
+ const file = realpathSync(join(root, ...parts));
766
+ const within = relative(root, file);
767
+ if (!within || within === ".." || within.startsWith(`..${sep}`) || !statSync(file).isFile()) {
768
+ throw new Error("Secrets import export escapes its package or is not a file");
769
+ }
770
+ return pathToFileURL(file).href;
771
+ }
772
+ function resolveInstalledSecrets(parentUrl) {
773
+ let directory = dirname(fileURLToPath(parentUrl));
774
+ for (;; ) {
775
+ if (basename(directory) !== "node_modules") {
776
+ const candidate = join(directory, "node_modules", PACKAGE);
777
+ let absent = false;
778
+ try {
779
+ lstatSync(candidate);
780
+ } catch (error) {
781
+ if (error.code !== "ENOENT")
782
+ throw error;
783
+ absent = true;
1108
784
  }
1109
- report.failed++;
785
+ if (!absent)
786
+ return entryPoint(candidate);
1110
787
  }
788
+ const parent = dirname(directory);
789
+ if (parent === directory)
790
+ throw new Error("Secrets SDK is not installed for this consumer");
791
+ directory = parent;
1111
792
  }
1112
- return report;
1113
793
  }
1114
- function inputHash(payload) {
1115
- return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
794
+
795
+ class CredentialResolutionError extends Error {
796
+ appName;
797
+ attempted;
798
+ constructor(appName, message, attempted) {
799
+ super(message);
800
+ this.name = "CredentialResolutionError";
801
+ this.appName = appName;
802
+ this.attempted = attempted;
803
+ }
1116
804
  }
1117
- function memoKey(graphName, nodeId, input) {
1118
- return `${graphName}:${nodeId}:${inputHash(input)}`;
805
+
806
+ class CredentialFileUnsafeError extends Error {
807
+ path;
808
+ constructor(path, reason) {
809
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
810
+ this.name = "CredentialFileUnsafeError";
811
+ this.path = path;
812
+ }
813
+ }
814
+ var HASNA_HOME_ENV_KEY = "HASNA_HOME";
815
+ var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
816
+ var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
817
+ var HASNA_HOME_DIR = ".hasna";
818
+ var CONFIG_SUBDIR = "config";
819
+ var CREDENTIALS_FILE = "credentials";
820
+ var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
821
+ var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
822
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
823
+ var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
824
+ var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
825
+ var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
826
+ var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
827
+ var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
828
+ var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
829
+ function homeDir(env) {
830
+ const home = env.HOME?.trim();
831
+ return home ? home : null;
832
+ }
833
+ function absoluteOverride(env, key) {
834
+ const value = env[key]?.trim();
835
+ return value && isAbsolute(value) ? value : null;
836
+ }
837
+ function hasnaHomeDir(env) {
838
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
839
+ if (override)
840
+ return override;
841
+ const home = homeDir(env);
842
+ return home ? join2(home, HASNA_HOME_DIR) : null;
843
+ }
844
+ function appConfigDir(name, env) {
845
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
846
+ if (configRoot)
847
+ return join2(configRoot, name);
848
+ const root = hasnaHomeDir(env);
849
+ return root ? join2(root, name, CONFIG_SUBDIR) : null;
850
+ }
851
+ function credentialDiskSourceList(name, env, profile = null) {
852
+ if (!SAFE_APP_SLUG.test(name))
853
+ return [];
854
+ const directory = appConfigDir(name, env);
855
+ if (!directory)
856
+ return [];
857
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
858
+ return [{ path: join2(directory, file), tier: "disk" }];
1119
859
  }
1120
- function tryMemoHit(store, graphName, nodeId, input) {
1121
- const key = memoKey(graphName, nodeId, input);
1122
- const memo = store.memoGet(key);
1123
- if (!memo)
1124
- return;
1125
- store.memoHit(key);
1126
- return memo;
860
+ function credentialDiskSources(name, env) {
861
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
1127
862
  }
1128
- function recordMemo(store, graphName, nodeId, input, output) {
1129
- const key = memoKey(graphName, nodeId, input);
1130
- store.memoPut(key, graphName, nodeId, inputHash(input), JSON.stringify(output));
863
+ function profileDiskSources(name, env, profile) {
864
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
1131
865
  }
1132
- function memoWatchFingerprint(graph, dataDir) {
1133
- if (!graph.memoWatch || graph.memoWatch.length === 0)
1134
- return;
1135
- const entries = [];
1136
- for (const pattern of graph.memoWatch) {
1137
- if (hasGlobMagic(pattern)) {
1138
- const matches = [...new Bun.Glob(pattern).scanSync({ cwd: dataDir, onlyFiles: true })].sort();
1139
- for (const match of matches) {
1140
- const path2 = isAbsolute(match) ? match : join(dataDir, match);
1141
- entries.push(fingerprintFile(pattern, path2));
866
+ function parseEnvFile(text) {
867
+ const values = new Map;
868
+ const unusable = new Set;
869
+ for (const rawLine of text.split(/\r?\n/)) {
870
+ const line = rawLine.trim();
871
+ if (line.length === 0 || line.startsWith("#"))
872
+ continue;
873
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
874
+ const equals = withoutExport.indexOf("=");
875
+ if (equals <= 0)
876
+ continue;
877
+ const key = withoutExport.slice(0, equals).trim();
878
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
879
+ continue;
880
+ let value = withoutExport.slice(equals + 1).trim();
881
+ const quote = value[0];
882
+ if (quote === '"' || quote === "'") {
883
+ if (value.length < 2 || !value.endsWith(quote)) {
884
+ unusable.add(key);
885
+ continue;
1142
886
  }
887
+ value = value.slice(1, -1);
888
+ }
889
+ if (value.trim().length === 0) {
890
+ unusable.add(key);
1143
891
  continue;
1144
892
  }
1145
- const path = isAbsolute(pattern) ? pattern : join(dataDir, pattern);
1146
- entries.push(fingerprintFile(pattern, path));
893
+ if (values.has(key) && values.get(key) !== value)
894
+ unusable.add(key);
895
+ values.set(key, value);
1147
896
  }
1148
- return entries;
897
+ return { values, unusable };
898
+ }
899
+ function configFileModeAllowed(mode) {
900
+ const permissions = mode & 4095;
901
+ return permissions === 256 || permissions === 384;
1149
902
  }
1150
- function hasGlobMagic(pattern) {
1151
- return /[*?[\]{}]/.test(pattern);
903
+ function configFileReadsCoherent(before, after) {
904
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
1152
905
  }
1153
- function fingerprintFile(pattern, path) {
906
+ function readAppConfigFile(path) {
907
+ const unsafe = (reason) => {
908
+ throw new CredentialFileUnsafeError(path, reason);
909
+ };
910
+ let fd = -1;
1154
911
  try {
1155
- const stat = statSync(path);
1156
- if (!stat.isFile()) {
1157
- return { pattern, path, missing: true, mtimeMs: null, size: null, sha256: null };
912
+ fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
913
+ } catch (error) {
914
+ const code = error.code;
915
+ if (code === "ENOENT" || code === "ENOTDIR")
916
+ return null;
917
+ if (code === "ELOOP")
918
+ unsafe("the path is a symlink");
919
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
920
+ }
921
+ try {
922
+ const before = fstatSync2(fd);
923
+ if (!before.isFile())
924
+ unsafe("the path is not a regular file");
925
+ if (!configFileModeAllowed(before.mode)) {
926
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
927
+ }
928
+ const uid = process.getuid?.() ?? process.geteuid?.();
929
+ if (uid !== undefined && before.uid !== uid)
930
+ unsafe("the file is not owned by the current user");
931
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES)
932
+ unsafe("the file exceeds the size limit");
933
+ const bytes = readFileSync2(fd);
934
+ const after = fstatSync2(fd);
935
+ if (!configFileReadsCoherent(before, after)) {
936
+ unsafe("the file changed while being read");
937
+ }
938
+ return parseEnvFile(bytes.toString("utf8"));
939
+ } finally {
940
+ if (fd !== -1)
941
+ closeSync2(fd);
942
+ }
943
+ }
944
+ function readCredentialFile(path, apiKeyKeys, pointerKey) {
945
+ const parsed = readAppConfigFile(path);
946
+ if (!parsed)
947
+ return null;
948
+ for (const key of [...apiKeyKeys, pointerKey]) {
949
+ if (parsed.unusable.has(key)) {
950
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
951
+ }
952
+ }
953
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
954
+ const pointer = parsed.values.get(pointerKey)?.trim();
955
+ if (pointer !== undefined) {
956
+ if (!VAULT_POINTER_SHAPE.test(pointer))
957
+ throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
958
+ if (values.length)
959
+ throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
960
+ return { apiKey: "", pointerVaultKey: pointer };
961
+ }
962
+ if (new Set(values).size > 1) {
963
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
964
+ }
965
+ return values[0] === undefined ? null : { apiKey: values[0] };
966
+ }
967
+ var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
968
+ function appConfigDiskValue(name, env, keys, profile = null) {
969
+ if (profile !== null && !SAFE_PROFILE.test(profile)) {
970
+ throw new CredentialResolutionError(name, "The selected configuration profile is invalid.", ["profile"]);
971
+ }
972
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
973
+ if (wanted.length === 0)
974
+ return null;
975
+ for (const path of profileDiskSources(name, env, profile)) {
976
+ const parsed = readAppConfigFile(path);
977
+ if (!parsed)
978
+ continue;
979
+ if (wanted.some((key) => parsed.unusable.has(key))) {
980
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
981
+ }
982
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
983
+ if (new Set(values).size > 1)
984
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
985
+ for (const key of wanted) {
986
+ if (parsed.unusable.has(key))
987
+ return { key, value: "", path, unusable: true };
988
+ const value = parsed.values.get(key)?.trim();
989
+ if (value)
990
+ return { key, value, path };
1158
991
  }
1159
- const content = readFileSync(path);
1160
- return {
1161
- pattern,
1162
- path,
1163
- missing: false,
1164
- mtimeMs: stat.mtimeMs,
1165
- size: content.byteLength,
1166
- sha256: createHash("sha256").update(content).digest("hex")
1167
- };
1168
- } catch {
1169
- return { pattern, path, missing: true, mtimeMs: null, size: null, sha256: null };
1170
992
  }
993
+ return null;
1171
994
  }
1172
- function findRunByIdempotencyKey(store, key) {
1173
- const runs = store.listRuns({ limit: 1000 });
1174
- for (const run of runs) {
1175
- try {
1176
- const context = JSON.parse(run.contextJson);
1177
- if (context?.__wf?.idempotencyKey === key)
1178
- return run;
1179
- } catch {}
995
+ function assertUsableCredential(appName, source, value) {
996
+ if (VAULT_POINTER_SHAPE.test(value)) {
997
+ throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
998
+ }
999
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
1000
+ return;
1001
+ throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
1002
+ }
1003
+ var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
1004
+ var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
1005
+ var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
1006
+ function sealCredential(fields) {
1007
+ const { apiKey } = fields;
1008
+ const visible = {
1009
+ tier: fields.tier,
1010
+ source: fields.source,
1011
+ deliberate: fields.deliberate,
1012
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
1013
+ warning: fields.warning
1014
+ };
1015
+ const sealed = { ...visible };
1016
+ Object.defineProperty(sealed, "apiKey", {
1017
+ value: apiKey,
1018
+ enumerable: false,
1019
+ writable: false,
1020
+ configurable: false
1021
+ });
1022
+ if (fields.pointerVaultKey !== undefined) {
1023
+ Object.defineProperty(sealed, "pointerVaultKey", {
1024
+ value: fields.pointerVaultKey,
1025
+ enumerable: false,
1026
+ writable: false,
1027
+ configurable: false
1028
+ });
1180
1029
  }
1181
- return;
1030
+ Object.defineProperty(sealed, INSPECT_CUSTOM, {
1031
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
1032
+ enumerable: false,
1033
+ writable: false,
1034
+ configurable: false
1035
+ });
1036
+ Object.defineProperty(sealed, CREDENTIAL_SEAL, {
1037
+ value: true,
1038
+ enumerable: false,
1039
+ writable: false,
1040
+ configurable: false
1041
+ });
1042
+ return Object.freeze(sealed);
1043
+ }
1044
+ function isSealedCredential(credential) {
1045
+ return credential[CREDENTIAL_SEAL] === true;
1046
+ }
1047
+ function explicitCredential(appName, apiKey) {
1048
+ const source = "explicit apiKey option";
1049
+ assertUsableCredential(appName, source, apiKey);
1050
+ return sealCredential({
1051
+ apiKey,
1052
+ tier: "argument",
1053
+ source,
1054
+ deliberate: true,
1055
+ diskCandidates: [],
1056
+ warning: null
1057
+ });
1182
1058
  }
1183
-
1184
- // src/daemon.ts
1185
- init_types();
1186
- var DEFAULT_LEASE_TTL_MS = 120000;
1187
- var DEFAULT_MAX_ATTEMPTS2 = 3;
1188
- var DEFAULT_MAX_DISPATCH = 4;
1189
- var DEFAULT_MAX_STEPS = 25;
1190
- var DEFAULT_TIMEOUT_MS = 120000;
1191
- function nowIso() {
1192
- return new Date().toISOString();
1193
- }
1194
- function stripRunState(context) {
1195
- const { __wf: _wf, steps: _steps, ...user } = context;
1196
- return user;
1197
- }
1198
- function runCommand(job) {
1199
- if (!job.command) {
1200
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "no command" };
1201
- }
1202
- const started = Date.now();
1203
- const timeoutMs = job.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1204
- const proc = Bun.spawnSync({
1205
- cmd: ["/bin/sh", "-c", job.command],
1206
- cwd: job.cwd,
1207
- env: { ...process.env, ...job.env ?? {} },
1208
- stdout: "pipe",
1209
- stderr: "pipe",
1210
- timeout: timeoutMs
1059
+ function validateAndSealResolvedCredential(appName, credential) {
1060
+ const apiKey = credential.apiKey;
1061
+ assertUsableCredential(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
1062
+ if (!isSealedCredential(credential)) {
1063
+ return sealCredential({
1064
+ apiKey,
1065
+ tier: "argument",
1066
+ source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
1067
+ deliberate: true,
1068
+ diskCandidates: [],
1069
+ warning: null
1070
+ });
1071
+ }
1072
+ return sealCredential({
1073
+ apiKey,
1074
+ tier: credential.tier,
1075
+ source: credential.source,
1076
+ deliberate: credential.deliberate,
1077
+ diskCandidates: credential.diskCandidates,
1078
+ warning: credential.warning,
1079
+ ...credential.pointerVaultKey !== undefined ? { pointerVaultKey: credential.pointerVaultKey } : {}
1211
1080
  });
1212
- const durationMs = Date.now() - started;
1213
- const stdout = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
1214
- const stderr = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
1215
- const output = stderr ? `${stdout}
1216
- ${stderr}`.trim() : stdout.trim();
1217
- const exitCode = proc.exitCode ?? (proc.signalCode ? 1 : 1);
1218
- return { ok: exitCode === 0, exitCode, output, durationMs };
1219
1081
  }
1220
-
1221
- class WorkflowsDaemon {
1222
- store;
1223
- wal;
1224
- worker;
1225
- leaseTtlMs;
1226
- maxAttempts;
1227
- maxDispatchPerCycle;
1228
- maxStepsPerCycle;
1229
- time;
1230
- laneRunner;
1231
- env;
1232
- leases = new Map;
1233
- fencingCounter = new Map;
1234
- graphCache = new Map;
1235
- isolatedRunId = null;
1236
- constructor(store, wal, options = {}) {
1237
- this.store = store;
1238
- this.wal = wal;
1239
- this.worker = options.worker ?? "local";
1240
- this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
1241
- this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS2;
1242
- this.maxDispatchPerCycle = options.maxDispatchPerCycle ?? DEFAULT_MAX_DISPATCH;
1243
- this.maxStepsPerCycle = options.maxStepsPerCycle ?? DEFAULT_MAX_STEPS;
1244
- this.time = options.time ?? Date.now;
1245
- this.env = options.env ?? {};
1246
- this.laneRunner = options.laneRunner ?? defaultLaneRunner;
1247
- this.isolatedRunId = options.isolatedRunId ?? null;
1248
- this.replayRegisteredGraphs();
1249
- }
1250
- isolateRun(runId) {
1251
- this.isolatedRunId = runId;
1252
- }
1253
- replayRegisteredGraphs() {
1254
- const entries = this.wal.replay().entries;
1255
- for (const entry of entries) {
1256
- const op = entry.op;
1257
- if (op.op === "graph_registered") {
1258
- this.graphCache.set(op.name, op.graph);
1259
- }
1260
- }
1082
+ function firstEnvValue(env, keys) {
1083
+ for (const key of keys) {
1084
+ if (!Object.prototype.hasOwnProperty.call(env, key))
1085
+ continue;
1086
+ const value = env[key]?.trim();
1087
+ if (value)
1088
+ return { key, value };
1261
1089
  }
1262
- liveLeases() {
1263
- const now = this.time();
1264
- for (const [runId, lease] of this.leases) {
1265
- if (lease.expiresAtMs <= now)
1266
- this.leases.delete(runId);
1090
+ return null;
1091
+ }
1092
+ var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
1093
+ function isAmbientEnvironment(env) {
1094
+ return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
1095
+ }
1096
+ function defaultKeychainRunner(argv) {
1097
+ const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
1098
+ encoding: "utf8",
1099
+ stdio: ["ignore", "pipe", "pipe"],
1100
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
1101
+ });
1102
+ return {
1103
+ status: result.status,
1104
+ stdout: result.stdout ?? "",
1105
+ stderr: result.error ? result.error.message : result.stderr ?? ""
1106
+ };
1107
+ }
1108
+ function keychainTierEnabled(env, options) {
1109
+ if ((options.platform ?? process.platform) !== "darwin")
1110
+ return false;
1111
+ if (options.enabled !== undefined)
1112
+ return options.enabled;
1113
+ return options.run !== undefined || isAmbientEnvironment(env);
1114
+ }
1115
+ function keychainAccount(env, options) {
1116
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
1117
+ if (station)
1118
+ return station;
1119
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
1120
+ if (host)
1121
+ return host;
1122
+ const user = env.USER?.trim();
1123
+ return user || null;
1124
+ }
1125
+ function keychainFailureHint(text) {
1126
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
1127
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
1128
+ return clean ? `: ${clean}` : "";
1129
+ }
1130
+ function readKeychainItem(name, env, kind, options) {
1131
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
1132
+ return null;
1133
+ const account = keychainAccount(env, options);
1134
+ if (!account)
1135
+ return null;
1136
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
1137
+ const source = `keychain:${service}@${account}`;
1138
+ const run = options.run ?? defaultKeychainRunner;
1139
+ let result;
1140
+ try {
1141
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
1142
+ } catch (error) {
1143
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
1144
+ throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
1145
+ }
1146
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
1147
+ return null;
1148
+ if (result.status !== 0) {
1149
+ throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
1150
+ }
1151
+ const value = result.stdout.trim();
1152
+ if (!value) {
1153
+ throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
1154
+ }
1155
+ return { value, source };
1156
+ }
1157
+ function keychainConfigValue(name, env, options = {}) {
1158
+ return readKeychainItem(name, env, "api-url", options);
1159
+ }
1160
+ function snapshotClientEnvironment(name, env) {
1161
+ const keys = clientTransportEnvKeys(name);
1162
+ const ambient = isAmbientEnvironment(env);
1163
+ const snapshot = Object.create(null);
1164
+ for (const key of [
1165
+ ...keys.apiUrlKeys,
1166
+ ...keys.apiKeyKeys,
1167
+ credentialOverrideEnvKey(name),
1168
+ credentialPointerEnvKey(name),
1169
+ CREDENTIAL_PROFILE_ENV_KEY,
1170
+ "HOME",
1171
+ HASNA_HOME_ENV_KEY,
1172
+ HASNA_CONFIG_HOME_ENV_KEY,
1173
+ KEYCHAIN_STATION_ENV_KEY,
1174
+ "USER"
1175
+ ]) {
1176
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
1177
+ if (!descriptor)
1178
+ continue;
1179
+ if (!("value" in descriptor)) {
1180
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
1267
1181
  }
1268
- return this.leases;
1182
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
1183
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
1184
+ }
1185
+ snapshot[key] = descriptor.value;
1269
1186
  }
1270
- claim(runId) {
1271
- const now = this.time();
1272
- const existing = this.leases.get(runId);
1273
- if (existing && existing.worker !== this.worker && existing.expiresAtMs > now) {
1274
- return null;
1187
+ if (ambient) {
1188
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
1189
+ value: true,
1190
+ enumerable: false,
1191
+ writable: false,
1192
+ configurable: false
1193
+ });
1194
+ }
1195
+ return Object.freeze(snapshot);
1196
+ }
1197
+ function resolveCredential(name, env, options = {}) {
1198
+ env = snapshotClientEnvironment(name, env);
1199
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
1200
+ const diskPaths = credentialDiskSources(name, env);
1201
+ if (options.apiKey !== undefined) {
1202
+ const explicitKey = options.apiKey.trim();
1203
+ if (!explicitKey) {
1204
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
1205
+ }
1206
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
1207
+ return sealCredential({
1208
+ apiKey: explicitKey,
1209
+ tier: "argument",
1210
+ source: "explicit apiKey argument",
1211
+ deliberate: true,
1212
+ diskCandidates: diskPaths,
1213
+ warning: null
1214
+ });
1215
+ }
1216
+ const overrideKeyName = credentialOverrideEnvKey(name);
1217
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
1218
+ if (overrideRaw !== undefined) {
1219
+ const override = overrideRaw.trim();
1220
+ if (!override) {
1221
+ throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
1222
+ }
1223
+ assertUsableCredential(name, overrideKeyName, override);
1224
+ return sealCredential({
1225
+ apiKey: override,
1226
+ tier: "override",
1227
+ source: overrideKeyName,
1228
+ deliberate: true,
1229
+ diskCandidates: diskPaths,
1230
+ warning: null
1231
+ });
1232
+ }
1233
+ const pointerKeyName = credentialPointerEnvKey(name);
1234
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
1235
+ if (pointerRaw !== undefined) {
1236
+ const pointer = pointerRaw.trim();
1237
+ if (!pointer) {
1238
+ throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
1239
+ }
1240
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
1241
+ throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
1242
+ }
1243
+ return sealCredential({
1244
+ apiKey: "",
1245
+ pointerVaultKey: pointer,
1246
+ tier: "pointer",
1247
+ source: pointerKeyName,
1248
+ deliberate: true,
1249
+ diskCandidates: diskPaths,
1250
+ warning: null
1251
+ });
1252
+ }
1253
+ if (options.profile !== undefined && !options.profile.trim()) {
1254
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
1255
+ }
1256
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
1257
+ if (profileRaw !== undefined && !profileRaw.trim()) {
1258
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
1259
+ }
1260
+ const profile = options.profile?.trim() || profileRaw?.trim();
1261
+ if (profile) {
1262
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
1263
+ if (!SAFE_PROFILE.test(profile)) {
1264
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
1265
+ }
1266
+ const paths = profileDiskSources(name, env, profile);
1267
+ for (const path of paths) {
1268
+ const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
1269
+ if (value) {
1270
+ if (!value.pointerVaultKey)
1271
+ assertUsableCredential(name, path, value.apiKey);
1272
+ return sealCredential({
1273
+ ...value,
1274
+ tier: value.pointerVaultKey ? "pointer" : "profile",
1275
+ source: path,
1276
+ deliberate: true,
1277
+ diskCandidates: paths,
1278
+ warning: null
1279
+ });
1280
+ }
1275
1281
  }
1276
- const liveClaims = this.wal.replay().liveClaims(now);
1277
- const foreign = liveClaims.get(runId);
1278
- if (foreign && foreign.worker !== this.worker && foreign.expiresAtMs > now) {
1279
- return null;
1282
+ throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} or ${pointerKeyName} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
1283
+ }
1284
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
1285
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
1286
+ if (blankEnv) {
1287
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
1288
+ }
1289
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
1290
+ throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
1291
+ }
1292
+ const envHit = firstEnvValue(env, apiKeyKeys);
1293
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
1294
+ if (keychainHit) {
1295
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
1296
+ const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain is re-read on every call while ` + `an environment variable is a snapshot. Reconcile them \u2014 a rotation that updated only one leaves ` + `the other to fail 401 wherever it is loaded first.` : null;
1297
+ return sealCredential({
1298
+ apiKey: keychainHit.value,
1299
+ tier: "keychain",
1300
+ source: keychainHit.source,
1301
+ deliberate: false,
1302
+ diskCandidates: diskPaths,
1303
+ warning
1304
+ });
1305
+ }
1306
+ const diskSourceList = credentialDiskSourceList(name, env, null);
1307
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
1308
+ if (diskHits.length > 0) {
1309
+ const winner = diskHits[0];
1310
+ if (winner.value.pointerVaultKey) {
1311
+ return sealCredential({
1312
+ ...winner.value,
1313
+ tier: "pointer",
1314
+ source: winner.src.path,
1315
+ deliberate: false,
1316
+ diskCandidates: diskPaths,
1317
+ warning: null
1318
+ });
1280
1319
  }
1281
- const fencing = (this.fencingCounter.get(runId) ?? 0) + 1;
1282
- this.fencingCounter.set(runId, fencing);
1283
- const lease = { runId, worker: this.worker, expiresAtMs: now + this.leaseTtlMs, fencing };
1284
- this.leases.set(runId, lease);
1285
- this.wal.append({
1286
- op: "claim_acquired",
1287
- runId,
1288
- worker: this.worker,
1289
- expiresAt: new Date(lease.expiresAtMs).toISOString(),
1290
- fencing,
1291
- at: nowIso()
1320
+ assertUsableCredential(name, winner.src.path, winner.value.apiKey);
1321
+ const divergentSources = [
1322
+ ...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
1323
+ ...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
1324
+ ];
1325
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
1326
+ return sealCredential({
1327
+ apiKey: winner.value.apiKey,
1328
+ tier: winner.src.tier,
1329
+ source: winner.src.path,
1330
+ deliberate: false,
1331
+ diskCandidates: diskPaths,
1332
+ warning
1333
+ });
1334
+ }
1335
+ if (envHit) {
1336
+ assertUsableCredential(name, envHit.key, envHit.value);
1337
+ return sealCredential({
1338
+ apiKey: envHit.value,
1339
+ tier: "env",
1340
+ source: envHit.key,
1341
+ deliberate: false,
1342
+ diskCandidates: diskPaths,
1343
+ warning: null
1292
1344
  });
1293
- return lease;
1294
- }
1295
- heartbeat(runId, fencing) {
1296
- const lease = this.leases.get(runId);
1297
- if (!lease || lease.fencing !== fencing)
1298
- return false;
1299
- lease.expiresAtMs = this.time() + this.leaseTtlMs;
1300
- return true;
1301
- }
1302
- release(runId, fencing) {
1303
- const lease = this.leases.get(runId);
1304
- if (!lease || lease.fencing !== fencing)
1305
- return false;
1306
- this.leases.delete(runId);
1307
- this.wal.append({ op: "claim_released", runId, fencing, at: nowIso() });
1308
- return true;
1309
- }
1310
- async reap() {
1311
- const report = { expired: 0, interrupted: 0, requeued: 0, failed: 0, dispatched: 0, advanced: 0, completed: 0 };
1312
- const now = this.time();
1313
- for (const [runId, lease] of this.leases) {
1314
- if (lease.expiresAtMs <= now) {
1315
- this.leases.delete(runId);
1316
- report.expired++;
1345
+ }
1346
+ return null;
1347
+ }
1348
+ var requireSecretsSdk = createRequire(import.meta.url);
1349
+ async function completePointerCredential(name, pointerResolution, env = process.env) {
1350
+ const secretsEnv = snapshotClientEnvironment("secrets", env);
1351
+ const vaultKey = pointerResolution.pointerVaultKey;
1352
+ const pointerEnvKey = pointerResolution.source;
1353
+ if (!vaultKey) {
1354
+ throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
1355
+ }
1356
+ if (name === "secrets") {
1357
+ throw new CredentialResolutionError(name, "The Secrets bootstrap credential cannot reference the same hosted vault; configure an independent bootstrap provider.", [pointerEnvKey]);
1358
+ }
1359
+ let secretsSdk;
1360
+ try {
1361
+ const sdkUrl = resolveInstalledSecrets(import.meta.url);
1362
+ secretsSdk = requireSecretsSdk(fileURLToPath2(sdkUrl));
1363
+ } catch {
1364
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
1365
+ }
1366
+ let client;
1367
+ try {
1368
+ const bootstrap = resolveCredential("secrets", secretsEnv);
1369
+ if (!bootstrap || bootstrap.tier === "pointer") {
1370
+ throw new CredentialResolutionError("secrets", "The Secrets vault requires an independent, non-reference bootstrap credential.", bootstrap ? [bootstrap.source] : []);
1371
+ }
1372
+ client = secretsSdk.createSecretsClientFromEnv(secretsEnv);
1373
+ } catch {
1374
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
1375
+ }
1376
+ let secret;
1377
+ try {
1378
+ secret = await client.getSecret({ key: vaultKey });
1379
+ } catch {
1380
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
1381
+ }
1382
+ const value = secret.value;
1383
+ if (!value) {
1384
+ throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
1385
+ }
1386
+ assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
1387
+ return sealCredential({
1388
+ apiKey: value,
1389
+ tier: "pointer",
1390
+ source: `${pointerEnvKey} -> vault:${vaultKey}`,
1391
+ deliberate: pointerResolution.deliberate,
1392
+ diskCandidates: pointerResolution.diskCandidates,
1393
+ warning: null
1394
+ });
1395
+ }
1396
+ var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
1397
+ var DEFAULT_AUTHORITY_SOURCE = "default";
1398
+ function defaultFleetGatewayBaseUrl(name) {
1399
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
1400
+ }
1401
+ var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
1402
+ var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
1403
+ function isValidDnsDomain(value) {
1404
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
1405
+ return false;
1406
+ }
1407
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
1408
+ }
1409
+ function validateAppSlug(name) {
1410
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
1411
+ throw new Error("App name must be one lowercase DNS label.");
1412
+ }
1413
+ return name;
1414
+ }
1415
+ function rawAuthority(value) {
1416
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
1417
+ if (!match)
1418
+ throw new Error("API URL must be absolute.");
1419
+ const afterScheme = value.slice(match[0].length);
1420
+ const boundary = afterScheme.search(/[/?#]/);
1421
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
1422
+ if (!authority)
1423
+ throw new Error("API URL must include a hostname.");
1424
+ return authority;
1425
+ }
1426
+ function assertCanonicalPort(port) {
1427
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
1428
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
1429
+ }
1430
+ const numericPort = Number(port);
1431
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
1432
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
1433
+ }
1434
+ }
1435
+ function canonicalAuthorityHostname(authority) {
1436
+ let rawHostname;
1437
+ if (authority.startsWith("[")) {
1438
+ const closingBracket = authority.indexOf("]");
1439
+ if (closingBracket === -1) {
1440
+ throw new Error("API URL authority must contain a canonical hostname.");
1441
+ }
1442
+ rawHostname = authority.slice(0, closingBracket + 1);
1443
+ const portSuffix = authority.slice(closingBracket + 1);
1444
+ if (portSuffix) {
1445
+ if (!portSuffix.startsWith(":")) {
1446
+ throw new Error("API URL authority must contain a canonical hostname and port.");
1317
1447
  }
1448
+ assertCanonicalPort(portSuffix.slice(1));
1318
1449
  }
1319
- if (this.isolatedRunId === null) {
1320
- const repair = repairTornRuns(this.store, this.wal, { maxAttempts: this.maxAttempts, now });
1321
- report.interrupted += repair.interrupted;
1322
- report.requeued += repair.requeued;
1323
- report.failed += repair.failed;
1450
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
1451
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
1324
1452
  }
1325
- const pending = this.store.listRuns({ status: "pending", limit: this.maxDispatchPerCycle }).concat(this.store.listRuns({ status: "interrupted", limit: this.maxDispatchPerCycle })).sort((a, b) => a.createdAt === b.createdAt ? a.id < b.id ? 1 : -1 : a.createdAt < b.createdAt ? 1 : -1).slice(0, this.maxDispatchPerCycle);
1326
- for (const run of pending) {
1327
- if (this.isolatedRunId !== null && run.id !== this.isolatedRunId)
1328
- continue;
1329
- const lease = this.claim(run.id);
1330
- if (!lease)
1331
- continue;
1332
- this.store.setRunStatus(run.id, "running");
1333
- this.wal.append({ op: "run_started", runId: run.id, at: nowIso() });
1334
- report.dispatched++;
1335
- }
1336
- const running = this.store.listRuns({ status: "running", limit: this.maxStepsPerCycle });
1337
- let steps = 0;
1338
- for (const run of running) {
1339
- if (this.isolatedRunId !== null && run.id !== this.isolatedRunId)
1340
- continue;
1341
- if (steps >= this.maxStepsPerCycle)
1342
- break;
1343
- const outcome = await this.advanceRun(run.id);
1344
- steps++;
1345
- if (outcome.kind === "completed")
1346
- report.completed++;
1347
- if (outcome.kind === "failed")
1348
- report.failed++;
1349
- if (outcome.kind === "advanced")
1350
- report.advanced++;
1351
- }
1352
- return report;
1353
- }
1354
- startRun(graph, context) {
1355
- const validation = validateGraph(graph);
1356
- if (!validation.ok) {
1357
- throw new Error(`graph validation failed: ${validation.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
1358
- }
1359
- assertNoSecrets(context, "run context");
1360
- assertNoSecrets(graph, "graph definition");
1361
- const existing = this.graphCache.get(graph.name);
1362
- const serialized = JSON.stringify(graph);
1363
- if (!existing || JSON.stringify(existing) !== serialized) {
1364
- this.graphCache.set(graph.name, graph);
1365
- this.wal.append({ op: "graph_registered", name: graph.name, version: graph.version, graph, at: nowIso() });
1453
+ } else {
1454
+ const firstColon = authority.indexOf(":");
1455
+ const lastColon = authority.lastIndexOf(":");
1456
+ if (firstColon !== lastColon) {
1457
+ throw new Error("IPv6 API URL authorities must use brackets.");
1458
+ }
1459
+ if (lastColon !== -1) {
1460
+ const port = authority.slice(lastColon + 1);
1461
+ assertCanonicalPort(port);
1462
+ rawHostname = authority.slice(0, lastColon);
1366
1463
  } else {
1367
- this.graphCache.set(graph.name, existing);
1368
- }
1369
- return this.store.createRun({ graphName: graph.name, graphVersion: graph.version, context: context ?? {} });
1370
- }
1371
- async advanceRun(runId) {
1372
- const run = this.store.getRun(runId);
1373
- if (!run || run.status !== "running")
1374
- return { kind: "noop" };
1375
- const lease = this.leases.get(runId);
1376
- if (!lease || lease.expiresAtMs <= this.time())
1377
- return { kind: "blocked" };
1378
- const graph = this.loadGraph(run.graphName);
1379
- if (!graph) {
1380
- this.failRun(run, `graph ${run.graphName} not found in daemon graph cache`);
1381
- return { kind: "failed", error: "graph not found" };
1382
- }
1383
- const context = JSON.parse(run.contextJson);
1384
- const rawWf = context.__wf ?? {};
1385
- const wf = {
1386
- ...rawWf,
1387
- cursor: typeof rawWf.cursor === "string" ? rawWf.cursor : undefined,
1388
- loops: rawWf.loops ?? {},
1389
- completedLoops: rawWf.completedLoops ?? {}
1390
- };
1391
- try {
1392
- const cursor = wf.cursor;
1393
- const node = cursor === undefined ? graph.nodes.find((n) => n.type === "start") : findNode(graph, cursor);
1394
- if (!node) {
1395
- this.failRun(run, `cursor ${cursor ?? "<start>"} resolves to no node`);
1396
- return { kind: "failed", error: "no node at cursor" };
1397
- }
1398
- switch (node.type) {
1399
- case "start": {
1400
- wf.cursor = node.next;
1401
- this.persistContext(run, context, wf);
1402
- return { kind: "advanced", nodeId: node.id };
1403
- }
1404
- case "step": {
1405
- return await this.executeStep(run, graph, context, wf, node);
1406
- }
1407
- case "decision": {
1408
- const value = evaluateExpr(node.condition, this.conditionScope(context));
1409
- const next = value ? node.then : node.else;
1410
- if (!next) {
1411
- this.failRun(run, `decision ${node.id} evaluated ${value} but no ${value ? "then" : "else"} edge`);
1412
- return { kind: "failed", error: "missing decision edge" };
1413
- }
1414
- wf.cursor = next;
1415
- this.persistContext(run, context, wf);
1416
- return { kind: "advanced", nodeId: node.id };
1417
- }
1418
- case "while": {
1419
- return this.executeWhile(run, graph, context, wf, node);
1420
- }
1421
- case "end": {
1422
- this.completeRun(run, context);
1423
- return { kind: "completed" };
1424
- }
1425
- }
1426
- } catch (err) {
1427
- if (err instanceof ExprEvalError || err instanceof ExprSyntaxError) {
1428
- this.failRun(run, `condition error at ${wf.cursor ?? "<start>"}: ${err.message}`);
1429
- return { kind: "failed", error: err.message };
1430
- }
1431
- throw err;
1432
- }
1433
- }
1434
- loadGraph(name) {
1435
- return this.graphCache.get(name);
1436
- }
1437
- conditionScope(context) {
1438
- return { ...context, steps: context.steps ?? {} };
1439
- }
1440
- persistContext(run, context, wf) {
1441
- context.__wf = wf;
1442
- this.store.setRunContext(run.id, context);
1443
- }
1444
- async executeStep(run, graph, context, wf, node) {
1445
- const nodeId = node.id;
1446
- const latest = this.store.getLatestRunNode(run.id, nodeId);
1447
- const isLoopBody = graph.nodes.some((candidate) => candidate.type === "while" && candidate.body.includes(nodeId));
1448
- const nodeRow = latest === undefined || isLoopBody && latest.status === "completed" ? this.store.createRunNode({ runId: run.id, nodeId, lane: node.lane ?? "claude" }) : latest;
1449
- const memoWatch = node.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
1450
- const memoSource = { command: node.command, prompt: node.prompt, context: stripRunState(context) };
1451
- const memoInput = node.memo ? memoWatch !== undefined ? { ...memoSource, memoWatch } : memoSource : undefined;
1452
- if (memoInput !== undefined) {
1453
- const hit = tryMemoHit(this.store, run.graphName, nodeId, memoInput);
1454
- if (hit) {
1455
- const cached = JSON.parse(hit.outputJson);
1456
- this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: Number(cached.exitCode ?? 0), output: { ...cached, memoHit: true } });
1457
- this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
1458
- this.recordStepResult(context, nodeId, { ...cached, memoHit: true });
1459
- return this.afterNode(run, context, wf, node.next);
1460
- }
1464
+ rawHostname = authority;
1461
1465
  }
1462
- if (nodeRow.status !== "running") {
1463
- this.store.setRunNodeStatus(nodeRow.id, "running");
1464
- this.wal.append({ op: "node_started", runId: run.id, nodeId, at: nowIso() });
1466
+ const ipVersion = isIP(rawHostname);
1467
+ const numericAddressParts = rawHostname.split(".");
1468
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
1469
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
1470
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
1465
1471
  }
1466
- const job = {
1467
- lane: node.lane ?? "claude",
1468
- prompt: node.prompt,
1469
- command: node.command,
1470
- timeoutMs: node.timeoutMs,
1471
- env: this.env
1472
- };
1473
- let result;
1472
+ }
1473
+ return rawHostname.toLowerCase();
1474
+ }
1475
+ function isDeliberateLoopbackHttpAuthority(authority) {
1476
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
1477
+ }
1478
+ function toV1BaseUrl(apiUrl) {
1479
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
1480
+ throw new Error("API URL must not contain ASCII control characters.");
1481
+ }
1482
+ const input = apiUrl.trim();
1483
+ const authority = rawAuthority(input);
1484
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
1485
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
1486
+ }
1487
+ const canonicalHostname = canonicalAuthorityHostname(authority);
1488
+ const url = new URL(input);
1489
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1490
+ throw new Error("API URL must use http or https.");
1491
+ }
1492
+ if (url.username || url.password) {
1493
+ throw new Error("API URL must not include credentials.");
1494
+ }
1495
+ if (!url.hostname || url.hostname.endsWith(".")) {
1496
+ throw new Error("API URL must include a canonical hostname.");
1497
+ }
1498
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
1499
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
1500
+ }
1501
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
1502
+ throw new Error("API URL must not use IDN or punycode hostnames.");
1503
+ }
1504
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
1505
+ throw new Error("API URL may use http only for an exact loopback authority.");
1506
+ }
1507
+ if (url.search || url.hash) {
1508
+ throw new Error("API URL must not include a query string or fragment.");
1509
+ }
1510
+ let path = url.pathname.replace(/\/+$/, "");
1511
+ if (path.endsWith("/v1"))
1512
+ path = path.slice(0, -"/v1".length);
1513
+ url.pathname = `${path}/v1`;
1514
+ return url.toString().replace(/\/+$/, "");
1515
+ }
1516
+ class ClientTransportConfigurationError extends Error {
1517
+ appName;
1518
+ sources;
1519
+ constructor(appName, message, sources = []) {
1520
+ super(message);
1521
+ this.name = "ClientTransportConfigurationError";
1522
+ this.appName = appName;
1523
+ this.sources = Object.freeze([...sources]);
1524
+ }
1525
+ }
1526
+
1527
+ class ClientCredentialMissingError extends ClientTransportConfigurationError {
1528
+ authorityConfigured;
1529
+ constructor(name, message, sources, authorityConfigured) {
1530
+ super(name, message, sources);
1531
+ this.authorityConfigured = authorityConfigured;
1532
+ this.name = "ClientCredentialMissingError";
1533
+ }
1534
+ }
1535
+ function resolveClientAuthoritySnapshot(name, env = process.env, options = {}) {
1536
+ env = snapshotClientEnvironment(name, env);
1537
+ const keys = clientTransportEnvKeys(name);
1538
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
1539
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
1540
+ if (blankUrl) {
1541
+ throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
1542
+ }
1543
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
1544
+ if (controlledUrl) {
1545
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
1546
+ }
1547
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
1548
+ const normalizedAuthority = (entry) => {
1474
1549
  try {
1475
- result = await this.laneRunner(job);
1476
- } catch (err) {
1477
- result = { ok: false, exitCode: 1, output: "", durationMs: 0, error: String(err instanceof Error ? err.message : err) };
1478
- }
1479
- const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
1480
- if (!result.ok) {
1481
- const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
1482
- this.store.bumpAttemptsNode(nodeRow.id);
1483
- if (shouldRetry) {
1484
- this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
1485
- return { kind: "advanced", nodeId };
1486
- }
1487
- const error = result.error ?? `step ${nodeId} failed with exit ${result.exitCode}`;
1488
- this.store.setRunNodeStatus(nodeRow.id, "failed", { exitCode: result.exitCode, error });
1489
- this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "failed", at: nowIso() });
1490
- this.failRun(run, error);
1491
- return { kind: "failed", error };
1550
+ return toV1BaseUrl(entry.value);
1551
+ } catch {
1552
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${entry.key}; use an absolute HTTPS URL or exact loopback HTTP authority without credentials, query or fragment.`, [entry.key]);
1492
1553
  }
1554
+ };
1555
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map(normalizedAuthority)).size > 1) {
1556
+ throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must select the same versioned API authority or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
1557
+ }
1558
+ const envUrlHit = usableUrlEntries[0] ?? null;
1559
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
1560
+ const profile = options.bindUnpairedCredentialsToFleet ? options.credentials?.profile?.trim() || env.HASNA_PROFILE?.trim() || null : null;
1561
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys, profile);
1562
+ if (diskConfigUrlHit?.unusable) {
1563
+ throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
1564
+ }
1565
+ const urlCandidates = [
1566
+ ...envUrlHit ? [envUrlHit] : [],
1567
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
1568
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
1569
+ ];
1570
+ const configuredUrl = urlCandidates[0] ?? null;
1571
+ const normalizedUrls = urlCandidates.map(normalizedAuthority);
1572
+ const divergentUrls = urlCandidates.filter((_, index) => normalizedUrls[index] !== normalizedUrls[0]);
1573
+ if (configuredUrl && divergentUrls.length > 0) {
1574
+ throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
1575
+ }
1576
+ if (configuredUrl && options.bindUnpairedCredentialsToFleet) {
1493
1577
  try {
1494
- assertNoSecrets(output, `node ${nodeId} output`);
1495
- } catch (err) {
1496
- const message = err instanceof Error ? err.message : String(err);
1497
- this.store.setRunNodeStatus(nodeRow.id, "failed", { error: message });
1498
- this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "failed", at: nowIso() });
1499
- this.failRun(run, message);
1500
- return { kind: "failed", error: message };
1501
- }
1502
- this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: result.exitCode, output });
1503
- this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
1504
- if (memoInput !== undefined) {
1505
- const memoInputAfter = node.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
1506
- const stable = memoWatch === undefined && memoInputAfter === undefined || memoWatch !== undefined && memoInputAfter !== undefined && JSON.stringify(memoWatch) === JSON.stringify(memoInputAfter);
1507
- if (stable) {
1508
- recordMemo(this.store, run.graphName, nodeId, memoInput, output);
1509
- this.wal.append({ op: "memo_set", key: memoKey(run.graphName, nodeId, memoInput), at: nowIso() });
1510
- }
1511
- }
1512
- this.recordStepResult(context, nodeId, output);
1513
- return this.afterNode(run, context, wf, node.next);
1514
- }
1515
- afterNode(run, context, wf, next) {
1516
- if (next) {
1517
- wf.cursor = next;
1518
- this.persistContext(run, context, wf);
1519
- return { kind: "advanced", nodeId: wf.cursor };
1520
- }
1521
- const cursor = wf.cursor;
1522
- const graph = this.loadGraph(run.graphName);
1523
- const whileNode = graph.nodes.find((n) => n.type === "while" && n.body.includes(cursor ?? ""));
1524
- if (whileNode) {
1525
- const index = whileNode.body.indexOf(cursor ?? "");
1526
- if (index >= 0 && index < whileNode.body.length - 1) {
1527
- wf.cursor = whileNode.body[index + 1];
1528
- this.persistContext(run, context, wf);
1529
- return { kind: "advanced", nodeId: wf.cursor };
1530
- }
1531
- wf.loops[whileNode.id] = (wf.loops[whileNode.id] ?? 0) + 1;
1532
- wf.cursor = whileNode.id;
1533
- this.persistContext(run, context, wf);
1534
- return { kind: "advanced", nodeId: whileNode.id };
1535
- }
1536
- this.completeRun(run, context);
1537
- return { kind: "completed" };
1538
- }
1539
- executeWhile(run, graph, context, wf, node) {
1540
- const iteration = wf.loops[node.id] ?? 0;
1541
- if (iteration >= node.maxIterations) {
1542
- const error = `while node ${node.id} exceeded maxIterations (${node.maxIterations})`;
1543
- this.failRun(run, error);
1544
- return { kind: "failed", error };
1545
- }
1546
- const scope = { ...this.conditionScope(context), i: iteration };
1547
- const keepGoing = evaluateExpr(node.condition, scope);
1548
- if (!keepGoing) {
1549
- wf.completedLoops[node.id] = iteration;
1550
- delete wf.loops[node.id];
1551
- wf.cursor = node.next;
1552
- this.persistContext(run, context, wf);
1553
- return { kind: "advanced", nodeId: node.id };
1554
- }
1555
- wf.cursor = node.body[0];
1556
- this.persistContext(run, context, wf);
1557
- return { kind: "advanced", nodeId: node.id };
1558
- }
1559
- recordStepResult(context, nodeId, output) {
1560
- const steps = context.steps ?? {};
1561
- steps[nodeId] = output;
1562
- context.steps = steps;
1563
- }
1564
- completeRun(run, context) {
1565
- const result = {
1566
- steps: context.steps ?? {},
1567
- iterations: context.__wf?.completedLoops ?? {}
1568
- };
1578
+ toV1BaseUrl(configuredUrl.value);
1579
+ } catch (error) {
1580
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${configuredUrl.key}: ${error instanceof Error ? error.message : String(error)}`, [configuredUrl.key]);
1581
+ }
1582
+ }
1583
+ return { keys, configuredUrl, envUrlHit, keychainUrlHit, diskConfigUrlHit };
1584
+ }
1585
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
1586
+ env = snapshotClientEnvironment(name, env);
1587
+ const { keys, configuredUrl, envUrlHit, keychainUrlHit, diskConfigUrlHit } = resolveClientAuthoritySnapshot(name, env, options);
1588
+ const warnings = [];
1589
+ if (configuredUrl && !envUrlHit) {
1590
+ warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
1591
+ }
1592
+ const credential = resolveCredential(name, env, options.credentials);
1593
+ if (!credential) {
1594
+ const diskHint = credentialDiskSourcesForMessage(name, env);
1595
+ const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
1596
+ warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
1597
+ const sources = [configuredUrl?.key ?? keys.apiUrlKeys[0]];
1598
+ if (options.bindUnpairedCredentialsToFleet)
1599
+ throw new ClientCredentialMissingError(name, warnings.join(" "), sources, configuredUrl !== null);
1600
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), sources);
1601
+ }
1602
+ if (credential.warning)
1603
+ warnings.push(credential.warning);
1604
+ let urlHit;
1605
+ if (configuredUrl) {
1606
+ urlHit = configuredUrl;
1607
+ } else {
1569
1608
  try {
1570
- assertNoSecrets(result, "run result");
1571
- } catch (err) {
1572
- const message = err instanceof Error ? err.message : String(err);
1573
- this.failRun(run, message);
1574
- return;
1609
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
1610
+ } catch (error) {
1611
+ const message = error instanceof Error ? error.message : String(error);
1612
+ throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
1575
1613
  }
1576
- this.store.setRunStatus(run.id, "completed", { result, finishedAt: nowIso(), error: null });
1577
- this.wal.append({ op: "run_finished", runId: run.id, status: "completed", at: nowIso() });
1578
- const lease = this.leases.get(run.id);
1579
- if (lease)
1580
- this.release(run.id, lease.fencing);
1581
- }
1582
- failRun(run, error) {
1583
- this.store.setRunStatus(run.id, "failed", { error, finishedAt: nowIso() });
1584
- this.wal.append({ op: "run_finished", runId: run.id, status: "failed", at: nowIso() });
1585
- const lease = this.leases.get(run.id);
1586
- if (lease)
1587
- this.release(run.id, lease.fencing);
1588
- }
1589
- }
1590
- var defaultLaneRunner = async (job) => {
1591
- if (job.command)
1592
- return runCommand(job);
1593
- const { runLaneJob: runLaneJob2 } = await Promise.resolve().then(() => (init_lanes(), exports_lanes));
1594
- return runLaneJob2(job);
1595
- };
1596
- async function runGraphToCompletion(store, wal, graph, context, options = {}) {
1597
- const daemon = new WorkflowsDaemon(store, wal, {
1598
- worker: "local",
1599
- laneRunner: options.laneRunner,
1600
- time: options.time,
1601
- env: options.env,
1602
- isolatedRunId: options.isolatedRunId
1603
- });
1604
- const run = daemon.startRun(graph, context);
1605
- options.onRunCreated?.(run);
1606
- daemon.isolateRun(run.id);
1607
- const maxCycles = options.maxCycles ?? 500;
1608
- for (let cycle = 0;cycle < maxCycles; cycle++) {
1609
- const report = await daemon.reap();
1610
- const current = store.getRun(run.id);
1611
- if (current.status !== "running")
1612
- return current;
1613
- if (report.advanced === 0 && report.dispatched === 0) {
1614
- return store.getRun(run.id);
1614
+ }
1615
+ const apiUrlSource = urlHit.key;
1616
+ let baseUrl;
1617
+ try {
1618
+ baseUrl = toV1BaseUrl(urlHit.value);
1619
+ } catch (error) {
1620
+ const message = error instanceof Error ? error.message : String(error);
1621
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
1622
+ }
1623
+ if (options.bindUnpairedCredentialsToFleet && baseUrl !== toV1BaseUrl(defaultFleetGatewayBaseUrl(name))) {
1624
+ const pairedDisk = diskConfigUrlHit?.path === credential.source && (credential.tier === "disk" || credential.tier === "profile" || credential.tier === "pointer");
1625
+ const pairedKeychain = credential.tier === "keychain" && keychainUrlHit?.source === credential.source.replace(".api-key@", ".api-url@");
1626
+ const deliberatePair = envUrlHit !== null && (credential.tier === "argument" || credential.tier === "override");
1627
+ if (!pairedDisk && !pairedKeychain && !deliberatePair) {
1628
+ throw new ClientTransportConfigurationError(name, "An unpaired legacy credential is bound to the fleet gateway. Configure a stored authority/credential pair or an explicit API key override for the intended authority; no credential was sent.");
1615
1629
  }
1616
1630
  }
1617
- return store.getRun(run.id);
1618
- }
1619
-
1620
- // src/agent-tools.ts
1621
- init_lanes();
1622
-
1623
- // src/store.ts
1624
- import { Database } from "bun:sqlite";
1625
- import { mkdirSync } from "fs";
1626
- import { join as join2 } from "path";
1627
- import { randomUUID } from "crypto";
1628
- function nowIso2() {
1629
- return new Date().toISOString();
1630
- }
1631
- var BUSY_TIMEOUT_MS = 3000;
1632
- var BUSY_RETRIES = 2;
1633
- var BUSY_RETRY_SLEEP_MS = 50;
1634
- function isBusyError(err) {
1635
- if (!(err instanceof Error))
1636
- return false;
1637
- const code = String(err.code ?? "");
1638
- return code.includes("BUSY") || /\bdatabase is locked\b/i.test(err.message);
1639
- }
1640
- function retryBusy(fn) {
1641
- let last;
1642
- for (let attempt = 0;attempt <= BUSY_RETRIES; attempt++) {
1643
- try {
1644
- return fn();
1645
- } catch (err) {
1646
- if (!isBusyError(err))
1647
- throw err;
1648
- last = err;
1649
- Bun.sleepSync(BUSY_RETRY_SLEEP_MS);
1650
- }
1651
- }
1652
- throw last;
1653
- }
1654
- function openStore(dataDir) {
1655
- mkdirSync(dataDir, { recursive: true });
1656
- const db = new Database(join2(dataDir, "workflows.db"));
1657
- retryBusy(() => db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`));
1658
- retryBusy(() => db.exec("PRAGMA journal_mode = WAL"));
1659
- retryBusy(() => db.exec("PRAGMA foreign_keys = ON"));
1660
- retryBusy(() => db.exec(`
1661
- CREATE TABLE IF NOT EXISTS runs (
1662
- id TEXT PRIMARY KEY,
1663
- graph_name TEXT NOT NULL,
1664
- graph_version TEXT NOT NULL,
1665
- status TEXT NOT NULL,
1666
- attempts INTEGER NOT NULL DEFAULT 0,
1667
- context_json TEXT NOT NULL,
1668
- result_json TEXT,
1669
- error TEXT,
1670
- started_at TEXT,
1671
- finished_at TEXT,
1672
- created_at TEXT NOT NULL,
1673
- updated_at TEXT NOT NULL
1674
- );
1675
- CREATE TABLE IF NOT EXISTS run_nodes (
1676
- id TEXT PRIMARY KEY,
1677
- run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
1678
- node_id TEXT NOT NULL,
1679
- status TEXT NOT NULL,
1680
- attempts INTEGER NOT NULL DEFAULT 0,
1681
- lane TEXT,
1682
- exit_code INTEGER,
1683
- output_json TEXT,
1684
- error TEXT,
1685
- started_at TEXT,
1686
- finished_at TEXT
1687
- );
1688
- CREATE TABLE IF NOT EXISTS memos (
1689
- key TEXT PRIMARY KEY,
1690
- graph_name TEXT NOT NULL,
1691
- node_id TEXT NOT NULL,
1692
- input_hash TEXT NOT NULL,
1693
- output_json TEXT NOT NULL,
1694
- hit_count INTEGER NOT NULL DEFAULT 0,
1695
- created_at TEXT NOT NULL
1696
- );
1697
- CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
1698
- CREATE INDEX IF NOT EXISTS idx_runs_created ON runs(created_at);
1699
- CREATE INDEX IF NOT EXISTS idx_run_nodes_run ON run_nodes(run_id);
1700
- `));
1701
- const rowToRun = (r) => ({
1702
- id: String(r.id),
1703
- graphName: String(r.graph_name),
1704
- graphVersion: String(r.graph_version),
1705
- status: r.status,
1706
- attempts: Number(r.attempts),
1707
- contextJson: String(r.context_json),
1708
- resultJson: r.result_json === null ? null : String(r.result_json),
1709
- error: r.error === null ? null : String(r.error),
1710
- startedAt: r.started_at === null ? null : String(r.started_at),
1711
- finishedAt: r.finished_at === null ? null : String(r.finished_at),
1712
- createdAt: String(r.created_at),
1713
- updatedAt: String(r.updated_at)
1714
- });
1715
- const rowToNode = (r) => ({
1716
- id: String(r.id),
1717
- runId: String(r.run_id),
1718
- nodeId: String(r.node_id),
1719
- status: r.status,
1720
- attempts: Number(r.attempts),
1721
- lane: r.lane === null ? null : String(r.lane),
1722
- exitCode: r.exit_code === null ? null : Number(r.exit_code),
1723
- outputJson: r.output_json === null ? null : String(r.output_json),
1724
- error: r.error === null ? null : String(r.error),
1725
- startedAt: r.started_at === null ? null : String(r.started_at),
1726
- finishedAt: r.finished_at === null ? null : String(r.finished_at)
1727
- });
1728
- const rowToMemo = (r) => ({
1729
- key: String(r.key),
1730
- graphName: String(r.graph_name),
1731
- nodeId: String(r.node_id),
1732
- inputHash: String(r.input_hash),
1733
- outputJson: String(r.output_json),
1734
- hitCount: Number(r.hit_count),
1735
- createdAt: String(r.created_at)
1736
- });
1737
1631
  return {
1738
- dataDir,
1739
- close() {
1740
- db.close();
1741
- },
1742
- listTables() {
1743
- const rows = retryBusy(() => db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all());
1744
- return rows.map((r) => r.name);
1632
+ resolution: {
1633
+ transport: "http",
1634
+ transportSource: urlHit.key,
1635
+ baseUrl,
1636
+ apiUrlSource,
1637
+ apiKeyPresent: true,
1638
+ apiKeySource: credential.source,
1639
+ apiKeyTier: credential.tier,
1640
+ misconfigured: false,
1641
+ warning: warnings.length > 0 ? warnings.join(" ") : null
1745
1642
  },
1746
- createRun(input) {
1747
- const id = randomUUID();
1748
- const now = nowIso2();
1749
- retryBusy(() => db.query(`INSERT INTO runs (id, graph_name, graph_version, status, attempts, context_json, created_at, updated_at)
1750
- VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)`).run(id, input.graphName, input.graphVersion, JSON.stringify(input.context ?? {}), now, now));
1751
- return this.getRun(id);
1752
- },
1753
- getRun(id) {
1754
- const row = retryBusy(() => db.query(`SELECT * FROM runs WHERE id = ?`).get(id));
1755
- return row ? rowToRun(row) : undefined;
1756
- },
1757
- listRuns(opts = {}) {
1758
- const limit = Math.min(opts.limit ?? 100, 1000);
1759
- if (opts.status) {
1760
- const status = opts.status;
1761
- const rows2 = retryBusy(() => db.query(`SELECT * FROM runs WHERE status = ? ORDER BY created_at DESC, id DESC LIMIT ?`).all(status, limit));
1762
- return rows2.map((r) => rowToRun(r));
1763
- }
1764
- const rows = retryBusy(() => db.query(`SELECT * FROM runs ORDER BY created_at DESC, id DESC LIMIT ?`).all(limit));
1765
- return rows.map((r) => rowToRun(r));
1766
- },
1767
- setRunStatus(id, status, patch = {}) {
1768
- const now = nowIso2();
1769
- const existing = this.getRun(id);
1770
- const resultJson = patch.result !== undefined ? JSON.stringify(patch.result) : existing?.resultJson ?? null;
1771
- const error = patch.error !== undefined ? patch.error : existing?.error ?? null;
1772
- const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing?.startedAt === null ? now : existing?.startedAt ?? null;
1773
- const terminal = status === "completed" || status === "failed" || status === "cancelled" || status === "interrupted";
1774
- const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing?.finishedAt === null ? now : existing?.finishedAt ?? null;
1775
- retryBusy(() => db.query(`UPDATE runs SET status = ?, result_json = ?, error = ?, started_at = ?, finished_at = ?, updated_at = ? WHERE id = ?`).run(status, resultJson, error, startedAt, finishedAt, now, id));
1776
- },
1777
- bumpAttempts(id) {
1778
- retryBusy(() => db.query(`UPDATE runs SET attempts = attempts + 1, updated_at = ? WHERE id = ?`).run(nowIso2(), id));
1779
- },
1780
- setRunContext(id, context) {
1781
- retryBusy(() => db.query(`UPDATE runs SET context_json = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(context), nowIso2(), id));
1782
- },
1783
- createRunNode(input) {
1784
- const id = randomUUID();
1785
- retryBusy(() => db.query(`INSERT INTO run_nodes (id, run_id, node_id, status, attempts, lane)
1786
- VALUES (?, ?, ?, 'pending', 0, ?)`).run(id, input.runId, input.nodeId, input.lane ?? null));
1787
- return this.getRunNode(id);
1788
- },
1789
- getRunNode(id) {
1790
- const row = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE id = ?`).get(id));
1791
- return row ? rowToNode(row) : undefined;
1792
- },
1793
- listRunNodes(runId) {
1794
- const rows = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE run_id = ? ORDER BY rowid ASC`).all(runId));
1795
- return rows.map((r) => rowToNode(r));
1796
- },
1797
- getLatestRunNode(runId, nodeId) {
1798
- const row = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE run_id = ? AND node_id = ? ORDER BY rowid DESC LIMIT 1`).get(runId, nodeId));
1799
- return row ? rowToNode(row) : undefined;
1800
- },
1801
- setRunNodeStatus(id, status, patch = {}) {
1802
- const now = nowIso2();
1803
- const existing = this.getRunNode(id);
1804
- if (!existing)
1805
- return;
1806
- const exitCode = patch.exitCode !== undefined ? patch.exitCode : existing.exitCode;
1807
- const outputJson = patch.output !== undefined ? JSON.stringify(patch.output) : existing.outputJson;
1808
- const error = patch.error !== undefined ? patch.error : existing.error;
1809
- const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing.startedAt === null ? now : existing.startedAt;
1810
- const terminal = status === "completed" || status === "failed" || status === "skipped";
1811
- const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing.finishedAt === null ? now : existing.finishedAt;
1812
- retryBusy(() => db.query(`UPDATE run_nodes SET status = ?, exit_code = ?, output_json = ?, error = ?, started_at = ?, finished_at = ? WHERE id = ?`).run(status, exitCode, outputJson, error, startedAt, finishedAt, id));
1813
- },
1814
- bumpAttemptsNode(id) {
1815
- retryBusy(() => db.query(`UPDATE run_nodes SET attempts = attempts + 1 WHERE id = ?`).run(id));
1816
- },
1817
- memoPut(key, graphName, nodeId, inputHash2, outputJson) {
1818
- retryBusy(() => db.query(`INSERT INTO memos (key, graph_name, node_id, input_hash, output_json, hit_count, created_at)
1819
- VALUES (?, ?, ?, ?, ?, 0, ?)
1820
- ON CONFLICT(key) DO UPDATE SET output_json = excluded.output_json`).run(key, graphName, nodeId, inputHash2, outputJson, nowIso2()));
1821
- },
1822
- memoGet(key) {
1823
- const row = retryBusy(() => db.query(`SELECT * FROM memos WHERE key = ?`).get(key));
1824
- return row ? rowToMemo(row) : undefined;
1825
- },
1826
- memoHit(key) {
1827
- retryBusy(() => db.query(`UPDATE memos SET hit_count = hit_count + 1 WHERE key = ?`).run(key));
1828
- },
1829
- memoList() {
1830
- const rows = retryBusy(() => db.query(`SELECT * FROM memos ORDER BY created_at DESC LIMIT 1000`).all());
1831
- return rows.map((r) => rowToMemo(r));
1832
- },
1833
- memoClear() {
1834
- retryBusy(() => db.exec(`DELETE FROM memos`));
1835
- }
1643
+ credential
1836
1644
  };
1837
1645
  }
1838
-
1839
- // src/wal.ts
1840
- import { createHash as createHash2 } from "crypto";
1841
- import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, truncateSync, writeFileSync } from "fs";
1842
- import { join as join3 } from "path";
1843
- var WAL_FILE_NAME = "session.wal";
1844
- var SESSIONS_DIR_NAME = "sessions";
1845
- var MAX_WAL_BYTES = 256 * 1024 * 1024;
1846
- function checksumOf(op) {
1847
- if (op === undefined || op === null)
1848
- return "";
1849
- return createHash2("sha256").update(JSON.stringify(op)).digest("hex");
1646
+ function credentialDiskSourcesForMessage(name, env) {
1647
+ const paths = credentialDiskSources(name, env);
1648
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
1850
1649
  }
1851
1650
 
1852
- class SessionWAL {
1853
- filePath;
1854
- constructor(filePath) {
1855
- this.filePath = filePath;
1856
- }
1857
- static open(dataDir) {
1858
- mkdirSync2(join3(dataDir, SESSIONS_DIR_NAME), { recursive: true });
1859
- const canonical = join3(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1860
- const legacy = join3(dataDir, WAL_FILE_NAME);
1861
- if (existsSync(legacy) && !existsSync(canonical)) {
1862
- try {
1863
- renameSync(legacy, canonical);
1864
- } catch {
1865
- return new SessionWAL(legacy);
1651
+ class HasnaHttpError extends Error {
1652
+ status;
1653
+ method;
1654
+ path;
1655
+ credentialSource;
1656
+ credentialTier;
1657
+ constructor(method, path, status, body, credential) {
1658
+ const guidance = credential ? `. ${credential.guidance}` : "";
1659
+ super(`Hasna cloud request failed: ${method} ${path} -> ${status}${guidance}`);
1660
+ this.name = "HasnaHttpError";
1661
+ this.status = status;
1662
+ this.method = method;
1663
+ this.path = path;
1664
+ Object.defineProperty(this, "body", {
1665
+ value: body,
1666
+ enumerable: status !== 401 && status !== 403,
1667
+ writable: false,
1668
+ configurable: false
1669
+ });
1670
+ this.credentialSource = credential?.source ?? null;
1671
+ this.credentialTier = credential?.tier ?? null;
1672
+ }
1673
+ }
1674
+ function currentCredential(name, apiKey) {
1675
+ if (typeof apiKey === "function") {
1676
+ return validateAndSealResolvedCredential(name, apiKey());
1677
+ }
1678
+ return explicitCredential(name, apiKey);
1679
+ }
1680
+ async function resolveRequestCredential(name, apiKey, env = process.env) {
1681
+ const resolved = currentCredential(name, apiKey);
1682
+ if (resolved.tier === "pointer") {
1683
+ return completePointerCredential(name, resolved, env);
1684
+ }
1685
+ return resolved;
1686
+ }
1687
+ function authFailureGuidance(credential) {
1688
+ const origin = `The API key for this request came from ${credential.source}`;
1689
+ if (credential.deliberate) {
1690
+ const remedy = credential.source === CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE ? `Fix that provider so it returns the current key, or replace it with resolveCredential() ` + `so diagnostics can name the original source.` : `Rotate that key, or unset the override to use the credential on disk.`;
1691
+ return `${origin} \u2014 a credential you selected deliberately. It was NOT substituted with any other key: ` + `falling back here would authenticate as a different principal than the one you named, which is ` + `exactly the failure an override exists to prevent. ${remedy}`;
1692
+ }
1693
+ if (credential.tier === "env") {
1694
+ const target = credential.diskCandidates[0];
1695
+ const remedy = target ? `Store the CURRENT key in the Keychain or write it to ${target} \u2014 both are re-read on every call, so ` + `rotations take effect immediately and in every shell. Do not simply unset ${credential.source}: ` + `nothing was found in the Keychain or on disk, so that would leave this client with no credential at all.` : `This environment has no HOME or HASNA_HOME, so no credential file could be consulted; the disk tier is ` + `unavailable here and there is nothing to fall back to. Set HOME, or supply the key explicitly.`;
1696
+ return `${origin}, a variable in this process's environment. If a wrapper injected it for this one process, the ` + `wrapper re-reads its store on every invocation and the stored key itself is being rejected \u2014 rotate it. ` + `If this SHELL exported it, the export is a snapshot taken when the shell started: a STALE SHELL that ` + `exported the key before it was rotated keeps sending the old one until it exits. ${remedy}`;
1697
+ }
1698
+ if (credential.tier === "keychain") {
1699
+ return `${origin}, which was re-read from the Keychain on this very call \u2014 so a stale shell is NOT the cause ` + `here. The stored item is genuinely being rejected: update it with the current key, or re-run the fleet ` + `key distribution so this machine gets the current key.`;
1700
+ }
1701
+ return `${origin}, which was re-read from disk on this very call \u2014 so a stale shell is NOT the cause here. ` + `The stored credential is genuinely being rejected: rotate it, or re-run the fleet key distribution ` + `so this machine gets the current key.`;
1702
+ }
1703
+ var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
1704
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
1705
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
1706
+ "host",
1707
+ ":authority",
1708
+ "forwarded",
1709
+ "x-forwarded-host",
1710
+ "x-original-host"
1711
+ ]);
1712
+ function assertNoAuthorityOverrideHeaders(headers, source) {
1713
+ if (!headers)
1714
+ return;
1715
+ const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS.has(name.trim().toLowerCase()));
1716
+ if (forbidden) {
1717
+ throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
1718
+ }
1719
+ }
1720
+ function appendQuery(path, query) {
1721
+ if (!query)
1722
+ return path;
1723
+ const params = query instanceof URLSearchParams ? query : new URLSearchParams;
1724
+ if (!(query instanceof URLSearchParams)) {
1725
+ for (const [key, value] of Object.entries(query)) {
1726
+ if (value === null || value === undefined)
1727
+ continue;
1728
+ if (Array.isArray(value)) {
1729
+ for (const v of value)
1730
+ params.append(key, String(v));
1731
+ } else {
1732
+ params.append(key, String(value));
1866
1733
  }
1867
1734
  }
1868
- return new SessionWAL(canonical);
1869
- }
1870
- static openAt(filePath) {
1871
- return new SessionWAL(filePath);
1872
- }
1873
- append(op) {
1874
- const at = op.at ?? new Date().toISOString();
1875
- const full = { ...op, at };
1876
- const nextSeq = this.nextSeq();
1877
- const entry = { seq: nextSeq, at, checksum: checksumOf(full), op: full };
1878
- appendFileSync(this.filePath, JSON.stringify(entry) + `
1879
- `, "utf8");
1880
- return entry;
1881
- }
1882
- nextSeq() {
1883
- const replay = this.replay();
1884
- return replay.entries.length + 1;
1885
- }
1886
- replay() {
1887
- const entries = [];
1888
- let torn = false;
1889
- let repaired = false;
1890
- let content;
1735
+ }
1736
+ const qs = params.toString();
1737
+ if (!qs)
1738
+ return path;
1739
+ return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
1740
+ }
1741
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1742
+ function createHasnaHttpTransportInternal(options, requestBindingProvider) {
1743
+ const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
1744
+ const base = toV1BaseUrl(options.baseUrl);
1745
+ const timeoutMs = options.timeoutMs ?? 30000;
1746
+ const sleep = options.sleepImpl ?? defaultSleep;
1747
+ const defaultRetry = options.retry;
1748
+ function resolveRetry(callRetry) {
1749
+ const chosen = callRetry !== undefined ? callRetry : defaultRetry;
1750
+ if (chosen === false)
1751
+ return null;
1752
+ const r2 = chosen ?? {};
1753
+ return {
1754
+ retries: r2.retries ?? 2,
1755
+ baseDelayMs: r2.baseDelayMs ?? 200,
1756
+ maxDelayMs: r2.maxDelayMs ?? 2000,
1757
+ retryStatuses: r2.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
1758
+ };
1759
+ }
1760
+ async function once(method, rel, body, opts, bind) {
1761
+ const materializeHeaders = (input) => Object.fromEntries(Object.entries(input ?? {}).map(([key, value]) => [key, String(value)]));
1762
+ const transportHeaders = materializeHeaders(options.headers);
1763
+ const requestHeaders = materializeHeaders(opts.headers);
1764
+ assertNoAuthorityOverrideHeaders(transportHeaders, "transport");
1765
+ assertNoAuthorityOverrideHeaders(requestHeaders, "request");
1766
+ const headers = {
1767
+ Accept: "application/json",
1768
+ ...transportHeaders,
1769
+ ...requestHeaders
1770
+ };
1771
+ const idempotencyKey = opts.idempotencyKey;
1772
+ if (idempotencyKey)
1773
+ headers["Idempotency-Key"] = String(idempotencyKey);
1774
+ const init = {
1775
+ method,
1776
+ headers,
1777
+ redirect: "manual"
1778
+ };
1779
+ if (body !== undefined) {
1780
+ headers["Content-Type"] = "application/json";
1781
+ init.body = JSON.stringify(body);
1782
+ }
1783
+ const controller = new AbortController;
1784
+ const onAbort = () => controller.abort();
1785
+ const callerSignal = opts.signal;
1786
+ if (callerSignal) {
1787
+ if (callerSignal.aborted)
1788
+ controller.abort();
1789
+ else
1790
+ callerSignal.addEventListener("abort", onAbort, { once: true });
1791
+ }
1792
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
1793
+ init.signal = controller.signal;
1794
+ let response;
1795
+ let credential;
1891
1796
  try {
1892
- content = readFileSync2(this.filePath, "utf8");
1893
- } catch {
1894
- return { entries, torn, repaired, liveClaims: () => new Map };
1895
- }
1896
- if (content.length > MAX_WAL_BYTES) {
1897
- throw new Error(`session WAL exceeds ${MAX_WAL_BYTES} bytes; refusing to replay a truncated journal`);
1898
- }
1899
- if (content === "") {
1900
- return { entries, torn, repaired, liveClaims: () => new Map };
1901
- }
1902
- const lines = content.split(`
1903
- `);
1904
- let badOffset = null;
1905
- let expectedSeq = 1;
1906
- for (let i = 0;i < lines.length; i++) {
1907
- const line = lines[i];
1908
- if (line === "")
1909
- continue;
1910
- if (badOffset !== null)
1911
- break;
1912
- const lineOffset = cumulativeOffset(lines, i);
1913
- let entry;
1797
+ const binding = bind();
1798
+ credential = binding.credential;
1799
+ init.headers = {
1800
+ "x-api-key": credential.apiKey,
1801
+ Authorization: `Bearer ${credential.apiKey}`,
1802
+ ...headers
1803
+ };
1914
1804
  try {
1915
- entry = JSON.parse(line);
1916
- } catch {
1917
- badOffset = lineOffset;
1918
- break;
1805
+ response = await fetchImpl(`${binding.baseUrl}${rel}`, init);
1806
+ } catch (error) {
1807
+ const err = error instanceof Error ? error : new Error(String(error));
1808
+ if (callerSignal?.aborted)
1809
+ return { ok: false, retryable: false, error: err };
1810
+ return { ok: false, retryable: true, error: err };
1919
1811
  }
1920
- if (typeof entry?.seq !== "number" || entry.seq !== expectedSeq || entry.checksum !== checksumOf(entry.op)) {
1921
- badOffset = lineOffset;
1922
- break;
1923
- }
1924
- entries.push(entry);
1925
- expectedSeq++;
1812
+ } finally {
1813
+ clearTimeout(timer);
1814
+ if (callerSignal)
1815
+ callerSignal.removeEventListener("abort", onAbort);
1926
1816
  }
1927
- if (badOffset !== null) {
1928
- torn = true;
1817
+ const authenticationFailure = response.status === 401 || response.status === 403;
1818
+ let parsed = undefined;
1819
+ if (authenticationFailure) {
1929
1820
  try {
1930
- truncateSync(this.filePath, badOffset);
1931
- repaired = true;
1821
+ await response.body?.cancel();
1932
1822
  } catch {}
1823
+ } else {
1824
+ const text = await response.text();
1825
+ if (text.length > 0) {
1826
+ try {
1827
+ parsed = JSON.parse(text);
1828
+ } catch {
1829
+ parsed = text;
1830
+ }
1831
+ }
1933
1832
  }
1934
- return {
1935
- entries,
1936
- torn,
1937
- repaired,
1938
- liveClaims: (nowMs) => reconstructLiveClaims(entries, nowMs)
1939
- };
1940
- }
1941
- close() {}
1942
- }
1943
- function cumulativeOffset(lines, index) {
1944
- let offset = 0;
1945
- for (let i = 0;i < index; i++) {
1946
- offset += Buffer.byteLength(lines[i]) + 1;
1947
- }
1948
- return offset;
1949
- }
1950
- function reconstructLiveClaims(entries, nowMs = Date.now()) {
1951
- const claims = new Map;
1952
- for (const entry of entries) {
1953
- const op = entry.op;
1954
- if (op.op === "claim_acquired") {
1955
- claims.set(op.runId, { worker: op.worker, fencing: op.fencing, expiresAtMs: Date.parse(op.expiresAt) });
1956
- } else if (op.op === "claim_released") {
1957
- const existing = claims.get(op.runId);
1958
- if (existing && existing.fencing === op.fencing) {
1959
- claims.delete(op.runId);
1833
+ if (!response.ok) {
1834
+ if (response.status >= 300 && response.status < 400) {
1835
+ return {
1836
+ ok: false,
1837
+ retryable: false,
1838
+ error: new HasnaHttpError(method, rel, response.status, parsed)
1839
+ };
1840
+ }
1841
+ if (authenticationFailure) {
1842
+ return {
1843
+ ok: false,
1844
+ retryable: false,
1845
+ error: new HasnaHttpError(method, rel, response.status, undefined, {
1846
+ source: credential.source,
1847
+ tier: credential.tier,
1848
+ guidance: authFailureGuidance(credential)
1849
+ })
1850
+ };
1960
1851
  }
1852
+ const retry = resolveRetry(opts.retry);
1853
+ const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
1854
+ return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
1855
+ }
1856
+ return { ok: true, value: parsed };
1857
+ }
1858
+ async function request(method, path, body, opts = {}) {
1859
+ const upper = method.toUpperCase();
1860
+ const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
1861
+ const retry = resolveRetry(opts.retry);
1862
+ const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
1863
+ const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
1864
+ let finalize;
1865
+ if (requestBindingProvider) {
1866
+ const prepared = requestBindingProvider();
1867
+ finalize = typeof prepared === "function" ? prepared : await prepared;
1868
+ } else {
1869
+ const credential = await resolveRequestCredential(options.name, options.apiKey);
1870
+ finalize = () => ({ baseUrl: base, credential });
1871
+ }
1872
+ let binding;
1873
+ const bind = () => binding ??= finalize();
1874
+ let last = null;
1875
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
1876
+ const result = await once(upper, rel, body, opts, bind);
1877
+ if (result.ok)
1878
+ return result.value;
1879
+ last = result;
1880
+ const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
1881
+ if (!canRetry)
1882
+ break;
1883
+ const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
1884
+ const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
1885
+ await sleep(backoff + jitter);
1961
1886
  }
1887
+ throw last.error;
1962
1888
  }
1963
- for (const [runId, claim] of claims) {
1964
- if (claim.expiresAtMs <= nowMs)
1965
- claims.delete(runId);
1966
- }
1967
- return claims;
1968
- }
1969
-
1970
- // src/agent-tools.ts
1971
- function textResult(payload) {
1972
- return { content: [{ type: "text", text: JSON.stringify(payload) }], isError: false };
1889
+ return {
1890
+ baseUrl: base,
1891
+ request,
1892
+ get: (path, opts) => request("GET", path, undefined, opts),
1893
+ post: (path, body, opts) => request("POST", path, body, opts),
1894
+ put: (path, body, opts) => request("PUT", path, body, opts),
1895
+ patch: (path, body, opts) => request("PATCH", path, body, opts),
1896
+ del: (path, body, opts) => request("DELETE", path, body, opts)
1897
+ };
1973
1898
  }
1974
- function workflowsTools(_service) {
1975
- return [
1976
- {
1977
- name: "workflows_version",
1978
- description: "Report the installed @hasna/workflows version.",
1979
- inputSchema: { type: "object", properties: {} }
1980
- },
1981
- {
1982
- name: "workflows_health",
1983
- description: "Report the workflows service health (service, version, pid, uptime).",
1984
- inputSchema: { type: "object", properties: {} }
1985
- },
1986
- {
1987
- name: "workflows_ready",
1988
- description: "Report the workflows service readiness (version check).",
1989
- inputSchema: { type: "object", properties: {} }
1990
- },
1991
- {
1992
- name: "workflows_validate",
1993
- description: "Validate a workflow graph (JSON object with name, version, nodes).",
1994
- inputSchema: {
1995
- type: "object",
1996
- properties: { graph: { type: "object", description: "the workflow graph" } },
1997
- required: ["graph"]
1998
- }
1999
- },
2000
- {
2001
- name: "workflows_run",
2002
- description: "Run a workflow graph to a terminal state (bounded cycles; command steps execute; lane steps need the lane substrate).",
2003
- inputSchema: {
2004
- type: "object",
2005
- properties: {
2006
- graph: { type: "object", description: "the workflow graph" },
2007
- context: { type: "object", description: "optional run context" }
2008
- },
2009
- required: ["graph"]
2010
- }
2011
- },
2012
- {
2013
- name: "workflows_runs_list",
2014
- description: "List recent runs from the local store.",
2015
- inputSchema: {
2016
- type: "object",
2017
- properties: {
2018
- status: { type: "string", description: "optional status filter" },
2019
- limit: { type: "number", description: "max rows (default 100)" }
2020
- }
2021
- }
2022
- },
2023
- {
2024
- name: "workflows_lanes_list",
2025
- description: "List the four lane adapters with their wired-vs-not-ready-with-reason registry shape.",
2026
- inputSchema: { type: "object", properties: {} }
1899
+ function createClientTransport(name, env = process.env, overrides) {
1900
+ const credentialOptions = overrides?.credentials;
1901
+ const snapshotOptions = {
1902
+ ...credentialOptions ? { credentials: credentialOptions } : {},
1903
+ ...overrides?.bindUnpairedCredentialsToFleet === undefined ? {} : { bindUnpairedCredentialsToFleet: overrides.bindUnpairedCredentialsToFleet }
1904
+ };
1905
+ const resolution = resolveClientTransportSnapshot(name, env, snapshotOptions).resolution;
1906
+ const sameBinding = (left, right) => left.resolution.baseUrl === right.resolution.baseUrl && left.credential.apiKey === right.credential.apiKey && left.credential.pointerVaultKey === right.credential.pointerVaultKey && left.credential.source === right.credential.source && left.credential.tier === right.credential.tier;
1907
+ const unstableConfiguration = () => new ClientTransportConfigurationError(name, "The configured service authority or credential changed while a request was being prepared; no authenticated request was sent.");
1908
+ const assertPinnedAuthority = (snapshot) => {
1909
+ if (snapshot.resolution.baseUrl !== resolution.baseUrl) {
1910
+ throw new ClientTransportConfigurationError(name, "The configured service authority changed; rebuild the client before sending credentials.");
2027
1911
  }
2028
- ];
1912
+ };
1913
+ const requestBindingProvider = () => {
1914
+ const first = resolveClientTransportSnapshot(name, env, snapshotOptions);
1915
+ assertPinnedAuthority(first);
1916
+ const finalize = (reviewed2, completed) => () => {
1917
+ const final = resolveClientTransportSnapshot(name, env, snapshotOptions);
1918
+ if (!sameBinding(reviewed2, final))
1919
+ throw unstableConfiguration();
1920
+ assertPinnedAuthority(final);
1921
+ return {
1922
+ baseUrl: final.resolution.baseUrl,
1923
+ credential: completed ?? currentCredential(name, () => final.credential)
1924
+ };
1925
+ };
1926
+ if (first.credential.tier !== "pointer")
1927
+ return finalize(first);
1928
+ const reviewed = resolveClientTransportSnapshot(name, env, snapshotOptions);
1929
+ if (!sameBinding(first, reviewed))
1930
+ throw unstableConfiguration();
1931
+ assertPinnedAuthority(reviewed);
1932
+ return resolveRequestCredential(name, () => reviewed.credential, env).then((credential) => finalize(reviewed, credential));
1933
+ };
1934
+ return {
1935
+ transport: "http",
1936
+ client: createHasnaHttpTransportInternal({
1937
+ name,
1938
+ baseUrl: resolution.baseUrl,
1939
+ apiKey: () => {
1940
+ throw new Error("The authenticated request binding provider was not invoked.");
1941
+ },
1942
+ ...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
1943
+ ...overrides?.headers ? { headers: overrides.headers } : {},
1944
+ ...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
1945
+ ...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
1946
+ ...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
1947
+ }, requestBindingProvider),
1948
+ resolution
1949
+ };
1950
+ }
1951
+
1952
+ // src/client-error.ts
1953
+ class WorkflowInputError extends Error {
1954
+ }
1955
+ var guidance = {
1956
+ invalid_graph: "graph must match the workflow graph schema; use workflows validate <file>",
1957
+ graph_required: "body.graph must be a workflow graph object",
1958
+ invalid_context: "context must be a JSON object",
1959
+ reserved_context: "context.__wf and context.steps are reserved",
1960
+ invalid_max_cycles: "maxCycles must be an integer from 1 to 2000",
1961
+ invalid_idempotency_key: "idempotencyKey must contain 1 to 128 ASCII letters, digits, or _.:-",
1962
+ invalid_query: "limit must be 1 to 1000 and status must be a supported run status",
1963
+ invalid_run_id: "run id must contain 1 to 128 ASCII letters, digits, underscores or hyphens",
1964
+ invalid_lane: "lane must be claude, codex, cursor or grok",
1965
+ run_not_found: "run id does not exist",
1966
+ idempotency_conflict: "idempotencyKey is already bound to a different request",
1967
+ invalid_run_status: "run status does not permit this action",
1968
+ run_status_conflict: "run changed before this action",
1969
+ execution_active: "previous execution must finish or be reconciled before resume",
1970
+ graph_unavailable: "legacy run requires its original graph from the server operator",
1971
+ invalid_schema: "PostgreSQL workflow schema requires explicit initialization or a reviewed migration",
1972
+ storage_unavailable: "PostgreSQL workflow storage is unavailable"
1973
+ };
1974
+ function workflowErrorMessage(error) {
1975
+ if (error instanceof WorkflowInputError)
1976
+ return error.message;
1977
+ if (error instanceof ClientCredentialMissingError)
1978
+ return "Workflows credential is missing; configure it through this station's supported credential resolver";
1979
+ if (error instanceof ClientTransportConfigurationError)
1980
+ return "Workflows API URL or credential binding was refused; use an absolute HTTPS URL and its paired credential (exact loopback HTTP is supported)";
1981
+ if (error instanceof HasnaHttpError) {
1982
+ const code = error.body && typeof error.body === "object" ? error.body.error : undefined;
1983
+ const known = typeof code === "string" && Object.hasOwn(guidance, code) ? `: ${guidance[code]}` : "";
1984
+ return `Workflows API returned HTTP ${error.status}${known}`;
1985
+ }
1986
+ return "Workflows request failed; verify the API connection and configured credential";
2029
1987
  }
2030
- async function callWorkflowTool(service, name, args) {
1988
+
1989
+ // src/client-tools.ts
1990
+ async function callClientTool(client, name, args) {
2031
1991
  switch (name) {
2032
1992
  case "workflows_version":
2033
- return textResult({ service: service.name, version: service.version });
1993
+ return textResult({ service: "workflows", version: packageVersion() });
1994
+ case "workflows_validate":
1995
+ return textResult(validateGraph(args.graph));
2034
1996
  case "workflows_health":
2035
- return textResult(service.health());
1997
+ return textResult(await client().health());
2036
1998
  case "workflows_ready":
2037
- return textResult(service.ready());
2038
- case "workflows_validate": {
2039
- const graph = args.graph;
2040
- const result = validateGraph(graph);
2041
- return textResult(result);
2042
- }
1999
+ return textResult(await client().ready());
2043
2000
  case "workflows_run": {
2044
- const graph = args.graph;
2045
- const store = openStore(service.config.dataDir);
2046
- const wal = SessionWAL.open(service.config.dataDir);
2047
- try {
2048
- const final = await runGraphToCompletion(store, wal, graph, args.context ?? {});
2049
- return textResult({
2050
- runId: final.id,
2051
- status: final.status,
2052
- error: final.error ?? null,
2053
- result: final.resultJson ? JSON.parse(final.resultJson) : null
2054
- });
2055
- } finally {
2056
- store.close();
2057
- }
2058
- }
2059
- case "workflows_runs_list": {
2060
- const store = openStore(service.config.dataDir);
2061
- try {
2062
- const status = typeof args.status === "string" ? args.status : undefined;
2063
- const limit = typeof args.limit === "number" ? args.limit : 100;
2064
- return textResult(store.listRuns({ status, limit }));
2065
- } finally {
2066
- store.close();
2067
- }
2068
- }
2001
+ if (typeof args.idempotencyKey !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(args.idempotencyKey))
2002
+ throw new WorkflowInputError("workflows_run requires a stable idempotencyKey with 1 to 128 ASCII identity characters");
2003
+ return textResult(await client().triggerRun(args.graph, args.context ?? {}, {
2004
+ idempotencyKey: args.idempotencyKey,
2005
+ maxCycles: args.maxCycles
2006
+ }));
2007
+ }
2008
+ case "workflows_runs_list":
2009
+ return textResult(await client().listRuns({ status: args.status, limit: args.limit }));
2069
2010
  case "workflows_lanes_list":
2070
- return textResult(await laneInventory());
2011
+ return textResult(await client().lanes());
2071
2012
  default:
2072
- throw new Error(`Unknown tool: ${name}`);
2013
+ throw new Error("unknown_workflows_tool");
2073
2014
  }
2074
2015
  }
2075
- // package.json
2076
- var package_default = {
2077
- name: "@hasna/workflows",
2078
- version: "0.1.4",
2079
- description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
2080
- type: "module",
2081
- main: "./dist/index.js",
2082
- types: "./types/index.d.ts",
2083
- bin: {
2084
- workflows: "dist/cli/index.js",
2085
- "workflows-mcp": "dist/mcp/index.js",
2086
- "workflows-serve": "dist/serve/index.js"
2087
- },
2088
- exports: {
2089
- ".": {
2090
- types: "./types/index.d.ts",
2091
- import: "./dist/index.js"
2092
- },
2093
- "./sdk": {
2094
- types: "./types/index.d.ts",
2095
- import: "./dist/index.js"
2096
- },
2097
- "./cli": {
2098
- types: "./types/cli/index.d.ts",
2099
- import: "./dist/cli/index.js"
2100
- }
2101
- },
2102
- files: [
2103
- "dist",
2104
- "types",
2105
- "README.md",
2106
- "LICENSE",
2107
- "hasna.contract.json"
2108
- ],
2109
- scripts: {
2110
- build: "bun run build:runtime && bun run build:types",
2111
- "build:runtime": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun --packages external && bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external && bun build src/serve/index.ts --outdir dist/serve --target bun --packages external && bun build src/index.ts src/service.ts src/handlers.ts src/agent-tools.ts src/types.ts --root src --outdir dist --target bun --packages external",
2112
- "build:types": "rm -rf types && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir types",
2113
- "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
2114
- "contract:check": "bun scripts/contracts-cli.mjs repo-conformance .",
2115
- typecheck: "tsc --noEmit",
2116
- test: "bun test",
2117
- "test:pg": 'WORKFLOWS_TEST_DATABASE_URL="${WORKFLOWS_TEST_DATABASE_URL:?point it at a throwaway Postgres; the live-PG gate must not pass without one}" WORKFLOWS_REQUIRE_POSTGRES=1 bun test src/db/pg-live.test.ts',
2118
- prepack: "bun run build && bun run artifact-scan",
2119
- prepublishOnly: "bun run test && bun run build"
2120
- },
2121
- keywords: [
2122
- "workflows",
2123
- "graph",
2124
- "automation",
2125
- "cli",
2126
- "typescript",
2127
- "bun",
2128
- "hasna"
2129
- ],
2130
- publishConfig: {
2131
- registry: "https://registry.npmjs.org",
2132
- access: "public"
2133
- },
2134
- repository: {
2135
- type: "git",
2136
- url: "git+https://github.com/hasna/apps.git"
2137
- },
2138
- homepage: "https://github.com/hasna/apps/tree/main/apps/workflows",
2139
- bugs: {
2140
- url: "https://github.com/hasna/apps/issues"
2141
- },
2142
- engines: {
2143
- bun: ">=1.0.0"
2144
- },
2145
- author: "Hasna",
2146
- license: "Apache-2.0",
2147
- dependencies: {
2148
- "@anthropic-ai/claude-agent-sdk": "0.3.234",
2149
- "@cursor/sdk": "^1.0.28",
2150
- "@hasna/paths": "0.1.0",
2151
- "@openai/codex-sdk": "0.147.0",
2152
- commander: "^13.1.0"
2153
- },
2154
- devDependencies: {
2155
- "@hasna/contracts": "0.14.2",
2156
- "@types/bun": "1.3.14",
2157
- "@types/pg": "^8.15.6",
2158
- pg: "^8.16.3",
2159
- typescript: "^5.7.3"
2160
- }
2161
- };
2162
2016
 
2163
- // src/paths.ts
2164
- import { existsSync as existsSync2 } from "fs";
2165
- import { homedir } from "os";
2166
- import { join as join4 } from "path";
2167
- import { dataDir } from "@hasna/paths";
2168
- var APP = "workflows";
2169
- var STORE_DB_FILE = "workflows.db";
2170
- function getHomeDir(env = process.env) {
2171
- return env["HOME"] || env["USERPROFILE"] || homedir();
2172
- }
2173
- function getLegacyDataDir(env = process.env) {
2174
- return join4(getHomeDir(env), ".hasna", APP);
2175
- }
2176
- function getResolverDataDir(env = process.env) {
2177
- return dataDir({ app: APP, home: getHomeDir(env), env });
2178
- }
2179
- function adoptResolverDataDir(resolved, env = process.env) {
2180
- const dataOverride = env.HASNA_DATA_HOME;
2181
- if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
2182
- return true;
2183
- return existsSync2(join4(resolved, STORE_DB_FILE));
2184
- }
2185
- function getEffectiveDataDir(env = process.env) {
2186
- const resolved = getResolverDataDir(env);
2187
- return adoptResolverDataDir(resolved, env) ? resolved : getLegacyDataDir(env);
2017
+ // src/sdk/generated-api.ts
2018
+ class WorkflowsApi {
2019
+ async inspectDaemon() {
2020
+ return this.apiRequest("GET", `/daemon/status`, {
2021
+ body: undefined,
2022
+ query: undefined
2023
+ });
2024
+ }
2025
+ async getHealth() {
2026
+ return this.apiRequest("GET", `/health`, {
2027
+ body: undefined,
2028
+ query: undefined
2029
+ });
2030
+ }
2031
+ async listLanes() {
2032
+ return this.apiRequest("GET", `/lanes`, {
2033
+ body: undefined,
2034
+ query: undefined
2035
+ });
2036
+ }
2037
+ async probeLane(lane) {
2038
+ return this.apiRequest("GET", `/lanes/${encodeURIComponent(String(lane))}`, {
2039
+ body: undefined,
2040
+ query: undefined
2041
+ });
2042
+ }
2043
+ async listServerMachines() {
2044
+ return this.apiRequest("GET", `/machines/list`, {
2045
+ body: undefined,
2046
+ query: undefined
2047
+ });
2048
+ }
2049
+ async inspectServerMachine() {
2050
+ return this.apiRequest("GET", `/machines/status`, {
2051
+ body: undefined,
2052
+ query: undefined
2053
+ });
2054
+ }
2055
+ async listMemoRecords() {
2056
+ return this.apiRequest("GET", `/memos/list`, {
2057
+ body: undefined,
2058
+ query: undefined
2059
+ });
2060
+ }
2061
+ async getReadiness() {
2062
+ return this.apiRequest("GET", `/ready`, {
2063
+ body: undefined,
2064
+ query: undefined
2065
+ });
2066
+ }
2067
+ async listRunRecords(query) {
2068
+ return this.apiRequest("GET", `/runs`, {
2069
+ body: undefined,
2070
+ query
2071
+ });
2072
+ }
2073
+ async getRunRecord(id) {
2074
+ return this.apiRequest("GET", `/runs/${encodeURIComponent(String(id))}`, {
2075
+ body: undefined,
2076
+ query: undefined
2077
+ });
2078
+ }
2079
+ async cancelWorkflow(id) {
2080
+ return this.apiRequest("POST", `/runs/${encodeURIComponent(String(id))}/cancel`, {
2081
+ body: undefined,
2082
+ query: undefined
2083
+ });
2084
+ }
2085
+ async getRunEvents(id) {
2086
+ return this.apiRequest("GET", `/runs/${encodeURIComponent(String(id))}/events`, {
2087
+ body: undefined,
2088
+ query: undefined
2089
+ });
2090
+ }
2091
+ async getRunNodes(id) {
2092
+ return this.apiRequest("GET", `/runs/${encodeURIComponent(String(id))}/nodes`, {
2093
+ body: undefined,
2094
+ query: undefined
2095
+ });
2096
+ }
2097
+ async resumeWorkflow(id) {
2098
+ return this.apiRequest("POST", `/runs/${encodeURIComponent(String(id))}/resume`, {
2099
+ body: undefined,
2100
+ query: undefined
2101
+ });
2102
+ }
2103
+ async listSessionEvents() {
2104
+ return this.apiRequest("GET", `/sessions/list`, {
2105
+ body: undefined,
2106
+ query: undefined
2107
+ });
2108
+ }
2109
+ async inspectSession() {
2110
+ return this.apiRequest("GET", `/sessions/pull`, {
2111
+ body: undefined,
2112
+ query: undefined
2113
+ });
2114
+ }
2115
+ async triggerWorkflow(body) {
2116
+ return this.apiRequest("POST", `/trigger`, {
2117
+ body,
2118
+ query: undefined
2119
+ });
2120
+ }
2121
+ async validateWorkflow(body) {
2122
+ return this.apiRequest("POST", `/validate`, {
2123
+ body,
2124
+ query: undefined
2125
+ });
2126
+ }
2127
+ async getVersion() {
2128
+ return this.apiRequest("GET", `/version`, {
2129
+ body: undefined,
2130
+ query: undefined
2131
+ });
2132
+ }
2188
2133
  }
2189
2134
 
2190
- // src/service.ts
2191
- var DEFAULT_PORT = 8790;
2192
- var DEFAULT_HOST = "127.0.0.1";
2193
- function packageVersion() {
2194
- return package_default.version;
2195
- }
2196
- function envString(names) {
2197
- for (const name of names) {
2198
- const value = process.env[name];
2199
- if (value !== undefined && value !== "")
2200
- return value;
2135
+ // src/identity.ts
2136
+ import { createHash } from "crypto";
2137
+ import { hostname } from "os";
2138
+ function name(value) {
2139
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$/.test(value) || /^(owner|andrei-hasna)$/i.test(value)) {
2140
+ throw new WorkflowInputError("agent must be a non-owner identity with 1 to 128 identity characters");
2201
2141
  }
2202
- return;
2203
- }
2204
- function envInt(names, fallback) {
2205
- const value = envString(names);
2206
- if (value === undefined)
2207
- return fallback;
2208
- const parsed = Number(value);
2209
- return Number.isFinite(parsed) ? parsed : fallback;
2142
+ return value;
2210
2143
  }
2211
- function resolveWorkflowsConfig(overrides = {}) {
2212
- return {
2213
- port: overrides.port ?? envInt(["HASNA_WORKFLOWS_PORT", "WORKFLOWS_PORT"], DEFAULT_PORT),
2214
- host: overrides.host ?? envString(["HASNA_WORKFLOWS_HOST", "WORKFLOWS_HOST"]) ?? DEFAULT_HOST,
2215
- dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? getEffectiveDataDir(),
2216
- apiUrl: overrides.apiUrl ?? envString(["WORKFLOWS_API_URL"]),
2217
- apiKey: overrides.apiKey ?? envString(["WORKFLOWS_API_KEY"])
2218
- };
2144
+ function station(env) {
2145
+ const value = (env.HASNA_STATION ?? hostname()).split(".")[0].toLowerCase();
2146
+ return value.replace(/[^a-z0-9-]/g, "-").slice(0, 63) || "unknown";
2147
+ }
2148
+ function workflowAgent(env = process.env, explicit) {
2149
+ if (explicit !== undefined)
2150
+ return { name: name(explicit), source: "argument" };
2151
+ if (env.HASNA_WORKFLOWS_AGENT !== undefined)
2152
+ return { name: name(env.HASNA_WORKFLOWS_AGENT), source: "HASNA_WORKFLOWS_AGENT" };
2153
+ for (const [harness, key] of [["claude", "CLAUDE_CODE_SESSION_ID"], ["codex", "CODEX_THREAD_ID"]]) {
2154
+ if (env[key] !== undefined)
2155
+ return {
2156
+ name: `${harness}-${createHash("sha256").update(JSON.stringify([env[key], null])).digest("hex").slice(0, 32)}`,
2157
+ source: key
2158
+ };
2159
+ }
2160
+ return { name: `cli@${station(env)}`, source: "station" };
2161
+ }
2162
+ function workflowMcpAgent(env, harness, explicit, meta) {
2163
+ if (explicit !== undefined) {
2164
+ if (typeof explicit !== "string")
2165
+ throw new WorkflowInputError("agent must be a string");
2166
+ return { name: name(explicit), source: "argument" };
2167
+ }
2168
+ if (env.HASNA_WORKFLOWS_AGENT !== undefined)
2169
+ return workflowAgent(env);
2170
+ const label = harness.toLowerCase().replace(/[^a-z0-9_-]/g, "-").slice(0, 32) || "mcp";
2171
+ const id = meta && typeof meta === "object" ? meta["claudecode/agentId"] : undefined;
2172
+ if (typeof id === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(id))
2173
+ return { name: `${label}:${id}@${station(env)}`, source: "mcp-meta" };
2174
+ const native = workflowAgent(env);
2175
+ if (native.source !== "station")
2176
+ return native;
2177
+ return { name: `${label}@${station(env)}`, source: "mcp-client" };
2219
2178
  }
2220
2179
 
2221
- class WorkflowsService {
2180
+ // src/client.ts
2181
+ class WorkflowsClient extends WorkflowsApi {
2222
2182
  name = "workflows";
2223
- version;
2224
- config;
2225
- startedAt = Date.now();
2226
- constructor(config = {}) {
2227
- this.config = resolveWorkflowsConfig(config);
2228
- this.version = packageVersion();
2183
+ resolution;
2184
+ http;
2185
+ constructor(options = {}) {
2186
+ super();
2187
+ const env = { ...options.env ?? process.env };
2188
+ if (options.apiUrl !== undefined) {
2189
+ env.HASNA_WORKFLOWS_API_URL = options.apiUrl;
2190
+ delete env.WORKFLOWS_API_URL;
2191
+ }
2192
+ const { client, resolution } = createClientTransport("workflows", env, {
2193
+ credentials: { keychain: { enabled: options.env === undefined }, ...options.apiKey === undefined ? {} : { apiKey: options.apiKey } },
2194
+ bindUnpairedCredentialsToFleet: true,
2195
+ fetchImpl: options.fetchImpl,
2196
+ timeoutMs: options.timeoutMs ?? 120000,
2197
+ retry: false,
2198
+ headers: { "user-agent": `hasna-workflows-sdk/${packageVersion()}`, "x-hasna-agent": workflowAgent(env, options.agent).name }
2199
+ });
2200
+ this.http = client;
2201
+ this.resolution = resolution;
2202
+ }
2203
+ request(method, path, body) {
2204
+ return this.http.request(method, path, body);
2205
+ }
2206
+ apiRequest(method, path, opts) {
2207
+ const query = new URLSearchParams;
2208
+ for (const [key, value] of Object.entries(opts.query ?? {}))
2209
+ if (value !== undefined && value !== null)
2210
+ query.set(key, String(value));
2211
+ return this.request(method, `${path}${query.size ? `?${query}` : ""}`, opts.body);
2229
2212
  }
2230
2213
  health() {
2231
- return {
2232
- ok: true,
2233
- service: this.name,
2234
- version: this.version,
2235
- pid: process.pid,
2236
- uptimeMs: Date.now() - this.startedAt
2237
- };
2214
+ return this.getHealth();
2238
2215
  }
2239
2216
  ready() {
2240
- return {
2241
- ok: true,
2242
- service: this.name,
2243
- version: this.version,
2244
- checks: { version: "ok" }
2245
- };
2217
+ return this.getReadiness();
2246
2218
  }
2247
- async triggerRun(graph, context = {}, opts = {}) {
2248
- const store = openStore(this.config.dataDir);
2249
- const wal = SessionWAL.open(this.config.dataDir);
2250
- try {
2251
- if (opts.idempotencyKey) {
2252
- const existing = findRunByIdempotencyKey(store, opts.idempotencyKey);
2253
- if (existing) {
2254
- return {
2255
- runId: existing.id,
2256
- status: existing.status,
2257
- error: existing.error ?? null,
2258
- result: existing.resultJson ? JSON.parse(existing.resultJson) : null,
2259
- reused: true
2260
- };
2261
- }
2262
- }
2263
- const contextWithKey = opts.idempotencyKey ? {
2264
- ...context,
2265
- __wf: {
2266
- cursor: undefined,
2267
- loops: {},
2268
- completedLoops: {},
2269
- ...context?.__wf ?? {},
2270
- idempotencyKey: opts.idempotencyKey
2271
- }
2272
- } : context;
2273
- const maxCycles = Math.min(Math.max(opts.maxCycles ?? 500, 1), 2000);
2274
- const final = await runGraphToCompletion(store, wal, graph, contextWithKey, { maxCycles });
2275
- return {
2276
- runId: final.id,
2277
- status: final.status,
2278
- error: final.error ?? null,
2279
- result: final.resultJson ? JSON.parse(final.resultJson) : null,
2280
- reused: false
2281
- };
2282
- } finally {
2283
- store.close();
2284
- }
2219
+ version() {
2220
+ return this.getVersion();
2221
+ }
2222
+ validate(graph) {
2223
+ return this.validateWorkflow({ graph });
2224
+ }
2225
+ triggerRun(graph, context = {}, opts = {}) {
2226
+ return this.triggerWorkflow({ graph, context, ...opts });
2227
+ }
2228
+ listRuns(options = {}) {
2229
+ return this.listRunRecords(options);
2230
+ }
2231
+ getRun(id) {
2232
+ return this.getRunRecord(id);
2233
+ }
2234
+ listNodes(id) {
2235
+ return this.getRunNodes(id);
2236
+ }
2237
+ events(id) {
2238
+ return this.getRunEvents(id);
2239
+ }
2240
+ cancelRun(id) {
2241
+ return this.cancelWorkflow(id);
2242
+ }
2243
+ resumeRun(id) {
2244
+ return this.resumeWorkflow(id);
2245
+ }
2246
+ lanes() {
2247
+ return this.listLanes();
2285
2248
  }
2286
2249
  }
2287
- function createWorkflowsService(config = {}) {
2288
- return new WorkflowsService(config);
2250
+ function createWorkflowsClient(options = {}) {
2251
+ return new WorkflowsClient(options);
2289
2252
  }
2290
2253
 
2291
2254
  // src/mcp/index.ts
@@ -2307,34 +2270,60 @@ if (argv.includes("--help") || argv.includes("-h")) {
2307
2270
  console.log(HELP_TEXT);
2308
2271
  process.exit(0);
2309
2272
  }
2273
+ if (argv.some((argument) => argument !== "--stdio")) {
2274
+ console.error("unknown workflows-mcp argument; use --help");
2275
+ process.exit(2);
2276
+ }
2310
2277
  function respond(id, result) {
2311
- if (id === null || id === undefined)
2278
+ if (id === undefined)
2312
2279
  return;
2313
2280
  process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + `
2314
2281
  `);
2315
2282
  }
2316
2283
  function respondError(id, code, message) {
2317
- if (id === null || id === undefined)
2284
+ if (id === undefined)
2318
2285
  return;
2319
2286
  process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + `
2320
2287
  `);
2321
2288
  }
2322
- var service = createWorkflowsService();
2289
+ var harness = "mcp";
2290
+ var protocols = ["2025-06-18", "2025-03-26", "2024-11-05"];
2323
2291
  var rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
2324
2292
  rl.on("line", (line) => {
2325
2293
  const trimmed = line.trim();
2326
2294
  if (trimmed === "")
2327
2295
  return;
2328
- let msg;
2296
+ if (Buffer.byteLength(trimmed) > 1048576) {
2297
+ respondError(null, -32600, "Frame exceeds 1 MiB");
2298
+ return;
2299
+ }
2300
+ let candidate;
2329
2301
  try {
2330
- msg = JSON.parse(trimmed);
2302
+ candidate = JSON.parse(trimmed);
2331
2303
  } catch {
2304
+ respondError(null, -32700, "Parse error");
2305
+ return;
2306
+ }
2307
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
2308
+ respondError(null, -32600, "Invalid request");
2309
+ return;
2310
+ }
2311
+ const msg = candidate;
2312
+ if (msg.jsonrpc !== "2.0" || typeof msg.method !== "string" || msg.id !== undefined && msg.id !== null && typeof msg.id !== "number" && typeof msg.id !== "string") {
2313
+ respondError(null, -32600, "Invalid request");
2332
2314
  return;
2333
2315
  }
2334
- const method = msg.method ?? "";
2316
+ if (msg.params !== undefined && (!msg.params || typeof msg.params !== "object" || Array.isArray(msg.params))) {
2317
+ respondError(msg.id, -32602, "params must be an object");
2318
+ return;
2319
+ }
2320
+ const method = msg.method;
2335
2321
  if (method === "initialize") {
2336
- respond(msg.id ?? null, {
2337
- protocolVersion: "2024-11-05",
2322
+ const info = msg.params?.clientInfo;
2323
+ if (info && typeof info === "object" && typeof info.name === "string")
2324
+ harness = info.name;
2325
+ respond(msg.id, {
2326
+ protocolVersion: protocols.includes(msg.params?.protocolVersion) ? msg.params.protocolVersion : protocols[0],
2338
2327
  capabilities: { tools: {} },
2339
2328
  serverInfo: { name: "workflows-mcp", version: packageVersion() }
2340
2329
  });
@@ -2343,28 +2332,33 @@ rl.on("line", (line) => {
2343
2332
  if (method.startsWith("notifications/"))
2344
2333
  return;
2345
2334
  if (method === "ping") {
2346
- respond(msg.id ?? null, {});
2335
+ respond(msg.id, {});
2347
2336
  return;
2348
2337
  }
2349
2338
  if (method === "tools/list") {
2350
- respond(msg.id ?? null, { tools: workflowsTools(service) });
2339
+ respond(msg.id, { tools: workflowsTools() });
2351
2340
  return;
2352
2341
  }
2353
2342
  if (method === "tools/call") {
2354
2343
  const params = msg.params ?? {};
2355
- const name = typeof params.name === "string" ? params.name : "";
2344
+ const name2 = typeof params.name === "string" ? params.name : "";
2356
2345
  const args = params.arguments ?? {};
2346
+ if (!name2 || !args || typeof args !== "object" || Array.isArray(args)) {
2347
+ respondError(msg.id, -32602, "name must be a tool name and arguments must be an object");
2348
+ return;
2349
+ }
2357
2350
  (async () => {
2358
2351
  try {
2359
- respond(msg.id ?? null, await callWorkflowTool(service, name, args));
2352
+ const agent = workflowMcpAgent(process.env, harness, args.agent, params._meta);
2353
+ respond(msg.id, await callClientTool(() => createWorkflowsClient({ agent: agent.name }), name2, args));
2360
2354
  } catch (err) {
2361
- respond(msg.id ?? null, {
2362
- content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }],
2355
+ respond(msg.id, {
2356
+ content: [{ type: "text", text: workflowErrorMessage(err) }],
2363
2357
  isError: true
2364
2358
  });
2365
2359
  }
2366
2360
  })();
2367
2361
  return;
2368
2362
  }
2369
- respondError(msg.id ?? null, -32601, `Method not found: ${method}`);
2363
+ respondError(msg.id, -32601, "Method not found");
2370
2364
  });