@wopr-network/defcon 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +274 -0
- package/dist/api/router.d.ts +24 -0
- package/dist/api/router.js +44 -0
- package/dist/api/server.d.ts +13 -0
- package/dist/api/server.js +280 -0
- package/dist/api/wire-types.d.ts +46 -0
- package/dist/api/wire-types.js +5 -0
- package/dist/config/db-path.d.ts +1 -0
- package/dist/config/db-path.js +1 -0
- package/dist/config/exporter.d.ts +3 -0
- package/dist/config/exporter.js +87 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.js +4 -0
- package/dist/config/seed-loader.d.ts +10 -0
- package/dist/config/seed-loader.js +108 -0
- package/dist/config/zod-schemas.d.ts +165 -0
- package/dist/config/zod-schemas.js +283 -0
- package/dist/cors.d.ts +8 -0
- package/dist/cors.js +21 -0
- package/dist/engine/constants.d.ts +1 -0
- package/dist/engine/constants.js +1 -0
- package/dist/engine/engine.d.ts +69 -0
- package/dist/engine/engine.js +485 -0
- package/dist/engine/event-emitter.d.ts +9 -0
- package/dist/engine/event-emitter.js +19 -0
- package/dist/engine/event-types.d.ts +105 -0
- package/dist/engine/event-types.js +1 -0
- package/dist/engine/flow-spawner.d.ts +8 -0
- package/dist/engine/flow-spawner.js +28 -0
- package/dist/engine/gate-command-validator.d.ts +6 -0
- package/dist/engine/gate-command-validator.js +46 -0
- package/dist/engine/gate-evaluator.d.ts +12 -0
- package/dist/engine/gate-evaluator.js +233 -0
- package/dist/engine/handlebars.d.ts +9 -0
- package/dist/engine/handlebars.js +51 -0
- package/dist/engine/index.d.ts +12 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/invocation-builder.d.ts +18 -0
- package/dist/engine/invocation-builder.js +58 -0
- package/dist/engine/on-enter.d.ts +8 -0
- package/dist/engine/on-enter.js +102 -0
- package/dist/engine/ssrf-guard.d.ts +22 -0
- package/dist/engine/ssrf-guard.js +159 -0
- package/dist/engine/state-machine.d.ts +12 -0
- package/dist/engine/state-machine.js +74 -0
- package/dist/execution/active-runner.d.ts +45 -0
- package/dist/execution/active-runner.js +165 -0
- package/dist/execution/admin-schemas.d.ts +116 -0
- package/dist/execution/admin-schemas.js +125 -0
- package/dist/execution/cli.d.ts +57 -0
- package/dist/execution/cli.js +498 -0
- package/dist/execution/handlers/admin.d.ts +67 -0
- package/dist/execution/handlers/admin.js +200 -0
- package/dist/execution/handlers/flow.d.ts +25 -0
- package/dist/execution/handlers/flow.js +289 -0
- package/dist/execution/handlers/query.d.ts +31 -0
- package/dist/execution/handlers/query.js +64 -0
- package/dist/execution/index.d.ts +4 -0
- package/dist/execution/index.js +3 -0
- package/dist/execution/mcp-helpers.d.ts +42 -0
- package/dist/execution/mcp-helpers.js +23 -0
- package/dist/execution/mcp-server.d.ts +33 -0
- package/dist/execution/mcp-server.js +1020 -0
- package/dist/execution/provision-worktree.d.ts +16 -0
- package/dist/execution/provision-worktree.js +123 -0
- package/dist/execution/tool-schemas.d.ts +40 -0
- package/dist/execution/tool-schemas.js +44 -0
- package/dist/gates/blocking-graph.d.ts +26 -0
- package/dist/gates/blocking-graph.js +102 -0
- package/dist/gates/test/bad-return-gate.d.ts +1 -0
- package/dist/gates/test/bad-return-gate.js +4 -0
- package/dist/gates/test/passing-gate.d.ts +2 -0
- package/dist/gates/test/passing-gate.js +3 -0
- package/dist/gates/test/slow-gate.d.ts +2 -0
- package/dist/gates/test/slow-gate.js +5 -0
- package/dist/gates/test/throwing-gate.d.ts +1 -0
- package/dist/gates/test/throwing-gate.js +3 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +12 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.js +28 -0
- package/dist/repositories/drizzle/entity.repo.d.ts +27 -0
- package/dist/repositories/drizzle/entity.repo.js +190 -0
- package/dist/repositories/drizzle/event.repo.d.ts +12 -0
- package/dist/repositories/drizzle/event.repo.js +24 -0
- package/dist/repositories/drizzle/flow.repo.d.ts +22 -0
- package/dist/repositories/drizzle/flow.repo.js +364 -0
- package/dist/repositories/drizzle/gate.repo.d.ts +16 -0
- package/dist/repositories/drizzle/gate.repo.js +98 -0
- package/dist/repositories/drizzle/index.d.ts +6 -0
- package/dist/repositories/drizzle/index.js +7 -0
- package/dist/repositories/drizzle/invocation.repo.d.ts +23 -0
- package/dist/repositories/drizzle/invocation.repo.js +199 -0
- package/dist/repositories/drizzle/schema.d.ts +1932 -0
- package/dist/repositories/drizzle/schema.js +155 -0
- package/dist/repositories/drizzle/transition-log.repo.d.ts +11 -0
- package/dist/repositories/drizzle/transition-log.repo.js +42 -0
- package/dist/repositories/interfaces.d.ts +321 -0
- package/dist/repositories/interfaces.js +2 -0
- package/dist/src/api/router.d.ts +24 -0
- package/dist/src/api/router.js +44 -0
- package/dist/src/api/server.d.ts +13 -0
- package/dist/src/api/server.js +280 -0
- package/dist/src/api/wire-types.d.ts +46 -0
- package/dist/src/api/wire-types.js +5 -0
- package/dist/src/config/db-path.d.ts +1 -0
- package/dist/src/config/db-path.js +1 -0
- package/dist/src/config/exporter.d.ts +3 -0
- package/dist/src/config/exporter.js +87 -0
- package/dist/src/config/index.d.ts +4 -0
- package/dist/src/config/index.js +4 -0
- package/dist/src/config/seed-loader.d.ts +14 -0
- package/dist/src/config/seed-loader.js +131 -0
- package/dist/src/config/zod-schemas.d.ts +165 -0
- package/dist/src/config/zod-schemas.js +283 -0
- package/dist/src/cors.d.ts +8 -0
- package/dist/src/cors.js +21 -0
- package/dist/src/engine/constants.d.ts +1 -0
- package/dist/src/engine/constants.js +1 -0
- package/dist/src/engine/engine.d.ts +69 -0
- package/dist/src/engine/engine.js +485 -0
- package/dist/src/engine/event-emitter.d.ts +9 -0
- package/dist/src/engine/event-emitter.js +19 -0
- package/dist/src/engine/event-types.d.ts +105 -0
- package/dist/src/engine/event-types.js +1 -0
- package/dist/src/engine/flow-spawner.d.ts +8 -0
- package/dist/src/engine/flow-spawner.js +28 -0
- package/dist/src/engine/gate-command-validator.d.ts +6 -0
- package/dist/src/engine/gate-command-validator.js +46 -0
- package/dist/src/engine/gate-evaluator.d.ts +12 -0
- package/dist/src/engine/gate-evaluator.js +233 -0
- package/dist/src/engine/handlebars.d.ts +9 -0
- package/dist/src/engine/handlebars.js +51 -0
- package/dist/src/engine/index.d.ts +12 -0
- package/dist/src/engine/index.js +7 -0
- package/dist/src/engine/invocation-builder.d.ts +18 -0
- package/dist/src/engine/invocation-builder.js +58 -0
- package/dist/src/engine/on-enter.d.ts +8 -0
- package/dist/src/engine/on-enter.js +102 -0
- package/dist/src/engine/ssrf-guard.d.ts +22 -0
- package/dist/src/engine/ssrf-guard.js +159 -0
- package/dist/src/engine/state-machine.d.ts +12 -0
- package/dist/src/engine/state-machine.js +74 -0
- package/dist/src/execution/active-runner.d.ts +45 -0
- package/dist/src/execution/active-runner.js +165 -0
- package/dist/src/execution/admin-schemas.d.ts +116 -0
- package/dist/src/execution/admin-schemas.js +125 -0
- package/dist/src/execution/cli.d.ts +57 -0
- package/dist/src/execution/cli.js +501 -0
- package/dist/src/execution/handlers/admin.d.ts +67 -0
- package/dist/src/execution/handlers/admin.js +200 -0
- package/dist/src/execution/handlers/flow.d.ts +25 -0
- package/dist/src/execution/handlers/flow.js +289 -0
- package/dist/src/execution/handlers/query.d.ts +31 -0
- package/dist/src/execution/handlers/query.js +64 -0
- package/dist/src/execution/index.d.ts +4 -0
- package/dist/src/execution/index.js +3 -0
- package/dist/src/execution/mcp-helpers.d.ts +42 -0
- package/dist/src/execution/mcp-helpers.js +23 -0
- package/dist/src/execution/mcp-server.d.ts +33 -0
- package/dist/src/execution/mcp-server.js +1020 -0
- package/dist/src/execution/provision-worktree.d.ts +16 -0
- package/dist/src/execution/provision-worktree.js +123 -0
- package/dist/src/execution/tool-schemas.d.ts +40 -0
- package/dist/src/execution/tool-schemas.js +44 -0
- package/dist/src/logger.d.ts +8 -0
- package/dist/src/logger.js +12 -0
- package/dist/src/main.d.ts +14 -0
- package/dist/src/main.js +28 -0
- package/dist/src/repositories/drizzle/entity.repo.d.ts +27 -0
- package/dist/src/repositories/drizzle/entity.repo.js +190 -0
- package/dist/src/repositories/drizzle/event.repo.d.ts +12 -0
- package/dist/src/repositories/drizzle/event.repo.js +24 -0
- package/dist/src/repositories/drizzle/flow.repo.d.ts +22 -0
- package/dist/src/repositories/drizzle/flow.repo.js +364 -0
- package/dist/src/repositories/drizzle/gate.repo.d.ts +16 -0
- package/dist/src/repositories/drizzle/gate.repo.js +98 -0
- package/dist/src/repositories/drizzle/index.d.ts +6 -0
- package/dist/src/repositories/drizzle/index.js +7 -0
- package/dist/src/repositories/drizzle/invocation.repo.d.ts +23 -0
- package/dist/src/repositories/drizzle/invocation.repo.js +199 -0
- package/dist/src/repositories/drizzle/schema.d.ts +1932 -0
- package/dist/src/repositories/drizzle/schema.js +155 -0
- package/dist/src/repositories/drizzle/transition-log.repo.d.ts +11 -0
- package/dist/src/repositories/drizzle/transition-log.repo.js +42 -0
- package/dist/src/repositories/interfaces.d.ts +321 -0
- package/dist/src/repositories/interfaces.js +2 -0
- package/dist/src/utils/redact.d.ts +2 -0
- package/dist/src/utils/redact.js +62 -0
- package/dist/utils/redact.d.ts +2 -0
- package/dist/utils/redact.js +62 -0
- package/drizzle/.gitkeep +0 -0
- package/drizzle/0000_simple_surge.sql +144 -0
- package/drizzle/0001_peaceful_marvel_apes.sql +18 -0
- package/drizzle/0002_add_invocations_created_at.sql +1 -0
- package/drizzle/0003_drop_integration_config.sql +1 -0
- package/drizzle/0004_add_flow_discipline.sql +2 -0
- package/drizzle/0004_lucky_silverclaw.sql +5 -0
- package/drizzle/0005_old_blue_shield.sql +2 -0
- package/drizzle/0006_solid_magik.sql +2 -0
- package/drizzle/0007_fancy_luke_cage.sql +1 -0
- package/drizzle/0008_thick_dark_beast.sql +1 -0
- package/drizzle/0009_brief_midnight.sql +1 -0
- package/drizzle/0010_amusing_bastion.sql +1 -0
- package/drizzle/meta/0000_snapshot.json +996 -0
- package/drizzle/meta/0004_snapshot.json +1008 -0
- package/drizzle/meta/0005_snapshot.json +1023 -0
- package/drizzle/meta/0006_snapshot.json +1037 -0
- package/drizzle/meta/0007_snapshot.json +1044 -0
- package/drizzle/meta/0008_snapshot.json +1051 -0
- package/drizzle/meta/0009_snapshot.json +1058 -0
- package/drizzle/meta/0010_snapshot.json +1065 -0
- package/drizzle/meta/_journal.json +83 -0
- package/gates/.gitkeep +0 -0
- package/gates/blocking-graph.d.ts +26 -0
- package/gates/blocking-graph.js +102 -0
- package/gates/blocking-graph.ts +121 -0
- package/gates/check-design-posted.sh +39 -0
- package/gates/check-merge.sh +51 -0
- package/gates/check-pr-capacity.sh +17 -0
- package/gates/check-review-ready.sh +47 -0
- package/gates/check-spec-posted.sh +34 -0
- package/gates/check-unblocked.sh +56 -0
- package/gates/ci-green.sh +9 -0
- package/gates/merge-queue.sh +14 -0
- package/gates/review-bots-ready.sh +9 -0
- package/gates/spec-posted.sh +31 -0
- package/gates/test/bad-return-gate.d.ts +1 -0
- package/gates/test/bad-return-gate.js +4 -0
- package/gates/test/bad-return-gate.ts +4 -0
- package/gates/test/passing-gate.d.ts +2 -0
- package/gates/test/passing-gate.js +3 -0
- package/gates/test/passing-gate.ts +5 -0
- package/gates/test/slow-gate.d.ts +2 -0
- package/gates/test/slow-gate.js +5 -0
- package/gates/test/slow-gate.ts +7 -0
- package/gates/test/throwing-gate.d.ts +1 -0
- package/gates/test/throwing-gate.js +3 -0
- package/gates/test/throwing-gate.ts +3 -0
- package/gates/test-fail.sh +2 -0
- package/gates/test-pass.sh +2 -0
- package/gates/timeout-gate-script.sh +3 -0
- package/package.json +64 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { resolve4, resolve6 } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
const DNS_CACHE_TTL_MS = 60_000;
|
|
4
|
+
/** Exported for test access — clear between tests */
|
|
5
|
+
export const _dnsCache = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Returns true if the IP address is in a private/reserved range.
|
|
8
|
+
* Handles IPv4, IPv6 loopback, link-local, ULA, and IPv6-mapped IPv4.
|
|
9
|
+
*/
|
|
10
|
+
function isPrivateIp(ip) {
|
|
11
|
+
let normalized = ip;
|
|
12
|
+
// Handle IPv6-mapped IPv4: ::ffff:A.B.C.D
|
|
13
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(ip);
|
|
14
|
+
if (mapped) {
|
|
15
|
+
normalized = mapped[1];
|
|
16
|
+
}
|
|
17
|
+
// IPv6 loopback
|
|
18
|
+
if (normalized === "::1")
|
|
19
|
+
return true;
|
|
20
|
+
// IPv6 link-local fe80::/10
|
|
21
|
+
if (/^fe[89ab][0-9a-f]:/i.test(normalized))
|
|
22
|
+
return true;
|
|
23
|
+
// IPv6 ULA fc00::/7 (addresses starting with fc or fd)
|
|
24
|
+
if (/^f[cd]/i.test(normalized))
|
|
25
|
+
return true;
|
|
26
|
+
// IPv4 checks
|
|
27
|
+
const parts = normalized.split(".").map(Number);
|
|
28
|
+
if (parts.length === 4 && parts.every((p) => p >= 0 && p <= 255)) {
|
|
29
|
+
// 10.0.0.0/8
|
|
30
|
+
if (parts[0] === 10)
|
|
31
|
+
return true;
|
|
32
|
+
// 172.16.0.0/12
|
|
33
|
+
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
|
|
34
|
+
return true;
|
|
35
|
+
// 192.168.0.0/16
|
|
36
|
+
if (parts[0] === 192 && parts[1] === 168)
|
|
37
|
+
return true;
|
|
38
|
+
// 127.0.0.0/8
|
|
39
|
+
if (parts[0] === 127)
|
|
40
|
+
return true;
|
|
41
|
+
// 169.254.0.0/16 (link-local)
|
|
42
|
+
if (parts[0] === 169 && parts[1] === 254)
|
|
43
|
+
return true;
|
|
44
|
+
// 0.0.0.0
|
|
45
|
+
if (parts.every((p) => p === 0))
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse CIDR notation and check if an IP falls within the range.
|
|
52
|
+
*/
|
|
53
|
+
function ipInCidr(ip, cidr) {
|
|
54
|
+
const [base, prefixStr] = cidr.split("/");
|
|
55
|
+
if (!prefixStr)
|
|
56
|
+
return false;
|
|
57
|
+
const prefix = parseInt(prefixStr, 10);
|
|
58
|
+
if (Number.isNaN(prefix))
|
|
59
|
+
return false;
|
|
60
|
+
const baseParts = base.split(".").map(Number);
|
|
61
|
+
const ipParts = ip.split(".").map(Number);
|
|
62
|
+
if (baseParts.length !== 4 || ipParts.length !== 4)
|
|
63
|
+
return false;
|
|
64
|
+
// Validate prefix bounds for IPv4
|
|
65
|
+
if (prefix < 0 || prefix > 32)
|
|
66
|
+
return false;
|
|
67
|
+
const baseNum = (baseParts[0] << 24) | (baseParts[1] << 16) | (baseParts[2] << 8) | baseParts[3];
|
|
68
|
+
const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
|
|
69
|
+
const mask = prefix === 0 ? 0 : ~0 << (32 - prefix);
|
|
70
|
+
return (baseNum & mask) === (ipNum & mask);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve hostname to IP addresses, using cache with 60s TTL.
|
|
74
|
+
*/
|
|
75
|
+
async function resolveHost(hostname) {
|
|
76
|
+
const cached = _dnsCache.get(hostname);
|
|
77
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
78
|
+
return cached.ips;
|
|
79
|
+
}
|
|
80
|
+
const ips = [];
|
|
81
|
+
try {
|
|
82
|
+
const v4 = await resolve4(hostname);
|
|
83
|
+
ips.push(...v4);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// no A records
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const v6 = await resolve6(hostname);
|
|
90
|
+
ips.push(...v6);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// no AAAA records
|
|
94
|
+
}
|
|
95
|
+
_dnsCache.set(hostname, { ips, expiresAt: Date.now() + DNS_CACHE_TTL_MS });
|
|
96
|
+
return ips;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parse the DEFCON_GATE_ALLOWLIST env var value into hostname and CIDR entries.
|
|
100
|
+
*/
|
|
101
|
+
function parseAllowlist(allowlist) {
|
|
102
|
+
const hostnames = new Set();
|
|
103
|
+
const cidrs = [];
|
|
104
|
+
for (const entry of allowlist
|
|
105
|
+
.split(",")
|
|
106
|
+
.map((s) => s.trim())
|
|
107
|
+
.filter(Boolean)) {
|
|
108
|
+
if (entry.includes("/")) {
|
|
109
|
+
cidrs.push(entry);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
hostnames.add(entry);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { hostnames, cidrs };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check whether a URL is safe to fetch (not targeting private/reserved addresses).
|
|
119
|
+
*
|
|
120
|
+
* @param url - The full HTTPS URL to check
|
|
121
|
+
* @param allowlistEnv - Optional comma-separated allowlist (pass process.env.DEFCON_GATE_ALLOWLIST)
|
|
122
|
+
* @returns SsrfCheckResult indicating whether the request should proceed,
|
|
123
|
+
* including resolvedIps to use for the actual fetch to avoid DNS rebinding TOCTOU.
|
|
124
|
+
*/
|
|
125
|
+
export async function checkSsrf(url, allowlistEnv) {
|
|
126
|
+
const parsed = new URL(url);
|
|
127
|
+
const hostname = parsed.hostname;
|
|
128
|
+
// Parse allowlist once and reuse both hostnames and cidrs
|
|
129
|
+
const allowlist = allowlistEnv ? parseAllowlist(allowlistEnv) : null;
|
|
130
|
+
// If allowlist is set and hostname is explicitly allowed, skip checks
|
|
131
|
+
if (allowlist?.hostnames.has(hostname)) {
|
|
132
|
+
return { allowed: true };
|
|
133
|
+
}
|
|
134
|
+
// If the hostname is an IP literal, check directly without DNS
|
|
135
|
+
let ips;
|
|
136
|
+
if (isIP(hostname)) {
|
|
137
|
+
ips = [hostname];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
ips = await resolveHost(hostname);
|
|
141
|
+
if (ips.length === 0) {
|
|
142
|
+
return { allowed: false, reason: `SSRF_BLOCKED: ${hostname} is unresolvable` };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Check CIDR allowlist if present
|
|
146
|
+
if (allowlist && allowlist.cidrs.length > 0) {
|
|
147
|
+
const allIpsAllowed = ips.every((ip) => allowlist.cidrs.some((cidr) => ipInCidr(ip, cidr)));
|
|
148
|
+
if (allIpsAllowed) {
|
|
149
|
+
return { allowed: true, resolvedIps: ips };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Check all resolved IPs — block if ANY is private
|
|
153
|
+
for (const ip of ips) {
|
|
154
|
+
if (isPrivateIp(ip)) {
|
|
155
|
+
return { allowed: false, reason: `SSRF_BLOCKED: ${hostname} resolves to private/reserved address ${ip}` };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { allowed: true, resolvedIps: ips };
|
|
159
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Logger } from "../logger.js";
|
|
2
|
+
import type { Flow, Transition } from "../repositories/interfaces.js";
|
|
3
|
+
export declare function evaluateCondition(condition: string, context: Record<string, unknown>): boolean;
|
|
4
|
+
export declare function findTransition(flow: Flow, currentState: string, signal: string, context: Record<string, unknown>, skipGateCheck?: boolean, logger?: Logger): Transition | null;
|
|
5
|
+
export interface ValidationError {
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function validateFlow(flow: Flow): ValidationError[];
|
|
9
|
+
/**
|
|
10
|
+
* A state is terminal if no transitions use it as fromState.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isTerminal(flow: Flow, state: string): boolean;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { consoleLogger } from "../logger.js";
|
|
2
|
+
import { getHandlebars } from "./handlebars.js";
|
|
3
|
+
const hbs = getHandlebars();
|
|
4
|
+
// ─── Condition Evaluation ───
|
|
5
|
+
export function evaluateCondition(condition, context) {
|
|
6
|
+
const template = hbs.compile(condition);
|
|
7
|
+
const result = template(context).trim();
|
|
8
|
+
return result.length > 0 && result !== "false" && result !== "0";
|
|
9
|
+
}
|
|
10
|
+
// ─── Transition Matching ───
|
|
11
|
+
export function findTransition(flow, currentState, signal, context, skipGateCheck = false, logger = consoleLogger) {
|
|
12
|
+
const candidates = flow.transitions
|
|
13
|
+
.filter((t) => t.fromState === currentState && t.trigger === signal)
|
|
14
|
+
.sort((a, b) => b.priority - a.priority);
|
|
15
|
+
for (const candidate of candidates) {
|
|
16
|
+
if (!skipGateCheck && candidate.gateId !== null) {
|
|
17
|
+
const entity = context.entity;
|
|
18
|
+
const gatePassed = entity?.gateResults?.some((g) => g.gateId === candidate.gateId && g.passed) ?? false;
|
|
19
|
+
if (!gatePassed)
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
if (candidate.condition === null || evaluateCondition(candidate.condition, context)) {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
logger.warn(`Condition evaluation failed for transition '${candidate.fromState}' -> '${candidate.toState}' (trigger: '${candidate.trigger}'), skipping: ${err instanceof Error ? err.message : String(err)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
export function validateFlow(flow) {
|
|
34
|
+
const errors = [];
|
|
35
|
+
const stateNames = new Set(flow.states.map((s) => s.name));
|
|
36
|
+
if (!stateNames.has(flow.initialState)) {
|
|
37
|
+
errors.push({ message: `initialState "${flow.initialState}" is not a defined state` });
|
|
38
|
+
}
|
|
39
|
+
for (const t of flow.transitions) {
|
|
40
|
+
if (!stateNames.has(t.fromState)) {
|
|
41
|
+
errors.push({ message: `Transition from non-existent state "${t.fromState}"` });
|
|
42
|
+
}
|
|
43
|
+
if (!stateNames.has(t.toState)) {
|
|
44
|
+
errors.push({ message: `Transition to non-existent state "${t.toState}"` });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (stateNames.has(flow.initialState)) {
|
|
48
|
+
const reachable = new Set();
|
|
49
|
+
const queue = [flow.initialState];
|
|
50
|
+
while (queue.length > 0) {
|
|
51
|
+
const current = queue.pop();
|
|
52
|
+
if (reachable.has(current))
|
|
53
|
+
continue;
|
|
54
|
+
reachable.add(current);
|
|
55
|
+
for (const t of flow.transitions) {
|
|
56
|
+
if (t.fromState === current && stateNames.has(t.toState) && !reachable.has(t.toState)) {
|
|
57
|
+
queue.push(t.toState);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const name of stateNames) {
|
|
62
|
+
if (!reachable.has(name)) {
|
|
63
|
+
errors.push({ message: `State "${name}" is unreachable from initialState` });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return errors;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A state is terminal if no transitions use it as fromState.
|
|
71
|
+
*/
|
|
72
|
+
export function isTerminal(flow, state) {
|
|
73
|
+
return !flow.transitions.some((t) => t.fromState === state);
|
|
74
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Engine } from "../engine/engine.js";
|
|
2
|
+
import type { Logger } from "../logger.js";
|
|
3
|
+
import type { IEntityRepository, IFlowRepository, IInvocationRepository } from "../repositories/interfaces.js";
|
|
4
|
+
/** Adapter for AI model providers (Anthropic, OpenAI, etc.). */
|
|
5
|
+
export interface IAIProviderAdapter {
|
|
6
|
+
/** Send a prompt to an AI model and return the response content. */
|
|
7
|
+
invoke(prompt: string, config: {
|
|
8
|
+
model: string;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
content: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
export interface ActiveRunnerDeps {
|
|
14
|
+
engine: Engine;
|
|
15
|
+
aiAdapter: IAIProviderAdapter;
|
|
16
|
+
invocationRepo: IInvocationRepository;
|
|
17
|
+
entityRepo: IEntityRepository;
|
|
18
|
+
flowRepo: IFlowRepository;
|
|
19
|
+
modelTierMap?: Record<string, string>;
|
|
20
|
+
logger?: Logger;
|
|
21
|
+
}
|
|
22
|
+
export interface ActiveRunnerRunOptions {
|
|
23
|
+
pollIntervalMs?: number;
|
|
24
|
+
once?: boolean;
|
|
25
|
+
flowName?: string;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}
|
|
28
|
+
export declare class ActiveRunner {
|
|
29
|
+
private engine;
|
|
30
|
+
private aiAdapter;
|
|
31
|
+
private invocationRepo;
|
|
32
|
+
private entityRepo;
|
|
33
|
+
private flowRepo;
|
|
34
|
+
private modelTierMap;
|
|
35
|
+
private logger;
|
|
36
|
+
constructor(deps: ActiveRunnerDeps);
|
|
37
|
+
run(options?: ActiveRunnerRunOptions): Promise<void>;
|
|
38
|
+
private pollOnce;
|
|
39
|
+
private processInvocation;
|
|
40
|
+
private resolveModel;
|
|
41
|
+
parseResponse(content: string): {
|
|
42
|
+
signal: string;
|
|
43
|
+
artifacts: Record<string, unknown>;
|
|
44
|
+
} | null;
|
|
45
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { consoleLogger } from "../logger.js";
|
|
2
|
+
const DEFAULT_MODEL_TIER_MAP = {
|
|
3
|
+
opus: "claude-opus-4-6",
|
|
4
|
+
sonnet: "claude-sonnet-4-6",
|
|
5
|
+
haiku: "claude-haiku-4-5-20251001",
|
|
6
|
+
};
|
|
7
|
+
const DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
9
|
+
export class ActiveRunner {
|
|
10
|
+
engine;
|
|
11
|
+
aiAdapter;
|
|
12
|
+
invocationRepo;
|
|
13
|
+
entityRepo;
|
|
14
|
+
flowRepo;
|
|
15
|
+
modelTierMap;
|
|
16
|
+
logger;
|
|
17
|
+
constructor(deps) {
|
|
18
|
+
this.engine = deps.engine;
|
|
19
|
+
this.aiAdapter = deps.aiAdapter;
|
|
20
|
+
this.invocationRepo = deps.invocationRepo;
|
|
21
|
+
this.entityRepo = deps.entityRepo;
|
|
22
|
+
this.flowRepo = deps.flowRepo;
|
|
23
|
+
this.modelTierMap = deps.modelTierMap ?? DEFAULT_MODEL_TIER_MAP;
|
|
24
|
+
this.logger = deps.logger ?? consoleLogger;
|
|
25
|
+
}
|
|
26
|
+
async run(options = {}) {
|
|
27
|
+
const { pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, once = false, flowName, signal } = options;
|
|
28
|
+
let flowId;
|
|
29
|
+
if (flowName) {
|
|
30
|
+
const flow = await this.flowRepo.getByName(flowName);
|
|
31
|
+
if (!flow)
|
|
32
|
+
throw new Error(`Flow "${flowName}" not found`);
|
|
33
|
+
flowId = flow.id;
|
|
34
|
+
}
|
|
35
|
+
while (true) {
|
|
36
|
+
if (signal?.aborted)
|
|
37
|
+
break;
|
|
38
|
+
const processed = await this.pollOnce(flowId, signal);
|
|
39
|
+
if (once)
|
|
40
|
+
break;
|
|
41
|
+
if (!processed) {
|
|
42
|
+
await sleep(pollIntervalMs, signal);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async pollOnce(flowId, signal) {
|
|
47
|
+
const candidates = await this.invocationRepo.findUnclaimedActive(flowId);
|
|
48
|
+
if (candidates.length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
for (const invocation of candidates) {
|
|
51
|
+
const claimed = await this.invocationRepo.claim(invocation.id, "active-runner");
|
|
52
|
+
if (!claimed)
|
|
53
|
+
continue;
|
|
54
|
+
await this.processInvocation(claimed, signal);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
async processInvocation(invocation, signal) {
|
|
60
|
+
const model = await this.resolveModel(invocation);
|
|
61
|
+
let content;
|
|
62
|
+
try {
|
|
63
|
+
const response = await this.aiAdapter.invoke(invocation.prompt, { model });
|
|
64
|
+
content = response.content;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
68
|
+
await this.invocationRepo.fail(invocation.id, message);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const parsed = this.parseResponse(content);
|
|
72
|
+
if (!parsed) {
|
|
73
|
+
await this.invocationRepo.fail(invocation.id, "No SIGNAL found in AI response");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Complete the current invocation BEFORE calling processSignal so the
|
|
77
|
+
// concurrency check inside the engine doesn't count it as still-active.
|
|
78
|
+
try {
|
|
79
|
+
await this.invocationRepo.complete(invocation.id, parsed.signal, parsed.artifacts);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
this.logger.error(`[active-runner] complete() failed for invocation ${invocation.id} before processSignal:`, err);
|
|
83
|
+
try {
|
|
84
|
+
await this.invocationRepo.releaseClaim(invocation.id);
|
|
85
|
+
}
|
|
86
|
+
catch (releaseErr) {
|
|
87
|
+
this.logger.error("[active-runner] releaseClaim failed:", releaseErr);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const MAX_PROCESS_SIGNAL_RETRIES = 3;
|
|
92
|
+
let result;
|
|
93
|
+
try {
|
|
94
|
+
result = await this.engine.processSignal(invocation.entityId, parsed.signal, parsed.artifacts, invocation.id);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
this.logger.error(`[active-runner] processSignal failed for entity ${invocation.entityId}:`, err);
|
|
98
|
+
// processSignal failed after we already completed the invocation. Track retry count
|
|
99
|
+
// via context to prevent infinite re-queue loops on persistent errors.
|
|
100
|
+
const retryCount = invocation.context?.retryCount ?? 0;
|
|
101
|
+
if (retryCount >= MAX_PROCESS_SIGNAL_RETRIES) {
|
|
102
|
+
this.logger.error(`[active-runner] entity ${invocation.entityId} exceeded max processSignal retries (${MAX_PROCESS_SIGNAL_RETRIES}), marking as stuck`);
|
|
103
|
+
await this.entityRepo.updateArtifacts(invocation.entityId, { stuck: true, stuckAt: new Date().toISOString() });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
await this.invocationRepo.create(invocation.entityId, invocation.stage, invocation.prompt, invocation.mode, undefined, { ...(invocation.context ?? {}), retryCount: retryCount + 1 });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Gate timed out — re-queue after backoff instead of failing.
|
|
110
|
+
// Create a replacement unclaimed invocation so the entity can be reclaimed
|
|
111
|
+
// on the next poll cycle after the backoff wait.
|
|
112
|
+
if (result.gated && result.gateTimedOut) {
|
|
113
|
+
const retryAfterMs = 30000;
|
|
114
|
+
this.logger.info(`[active-runner] gate timed out for entity ${invocation.entityId}, re-queuing after ${retryAfterMs}ms`);
|
|
115
|
+
await this.invocationRepo.create(invocation.entityId, invocation.stage, invocation.prompt, invocation.mode, undefined, invocation.context ?? undefined);
|
|
116
|
+
await sleep(retryAfterMs, signal);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async resolveModel(invocation) {
|
|
121
|
+
const entity = await this.entityRepo.get(invocation.entityId);
|
|
122
|
+
if (!entity)
|
|
123
|
+
return DEFAULT_MODEL;
|
|
124
|
+
const flow = await this.flowRepo.get(entity.flowId);
|
|
125
|
+
if (!flow)
|
|
126
|
+
return DEFAULT_MODEL;
|
|
127
|
+
const state = flow.states.find((s) => s.name === invocation.stage);
|
|
128
|
+
const tier = state?.modelTier ?? flow.defaultModelTier;
|
|
129
|
+
if (!tier)
|
|
130
|
+
return DEFAULT_MODEL;
|
|
131
|
+
return this.modelTierMap[tier] ?? DEFAULT_MODEL;
|
|
132
|
+
}
|
|
133
|
+
parseResponse(content) {
|
|
134
|
+
const signalMatch = content.match(/^SIGNAL:\s*(.+)$/m);
|
|
135
|
+
if (!signalMatch)
|
|
136
|
+
return null;
|
|
137
|
+
const signal = signalMatch[1].trim();
|
|
138
|
+
let artifacts = {};
|
|
139
|
+
const artifactsMatch = content.match(/^ARTIFACTS:\s*\n([\s\S]*)/m);
|
|
140
|
+
if (artifactsMatch) {
|
|
141
|
+
try {
|
|
142
|
+
artifacts = JSON.parse(artifactsMatch[1].trim());
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
this.logger.warn("[active-runner] Failed to parse ARTIFACTS JSON, using empty object. Raw content:", artifactsMatch[1].trim());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { signal, artifacts };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function sleep(ms, signal) {
|
|
152
|
+
if (signal?.aborted)
|
|
153
|
+
return Promise.resolve();
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const onAbort = () => {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
resolve();
|
|
158
|
+
};
|
|
159
|
+
const timer = setTimeout(() => {
|
|
160
|
+
signal?.removeEventListener("abort", onAbort);
|
|
161
|
+
resolve();
|
|
162
|
+
}, ms);
|
|
163
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
export declare const AdminFlowCreateSchema: z.ZodObject<{
|
|
3
|
+
name: z.ZodString;
|
|
4
|
+
initialState: z.ZodString;
|
|
5
|
+
discipline: z.ZodOptional<z.ZodString>;
|
|
6
|
+
defaultModelTier: z.ZodOptional<z.ZodString>;
|
|
7
|
+
description: z.ZodOptional<z.ZodString>;
|
|
8
|
+
entitySchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
9
|
+
maxConcurrent: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
maxConcurrentPerRepo: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
affinityWindowMs: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
gateTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
createdBy: z.ZodOptional<z.ZodString>;
|
|
14
|
+
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
15
|
+
states: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
16
|
+
name: z.ZodString;
|
|
17
|
+
modelTier: z.ZodOptional<z.ZodString>;
|
|
18
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
19
|
+
passive: "passive";
|
|
20
|
+
active: "active";
|
|
21
|
+
}>>;
|
|
22
|
+
promptTemplate: z.ZodOptional<z.ZodString>;
|
|
23
|
+
constraints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
24
|
+
}, z.core.$strip>>>;
|
|
25
|
+
}, z.core.$strip>;
|
|
26
|
+
export declare const AdminFlowUpdateSchema: z.ZodObject<{
|
|
27
|
+
flow_name: z.ZodString;
|
|
28
|
+
description: z.ZodOptional<z.ZodString>;
|
|
29
|
+
discipline: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
30
|
+
defaultModelTier: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
31
|
+
maxConcurrent: z.ZodOptional<z.ZodNumber>;
|
|
32
|
+
maxConcurrentPerRepo: z.ZodOptional<z.ZodNumber>;
|
|
33
|
+
affinityWindowMs: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
gateTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
initialState: z.ZodOptional<z.ZodString>;
|
|
36
|
+
timeoutPrompt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
export declare const AdminStateCreateSchema: z.ZodObject<{
|
|
39
|
+
flow_name: z.ZodString;
|
|
40
|
+
name: z.ZodString;
|
|
41
|
+
modelTier: z.ZodOptional<z.ZodString>;
|
|
42
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
43
|
+
passive: "passive";
|
|
44
|
+
active: "active";
|
|
45
|
+
}>>;
|
|
46
|
+
promptTemplate: z.ZodOptional<z.ZodString>;
|
|
47
|
+
constraints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
export declare const AdminStateUpdateSchema: z.ZodObject<{
|
|
50
|
+
flow_name: z.ZodString;
|
|
51
|
+
state_name: z.ZodString;
|
|
52
|
+
modelTier: z.ZodOptional<z.ZodString>;
|
|
53
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
54
|
+
passive: "passive";
|
|
55
|
+
active: "active";
|
|
56
|
+
}>>;
|
|
57
|
+
promptTemplate: z.ZodOptional<z.ZodString>;
|
|
58
|
+
constraints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
59
|
+
}, z.core.$strip>;
|
|
60
|
+
export declare const AdminTransitionCreateSchema: z.ZodObject<{
|
|
61
|
+
flow_name: z.ZodString;
|
|
62
|
+
fromState: z.ZodString;
|
|
63
|
+
toState: z.ZodString;
|
|
64
|
+
trigger: z.ZodString;
|
|
65
|
+
gateName: z.ZodOptional<z.ZodString>;
|
|
66
|
+
condition: z.ZodOptional<z.ZodString>;
|
|
67
|
+
priority: z.ZodOptional<z.ZodNumber>;
|
|
68
|
+
spawnFlow: z.ZodOptional<z.ZodString>;
|
|
69
|
+
spawnTemplate: z.ZodOptional<z.ZodString>;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
export declare const AdminTransitionUpdateSchema: z.ZodObject<{
|
|
72
|
+
flow_name: z.ZodString;
|
|
73
|
+
transition_id: z.ZodString;
|
|
74
|
+
fromState: z.ZodOptional<z.ZodString>;
|
|
75
|
+
toState: z.ZodOptional<z.ZodString>;
|
|
76
|
+
trigger: z.ZodOptional<z.ZodString>;
|
|
77
|
+
gateName: z.ZodOptional<z.ZodString>;
|
|
78
|
+
condition: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
79
|
+
priority: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
80
|
+
spawnFlow: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
81
|
+
spawnTemplate: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
export declare const AdminGateCreateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
84
|
+
name: z.ZodString;
|
|
85
|
+
type: z.ZodLiteral<"command">;
|
|
86
|
+
command: z.ZodString;
|
|
87
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
88
|
+
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
89
|
+
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
90
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
91
|
+
name: z.ZodString;
|
|
92
|
+
type: z.ZodLiteral<"function">;
|
|
93
|
+
functionRef: z.ZodString;
|
|
94
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
95
|
+
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
96
|
+
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
97
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
98
|
+
name: z.ZodString;
|
|
99
|
+
type: z.ZodLiteral<"api">;
|
|
100
|
+
apiConfig: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
101
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
102
|
+
failurePrompt: z.ZodOptional<z.ZodString>;
|
|
103
|
+
timeoutPrompt: z.ZodOptional<z.ZodString>;
|
|
104
|
+
}, z.core.$strip>], "type">;
|
|
105
|
+
export declare const AdminGateAttachSchema: z.ZodObject<{
|
|
106
|
+
flow_name: z.ZodString;
|
|
107
|
+
transition_id: z.ZodString;
|
|
108
|
+
gate_name: z.ZodString;
|
|
109
|
+
}, z.core.$strip>;
|
|
110
|
+
export declare const AdminFlowSnapshotSchema: z.ZodObject<{
|
|
111
|
+
flow_name: z.ZodString;
|
|
112
|
+
}, z.core.$strip>;
|
|
113
|
+
export declare const AdminFlowRestoreSchema: z.ZodObject<{
|
|
114
|
+
flow_name: z.ZodString;
|
|
115
|
+
version: z.ZodNumber;
|
|
116
|
+
}, z.core.$strip>;
|