@wopr-network/defcon 0.2.0 → 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.
Files changed (102) hide show
  1. package/dist/src/execution/cli.js +0 -0
  2. package/package.json +3 -2
  3. package/dist/api/router.d.ts +0 -24
  4. package/dist/api/router.js +0 -44
  5. package/dist/api/server.d.ts +0 -13
  6. package/dist/api/server.js +0 -280
  7. package/dist/api/wire-types.d.ts +0 -46
  8. package/dist/api/wire-types.js +0 -5
  9. package/dist/config/db-path.d.ts +0 -1
  10. package/dist/config/db-path.js +0 -1
  11. package/dist/config/exporter.d.ts +0 -3
  12. package/dist/config/exporter.js +0 -87
  13. package/dist/config/index.d.ts +0 -4
  14. package/dist/config/index.js +0 -4
  15. package/dist/config/seed-loader.d.ts +0 -10
  16. package/dist/config/seed-loader.js +0 -108
  17. package/dist/config/zod-schemas.d.ts +0 -165
  18. package/dist/config/zod-schemas.js +0 -283
  19. package/dist/cors.d.ts +0 -8
  20. package/dist/cors.js +0 -21
  21. package/dist/engine/constants.d.ts +0 -1
  22. package/dist/engine/constants.js +0 -1
  23. package/dist/engine/engine.d.ts +0 -69
  24. package/dist/engine/engine.js +0 -485
  25. package/dist/engine/event-emitter.d.ts +0 -9
  26. package/dist/engine/event-emitter.js +0 -19
  27. package/dist/engine/event-types.d.ts +0 -105
  28. package/dist/engine/event-types.js +0 -1
  29. package/dist/engine/flow-spawner.d.ts +0 -8
  30. package/dist/engine/flow-spawner.js +0 -28
  31. package/dist/engine/gate-command-validator.d.ts +0 -6
  32. package/dist/engine/gate-command-validator.js +0 -46
  33. package/dist/engine/gate-evaluator.d.ts +0 -12
  34. package/dist/engine/gate-evaluator.js +0 -233
  35. package/dist/engine/handlebars.d.ts +0 -9
  36. package/dist/engine/handlebars.js +0 -51
  37. package/dist/engine/index.d.ts +0 -12
  38. package/dist/engine/index.js +0 -7
  39. package/dist/engine/invocation-builder.d.ts +0 -18
  40. package/dist/engine/invocation-builder.js +0 -58
  41. package/dist/engine/on-enter.d.ts +0 -8
  42. package/dist/engine/on-enter.js +0 -102
  43. package/dist/engine/ssrf-guard.d.ts +0 -22
  44. package/dist/engine/ssrf-guard.js +0 -159
  45. package/dist/engine/state-machine.d.ts +0 -12
  46. package/dist/engine/state-machine.js +0 -74
  47. package/dist/execution/active-runner.d.ts +0 -45
  48. package/dist/execution/active-runner.js +0 -165
  49. package/dist/execution/admin-schemas.d.ts +0 -116
  50. package/dist/execution/admin-schemas.js +0 -125
  51. package/dist/execution/cli.d.ts +0 -57
  52. package/dist/execution/cli.js +0 -498
  53. package/dist/execution/handlers/admin.d.ts +0 -67
  54. package/dist/execution/handlers/admin.js +0 -200
  55. package/dist/execution/handlers/flow.d.ts +0 -25
  56. package/dist/execution/handlers/flow.js +0 -289
  57. package/dist/execution/handlers/query.d.ts +0 -31
  58. package/dist/execution/handlers/query.js +0 -64
  59. package/dist/execution/index.d.ts +0 -4
  60. package/dist/execution/index.js +0 -3
  61. package/dist/execution/mcp-helpers.d.ts +0 -42
  62. package/dist/execution/mcp-helpers.js +0 -23
  63. package/dist/execution/mcp-server.d.ts +0 -33
  64. package/dist/execution/mcp-server.js +0 -1020
  65. package/dist/execution/provision-worktree.d.ts +0 -16
  66. package/dist/execution/provision-worktree.js +0 -123
  67. package/dist/execution/tool-schemas.d.ts +0 -40
  68. package/dist/execution/tool-schemas.js +0 -44
  69. package/dist/logger.d.ts +0 -8
  70. package/dist/logger.js +0 -12
  71. package/dist/main.d.ts +0 -14
  72. package/dist/main.js +0 -28
  73. package/dist/repositories/drizzle/entity.repo.d.ts +0 -27
  74. package/dist/repositories/drizzle/entity.repo.js +0 -190
  75. package/dist/repositories/drizzle/event.repo.d.ts +0 -12
  76. package/dist/repositories/drizzle/event.repo.js +0 -24
  77. package/dist/repositories/drizzle/flow.repo.d.ts +0 -22
  78. package/dist/repositories/drizzle/flow.repo.js +0 -364
  79. package/dist/repositories/drizzle/gate.repo.d.ts +0 -16
  80. package/dist/repositories/drizzle/gate.repo.js +0 -98
  81. package/dist/repositories/drizzle/index.d.ts +0 -6
  82. package/dist/repositories/drizzle/index.js +0 -7
  83. package/dist/repositories/drizzle/invocation.repo.d.ts +0 -23
  84. package/dist/repositories/drizzle/invocation.repo.js +0 -199
  85. package/dist/repositories/drizzle/schema.d.ts +0 -1932
  86. package/dist/repositories/drizzle/schema.js +0 -155
  87. package/dist/repositories/drizzle/transition-log.repo.d.ts +0 -11
  88. package/dist/repositories/drizzle/transition-log.repo.js +0 -42
  89. package/dist/repositories/interfaces.d.ts +0 -321
  90. package/dist/repositories/interfaces.js +0 -2
  91. package/dist/utils/redact.d.ts +0 -2
  92. package/dist/utils/redact.js +0 -62
  93. package/gates/blocking-graph.d.ts +0 -26
  94. package/gates/blocking-graph.js +0 -102
  95. package/gates/test/bad-return-gate.d.ts +0 -1
  96. package/gates/test/bad-return-gate.js +0 -4
  97. package/gates/test/passing-gate.d.ts +0 -2
  98. package/gates/test/passing-gate.js +0 -3
  99. package/gates/test/slow-gate.d.ts +0 -2
  100. package/gates/test/slow-gate.js +0 -5
  101. package/gates/test/throwing-gate.d.ts +0 -1
  102. package/gates/test/throwing-gate.js +0 -3
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/defcon",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
@@ -24,7 +24,8 @@
24
24
  "gates"
25
25
  ],
26
26
  "scripts": {
27
- "build": "tsc",
27
+ "build": "rm -rf dist && tsc",
28
+ "prepublishOnly": "pnpm build",
28
29
  "dev": "tsx src/main.ts",
29
30
  "lint": "biome check src/",
30
31
  "lint:fix": "biome check --fix src/",
@@ -1,24 +0,0 @@
1
- export interface ParsedRequest {
2
- params: Record<string, string>;
3
- query: URLSearchParams;
4
- body: Record<string, unknown> | null;
5
- authorization?: string;
6
- }
7
- export interface ApiResponse {
8
- status: number;
9
- body: unknown;
10
- }
11
- type Handler = (req: ParsedRequest) => Promise<ApiResponse>;
12
- interface MatchResult {
13
- params: Record<string, string>;
14
- handler: Handler;
15
- longRunning?: boolean;
16
- }
17
- export declare class Router {
18
- private routes;
19
- add(method: string, path: string, handler: Handler, options?: {
20
- longRunning?: boolean;
21
- }): void;
22
- match(method: string, pathname: string): MatchResult | null;
23
- }
24
- export {};
@@ -1,44 +0,0 @@
1
- export class Router {
2
- routes = [];
3
- add(method, path, handler, options) {
4
- const paramNames = [];
5
- // Split on param segments, escape literal segments, then reassemble
6
- const patternStr = path
7
- .split(/(:([^/]+))/g)
8
- .map((segment, i) => {
9
- // Every 3rd token (i % 3 === 1) is the full ":name" match — replace with capture group
10
- if (i % 3 === 1) {
11
- paramNames.push(segment.slice(1));
12
- return "([^/]+)";
13
- }
14
- // Every 3rd+1 token (i % 3 === 2) is the captured name — skip (already handled above)
15
- if (i % 3 === 2)
16
- return "";
17
- // Literal segment — escape regex metacharacters
18
- return segment.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
19
- })
20
- .join("");
21
- this.routes.push({
22
- method,
23
- pattern: new RegExp(`^${patternStr}$`),
24
- paramNames,
25
- handler,
26
- longRunning: options?.longRunning,
27
- });
28
- }
29
- match(method, pathname) {
30
- for (const route of this.routes) {
31
- if (route.method !== method)
32
- continue;
33
- const m = pathname.match(route.pattern);
34
- if (m) {
35
- const params = {};
36
- route.paramNames.forEach((name, i) => {
37
- params[name] = m[i + 1];
38
- });
39
- return { params, handler: route.handler, longRunning: route.longRunning };
40
- }
41
- }
42
- return null;
43
- }
44
- }
@@ -1,13 +0,0 @@
1
- import http from "node:http";
2
- import type { Engine } from "../engine/engine.js";
3
- import type { McpServerDeps } from "../execution/mcp-server.js";
4
- import type { Logger } from "../logger.js";
5
- export interface HttpServerDeps {
6
- engine: Engine;
7
- mcpDeps: McpServerDeps;
8
- adminToken?: string;
9
- workerToken?: string;
10
- corsOrigin?: string;
11
- logger?: Logger;
12
- }
13
- export declare function createHttpServer(deps: HttpServerDeps): http.Server;
@@ -1,280 +0,0 @@
1
- import { createHash, timingSafeEqual } from "node:crypto";
2
- import http from "node:http";
3
- import { callToolHandler } from "../execution/mcp-server.js";
4
- import { consoleLogger } from "../logger.js";
5
- import { Router } from "./router.js";
6
- function extractBearerToken(header) {
7
- if (!header)
8
- return undefined;
9
- const lower = header.toLowerCase();
10
- if (!lower.startsWith("bearer "))
11
- return undefined;
12
- return header.slice(7).trim() || undefined;
13
- }
14
- function requireWorkerToken(deps, req) {
15
- const configuredToken = deps.workerToken?.trim() || undefined; // treat "" and whitespace-only as unset
16
- if (!configuredToken)
17
- return null; // open mode
18
- const callerToken = extractBearerToken(req.authorization);
19
- if (!callerToken) {
20
- return { status: 401, body: { error: "Unauthorized: worker endpoints require authentication." } };
21
- }
22
- const hashA = createHash("sha256").update(configuredToken.trim()).digest();
23
- const hashB = createHash("sha256").update(callerToken.trim()).digest();
24
- if (!timingSafeEqual(hashA, hashB)) {
25
- return { status: 401, body: { error: "Unauthorized: worker endpoints require authentication." } };
26
- }
27
- return null;
28
- }
29
- const BODY_SIZE_LIMIT = 1024 * 1024; // 1MB
30
- function readBody(req) {
31
- return new Promise((resolve, reject) => {
32
- const chunks = [];
33
- let size = 0;
34
- let tooLarge = false;
35
- req.on("data", (chunk) => {
36
- size += chunk.length;
37
- if (size > BODY_SIZE_LIMIT) {
38
- tooLarge = true;
39
- req.destroy();
40
- return;
41
- }
42
- chunks.push(chunk);
43
- });
44
- req.on("end", () => resolve({ body: Buffer.concat(chunks).toString("utf8"), tooLarge }));
45
- req.on("error", (err) => {
46
- if (tooLarge)
47
- resolve({ body: "", tooLarge: true });
48
- else
49
- reject(err);
50
- });
51
- });
52
- }
53
- /** Unwrap MCP tool result into HTTP response */
54
- function mcpResultToApi(result) {
55
- const text = result.content[0]?.text ?? "";
56
- let body;
57
- try {
58
- body = JSON.parse(text);
59
- }
60
- catch {
61
- body = { message: text };
62
- }
63
- if (result.isError) {
64
- const msg = typeof body === "object" && body !== null && "message" in body ? body.message : text;
65
- const msgStr = String(msg);
66
- if (msgStr.includes("not found") || msgStr.includes("Not found"))
67
- return { status: 404, body: { error: msgStr } };
68
- if (msgStr.includes("Unauthorized"))
69
- return { status: 401, body: { error: msgStr } };
70
- if (msgStr.includes("Validation error"))
71
- return { status: 400, body: { error: msgStr } };
72
- if (msgStr.includes("No active invocation"))
73
- return { status: 409, body: { error: msgStr } };
74
- return { status: 500, body: { error: msgStr } };
75
- }
76
- if (body === null)
77
- return { status: 204, body: null };
78
- return { status: 200, body };
79
- }
80
- export function createHttpServer(deps) {
81
- const router = new Router();
82
- // Ensure engine is set on mcpDeps
83
- deps.mcpDeps.engine = deps.engine;
84
- if (deps.logger)
85
- deps.mcpDeps.logger = deps.logger;
86
- // --- Status ---
87
- router.add("GET", "/api/status", async () => {
88
- const status = await deps.engine.getStatus();
89
- return { status: 200, body: status };
90
- });
91
- // --- Flow claim (cross-flow: no flow filter) ---
92
- router.add("POST", "/api/claim", async (req) => {
93
- const authErr = requireWorkerToken(deps, req);
94
- if (authErr)
95
- return authErr;
96
- const args = { role: req.body?.role };
97
- const result = await callToolHandler(deps.mcpDeps, "flow.claim", args);
98
- return mcpResultToApi(result);
99
- });
100
- // --- Flow claim ---
101
- router.add("POST", "/api/flows/:flow/claim", async (req) => {
102
- const authErr = requireWorkerToken(deps, req);
103
- if (authErr)
104
- return authErr;
105
- const args = { role: req.body?.role, flow: req.params.flow };
106
- const result = await callToolHandler(deps.mcpDeps, "flow.claim", args);
107
- return mcpResultToApi(result);
108
- });
109
- // --- Entity report ---
110
- // longRunning: true — flow.report blocks for the duration of gate evaluation
111
- // (potentially many minutes). The server handler calls req.setTimeout(0) for
112
- // this route specifically so only this connection bypasses the global 30s timeout.
113
- router.add("POST", "/api/entities/:id/report", async (req) => {
114
- const authErr = requireWorkerToken(deps, req);
115
- if (authErr)
116
- return authErr;
117
- const args = {
118
- entity_id: req.params.id,
119
- signal: req.body?.signal,
120
- };
121
- if (req.body?.worker_id)
122
- args.worker_id = req.body.worker_id;
123
- if (req.body?.artifacts)
124
- args.artifacts = req.body.artifacts;
125
- const result = await callToolHandler(deps.mcpDeps, "flow.report", args);
126
- return mcpResultToApi(result);
127
- }, { longRunning: true });
128
- // --- Entity fail ---
129
- router.add("POST", "/api/entities/:id/fail", async (req) => {
130
- const authErr = requireWorkerToken(deps, req);
131
- if (authErr)
132
- return authErr;
133
- const args = { entity_id: req.params.id, error: req.body?.error };
134
- const result = await callToolHandler(deps.mcpDeps, "flow.fail", args);
135
- return mcpResultToApi(result);
136
- });
137
- // --- Entity CRUD ---
138
- router.add("POST", "/api/entities", async (req) => {
139
- const flowName = req.body?.flow;
140
- const refs = req.body?.refs;
141
- if (!flowName)
142
- return { status: 400, body: { error: "Missing required field: flow" } };
143
- try {
144
- const entity = await deps.engine.createEntity(flowName, refs);
145
- return { status: 201, body: entity };
146
- }
147
- catch (err) {
148
- const msg = err instanceof Error ? err.message : String(err);
149
- if (msg.includes("not found"))
150
- return { status: 404, body: { error: msg } };
151
- return { status: 500, body: { error: msg } };
152
- }
153
- });
154
- router.add("GET", "/api/entities/:id", async (req) => {
155
- const result = await callToolHandler(deps.mcpDeps, "query.entity", { id: req.params.id });
156
- return mcpResultToApi(result);
157
- });
158
- router.add("GET", "/api/entities", async (req) => {
159
- const flow = req.query.get("flow");
160
- const state = req.query.get("state");
161
- if (!flow || !state)
162
- return { status: 400, body: { error: "Required query params: flow, state" } };
163
- const limitStr = req.query.get("limit");
164
- const args = { flow, state };
165
- if (limitStr) {
166
- const limit = parseInt(limitStr, 10);
167
- if (!Number.isNaN(limit) && limit > 0)
168
- args.limit = limit;
169
- }
170
- const result = await callToolHandler(deps.mcpDeps, "query.entities", args);
171
- return mcpResultToApi(result);
172
- });
173
- // --- Flow definition CRUD ---
174
- router.add("GET", "/api/flows", async () => {
175
- const flows = await deps.mcpDeps.flows.listAll();
176
- return { status: 200, body: flows };
177
- });
178
- router.add("GET", "/api/flows/:id", async (req) => {
179
- const result = await callToolHandler(deps.mcpDeps, "query.flow", { name: req.params.id });
180
- return mcpResultToApi(result);
181
- });
182
- router.add("PUT", "/api/flows/:id", async (req) => {
183
- const existing = await deps.mcpDeps.flows.getByName(req.params.id);
184
- // Only pick known fields from body — never spread req.body to prevent param injection
185
- const definition = req.body?.definition;
186
- const description = req.body?.description;
187
- const callerToken = extractBearerToken(req.authorization);
188
- if (existing) {
189
- const result = await callToolHandler(deps.mcpDeps, "admin.flow.update", { flow_name: req.params.id, definition, description }, { adminToken: deps.adminToken, callerToken });
190
- return mcpResultToApi(result);
191
- }
192
- else {
193
- const result = await callToolHandler(deps.mcpDeps, "admin.flow.create", { name: req.params.id, definition, description }, { adminToken: deps.adminToken, callerToken });
194
- return mcpResultToApi(result);
195
- }
196
- });
197
- router.add("DELETE", "/api/flows/:id", async () => {
198
- return { status: 501, body: { error: "Flow deletion not implemented" } };
199
- });
200
- // --- HTTP server ---
201
- const server = http.createServer(async (req, res) => {
202
- // CORS
203
- const origin = req.headers.origin;
204
- if (origin) {
205
- const isLoopbackOrigin = /^https?:\/\/localhost(:\d+)?$/.test(origin) ||
206
- /^https?:\/\/127\.0\.0\.1(:\d+)?$/.test(origin) ||
207
- /^https?:\/\/\[::1\](:\d+)?$/.test(origin);
208
- const corsAllowed = deps.corsOrigin
209
- ? origin === deps.corsOrigin // explicit origin: exact match only
210
- : isLoopbackOrigin; // loopback mode: only reflect loopback origins
211
- if (corsAllowed) {
212
- res.setHeader("Vary", "Origin");
213
- res.setHeader("Access-Control-Allow-Origin", origin);
214
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
215
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
216
- }
217
- }
218
- if (req.method === "OPTIONS") {
219
- res.writeHead(204).end();
220
- return;
221
- }
222
- const url = new URL(req.url ?? "/", `http://localhost`);
223
- const match = router.match(req.method ?? "GET", url.pathname);
224
- if (!match) {
225
- res.writeHead(404, { "Content-Type": "application/json" });
226
- res.end(JSON.stringify({ error: "Not found" }));
227
- return;
228
- }
229
- // flow.report blocks until gate evaluation completes (potentially many
230
- // minutes). Extend timeout per-request for this route only; all other
231
- // routes keep the global 30s limit.
232
- if (match.longRunning) {
233
- req.setTimeout(0);
234
- }
235
- let body = null;
236
- if (req.method === "POST" || req.method === "PUT") {
237
- try {
238
- const { body: raw, tooLarge } = await readBody(req);
239
- if (tooLarge) {
240
- res.writeHead(413, { "Content-Type": "application/json" });
241
- res.end(JSON.stringify({ error: "Request body too large" }));
242
- return;
243
- }
244
- body = raw ? JSON.parse(raw) : null;
245
- }
246
- catch {
247
- res.writeHead(400, { "Content-Type": "application/json" });
248
- res.end(JSON.stringify({ error: "Invalid JSON" }));
249
- return;
250
- }
251
- }
252
- const parsed = {
253
- params: match.params,
254
- query: url.searchParams,
255
- body,
256
- authorization: req.headers.authorization,
257
- };
258
- try {
259
- const apiRes = await match.handler(parsed);
260
- if (apiRes.status === 204) {
261
- res.writeHead(204).end();
262
- }
263
- else {
264
- res.writeHead(apiRes.status, { "Content-Type": "application/json" });
265
- res.end(JSON.stringify(apiRes.body));
266
- }
267
- }
268
- catch (err) {
269
- (deps.logger ?? consoleLogger).error("Request error:", err);
270
- res.writeHead(500, { "Content-Type": "application/json" });
271
- res.end(JSON.stringify({ error: "Internal server error" }));
272
- }
273
- });
274
- // Sensible defaults protect all routes from Slowloris/slow-header DoS.
275
- // The entity report route calls req.setTimeout(0) per-request to bypass this
276
- // limit only for connections that need long-running gate evaluation.
277
- server.requestTimeout = 30000;
278
- server.headersTimeout = 10000;
279
- return server;
280
- }
@@ -1,46 +0,0 @@
1
- /**
2
- * Wire types for defcon's REST and MCP APIs.
3
- * Exported for consumers (e.g. norad) to import instead of duplicating.
4
- */
5
- export type ClaimResponse = {
6
- next_action: "check_back";
7
- retry_after_ms: number;
8
- message: string;
9
- } | {
10
- worker_id?: string;
11
- entity_id: string;
12
- invocation_id: string;
13
- flow: string | null;
14
- stage: string;
15
- prompt: string;
16
- context: Record<string, unknown> | null;
17
- worker_notice?: string;
18
- };
19
- export type ReportResponse = {
20
- next_action: "continue";
21
- new_state: string;
22
- gates_passed?: string[];
23
- prompt: string | null;
24
- context: Record<string, unknown> | null;
25
- } | {
26
- next_action: "waiting";
27
- new_state: null;
28
- gated: true;
29
- gate_output: string;
30
- gateName: string;
31
- failure_prompt: string | null;
32
- } | {
33
- next_action: "check_back";
34
- message: string;
35
- retry_after_ms: number;
36
- timeout_prompt?: string;
37
- } | {
38
- next_action: "completed";
39
- new_state: string;
40
- gates_passed?: string[];
41
- prompt: null;
42
- context: null;
43
- };
44
- export interface CreateEntityResponse {
45
- id: string;
46
- }
@@ -1,5 +0,0 @@
1
- /**
2
- * Wire types for defcon's REST and MCP APIs.
3
- * Exported for consumers (e.g. norad) to import instead of duplicating.
4
- */
5
- export {};
@@ -1 +0,0 @@
1
- export declare const DB_PATH = "./agentic-flow.db";
@@ -1 +0,0 @@
1
- export const DB_PATH = "./agentic-flow.db";
@@ -1,3 +0,0 @@
1
- import type { IFlowRepository, IGateRepository } from "../repositories/interfaces.js";
2
- import type { SeedFile } from "./zod-schemas.js";
3
- export declare function exportSeed(flowRepo: IFlowRepository, gateRepo: IGateRepository): Promise<SeedFile>;
@@ -1,87 +0,0 @@
1
- export async function exportSeed(flowRepo, gateRepo) {
2
- const flows = await flowRepo.listAll();
3
- // Fetch all gates once, build ID->name map (no N+1)
4
- const allGates = await gateRepo.listAll();
5
- const gateIdToName = new Map(allGates.map((g) => [g.id, g.name]));
6
- const gateById = new Map(allGates.map((g) => [g.id, g]));
7
- const exportedFlows = flows.filter((f) => f.discipline !== null);
8
- // Collect gate IDs referenced by transitions of exported flows only
9
- const referencedGateIds = new Set();
10
- for (const flow of exportedFlows) {
11
- for (const t of flow.transitions) {
12
- if (t.gateId)
13
- referencedGateIds.add(t.gateId);
14
- }
15
- }
16
- // Build gates array from referenced gates (already loaded)
17
- const gateEntries = [];
18
- for (const gateId of referencedGateIds) {
19
- const gate = gateById.get(gateId);
20
- if (!gate)
21
- continue;
22
- if (gate.type === "command" && gate.command) {
23
- gateEntries.push({
24
- name: gate.name,
25
- type: "command",
26
- command: gate.command,
27
- timeoutMs: gate.timeoutMs ?? undefined,
28
- });
29
- }
30
- else if (gate.type === "function" && gate.functionRef) {
31
- gateEntries.push({
32
- name: gate.name,
33
- type: "function",
34
- functionRef: gate.functionRef,
35
- timeoutMs: gate.timeoutMs ?? undefined,
36
- });
37
- }
38
- else if (gate.type === "api" && gate.apiConfig) {
39
- gateEntries.push({
40
- name: gate.name,
41
- type: "api",
42
- apiConfig: gate.apiConfig,
43
- timeoutMs: gate.timeoutMs ?? undefined,
44
- });
45
- }
46
- }
47
- const seedFlows = exportedFlows.map((f) => ({
48
- name: f.name,
49
- description: f.description ?? undefined,
50
- entitySchema: f.entitySchema ?? undefined,
51
- initialState: f.initialState,
52
- maxConcurrent: f.maxConcurrent,
53
- maxConcurrentPerRepo: f.maxConcurrentPerRepo,
54
- affinityWindowMs: f.affinityWindowMs,
55
- gateTimeoutMs: f.gateTimeoutMs ?? undefined,
56
- version: f.version,
57
- createdBy: f.createdBy ?? undefined,
58
- discipline: f.discipline,
59
- defaultModelTier: f.defaultModelTier ?? undefined,
60
- timeoutPrompt: f.timeoutPrompt ?? undefined,
61
- }));
62
- const seedStates = exportedFlows.flatMap((f) => f.states.map((s) => ({
63
- name: s.name,
64
- flowName: f.name,
65
- modelTier: s.modelTier ?? undefined,
66
- mode: s.mode,
67
- promptTemplate: s.promptTemplate ?? undefined,
68
- constraints: s.constraints ?? undefined,
69
- })));
70
- const seedTransitions = exportedFlows.flatMap((f) => f.transitions.map((t) => ({
71
- flowName: f.name,
72
- fromState: t.fromState,
73
- toState: t.toState,
74
- trigger: t.trigger,
75
- gateName: t.gateId ? gateIdToName.get(t.gateId) : undefined,
76
- condition: t.condition ?? undefined,
77
- priority: t.priority,
78
- spawnFlow: t.spawnFlow ?? undefined,
79
- spawnTemplate: t.spawnTemplate ?? undefined,
80
- })));
81
- return {
82
- flows: seedFlows,
83
- states: seedStates,
84
- gates: gateEntries,
85
- transitions: seedTransitions,
86
- };
87
- }
@@ -1,4 +0,0 @@
1
- export { exportSeed } from "./exporter.js";
2
- export type { LoadSeedOptions, LoadSeedResult } from "./seed-loader.js";
3
- export { loadSeed } from "./seed-loader.js";
4
- export * from "./zod-schemas.js";
@@ -1,4 +0,0 @@
1
- // Config module — seed loader, Zod schemas
2
- export { exportSeed } from "./exporter.js";
3
- export { loadSeed } from "./seed-loader.js";
4
- export * from "./zod-schemas.js";
@@ -1,10 +0,0 @@
1
- import type Database from "better-sqlite3";
2
- import type { IFlowRepository, IGateRepository } from "../repositories/interfaces.js";
3
- export interface LoadSeedOptions {
4
- allowedRoot?: string;
5
- }
6
- export interface LoadSeedResult {
7
- flows: number;
8
- gates: number;
9
- }
10
- export declare function loadSeed(seedPath: string, flowRepo: IFlowRepository, gateRepo: IGateRepository, sqlite: InstanceType<typeof Database>, options?: LoadSeedOptions): Promise<LoadSeedResult>;
@@ -1,108 +0,0 @@
1
- import { readFileSync, realpathSync } from "node:fs";
2
- import { relative, resolve, sep } from "node:path";
3
- import { SeedFileSchema } from "./zod-schemas.js";
4
- export async function loadSeed(seedPath, flowRepo, gateRepo, sqlite, options) {
5
- const allowedRoot = options?.allowedRoot ?? process.cwd();
6
- const resolvedRoot = resolve(allowedRoot);
7
- const resolvedSeed = resolve(seedPath);
8
- const lexicalRel = relative(resolvedRoot, resolvedSeed);
9
- if (lexicalRel === ".." || lexicalRel.startsWith(`..${sep}`)) {
10
- throw new Error(`Seed path escapes allowed root: ${resolvedSeed} is not under ${resolvedRoot}`);
11
- }
12
- const realSeed = realpathSync(resolvedSeed);
13
- let realRoot;
14
- try {
15
- realRoot = realpathSync(resolvedRoot);
16
- }
17
- catch {
18
- realRoot = resolvedRoot;
19
- }
20
- const realRel = relative(realRoot, realSeed);
21
- if (realRel === ".." || realRel.startsWith(`..${sep}`)) {
22
- throw new Error(`Seed path escapes allowed root: resolved symlink ${realSeed} is not under ${realRoot}`);
23
- }
24
- const raw = readFileSync(realSeed, "utf-8");
25
- return parseSeedAndLoad(parseJson(raw, realSeed), flowRepo, gateRepo, sqlite);
26
- }
27
- function parseJson(raw, path) {
28
- try {
29
- return JSON.parse(raw);
30
- }
31
- catch (e) {
32
- const msg = e instanceof Error ? e.message : String(e);
33
- throw new Error(`Invalid JSON in seed file: ${path}: ${msg}`, { cause: e });
34
- }
35
- }
36
- async function parseSeedAndLoad(json, flowRepo, gateRepo, sqlite) {
37
- const parsed = SeedFileSchema.parse(json);
38
- sqlite.exec("BEGIN");
39
- try {
40
- // 1. Create gates first (transitions reference them by name)
41
- const gateNameToId = new Map();
42
- for (const g of parsed.gates) {
43
- const gate = await gateRepo.create({
44
- name: g.name,
45
- type: g.type,
46
- command: "command" in g ? g.command : undefined,
47
- functionRef: "functionRef" in g ? g.functionRef : undefined,
48
- apiConfig: "apiConfig" in g ? g.apiConfig : undefined,
49
- timeoutMs: g.timeoutMs,
50
- failurePrompt: g.failurePrompt,
51
- timeoutPrompt: g.timeoutPrompt,
52
- });
53
- gateNameToId.set(g.name, gate.id);
54
- }
55
- // 2. Create flows, states, and transitions
56
- for (const f of parsed.flows) {
57
- const flow = await flowRepo.create({
58
- name: f.name,
59
- description: f.description,
60
- entitySchema: f.entitySchema,
61
- initialState: f.initialState,
62
- maxConcurrent: f.maxConcurrent,
63
- maxConcurrentPerRepo: f.maxConcurrentPerRepo,
64
- gateTimeoutMs: f.gateTimeoutMs,
65
- createdBy: f.createdBy,
66
- discipline: f.discipline,
67
- defaultModelTier: f.defaultModelTier,
68
- timeoutPrompt: f.timeoutPrompt,
69
- });
70
- const flowStates = parsed.states.filter((s) => s.flowName === f.name);
71
- for (const s of flowStates) {
72
- await flowRepo.addState(flow.id, {
73
- name: s.name,
74
- modelTier: s.modelTier,
75
- mode: s.mode,
76
- promptTemplate: s.promptTemplate,
77
- constraints: s.constraints,
78
- onEnter: s.onEnter,
79
- });
80
- }
81
- const flowTransitions = parsed.transitions.filter((t) => t.flowName === f.name);
82
- for (const t of flowTransitions) {
83
- if (t.gateName && !gateNameToId.has(t.gateName)) {
84
- throw new Error(`Transition from "${t.fromState}" to "${t.toState}" in flow "${f.name}" references unknown gate "${t.gateName}"`);
85
- }
86
- await flowRepo.addTransition(flow.id, {
87
- fromState: t.fromState,
88
- toState: t.toState,
89
- trigger: t.trigger,
90
- gateId: t.gateName ? gateNameToId.get(t.gateName) : undefined,
91
- condition: t.condition,
92
- priority: t.priority,
93
- spawnFlow: t.spawnFlow,
94
- spawnTemplate: t.spawnTemplate,
95
- });
96
- }
97
- }
98
- sqlite.exec("COMMIT");
99
- }
100
- catch (err) {
101
- sqlite.exec("ROLLBACK");
102
- throw err;
103
- }
104
- return {
105
- flows: parsed.flows.length,
106
- gates: parsed.gates.length,
107
- };
108
- }