@tomflow/proflow-execution-runtime 0.1.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 +5 -0
- package/conformance.json +4 -0
- package/dist/deployment/adapter.d.ts +458 -0
- package/dist/deployment/adapter.js +265 -0
- package/dist/deployment/descriptor.d.ts +105 -0
- package/dist/deployment/descriptor.js +123 -0
- package/dist/deployment/requirements.d.ts +5 -0
- package/dist/deployment/requirements.js +5 -0
- package/dist/deployment/verification.d.ts +3 -0
- package/dist/deployment/verification.js +7 -0
- package/dist/src/approval-owner.d.ts +40 -0
- package/dist/src/approval-owner.js +278 -0
- package/dist/src/browser-vision-client.d.ts +15 -0
- package/dist/src/browser-vision-client.js +79 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +50 -0
- package/dist/src/executor-port.d.ts +5 -0
- package/dist/src/executor-port.js +1 -0
- package/dist/src/executors/local-adapter.d.ts +4 -0
- package/dist/src/executors/local-adapter.js +55 -0
- package/dist/src/formal-process.d.ts +20 -0
- package/dist/src/formal-process.js +161 -0
- package/dist/src/index.d.ts +122 -0
- package/dist/src/index.js +983 -0
- package/dist/src/model-decision-client.d.ts +10 -0
- package/dist/src/model-decision-client.js +244 -0
- package/dist/src/service.d.ts +67 -0
- package/dist/src/service.js +473 -0
- package/package.json +56 -0
- package/proflow.module.json +139 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
5
|
+
import { artifactMaterializationResponseSchema, EXECUTION_CONTRACT_VERSION, materializeContextPackRequestSchema, materializeExternalFilesRequestSchema, materializeExternalFilesResponseSchema, materializePatchProposalRequestSchema, parseExecutionRef, } from "@tomflow/proflow-execution-contracts";
|
|
6
|
+
import { createLocalExecutorPort } from "./executors/local-adapter.js";
|
|
7
|
+
import { createExecutionRuntime, } from "./index.js";
|
|
8
|
+
function object(value, name) {
|
|
9
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
10
|
+
throw new TypeError(`${name} must be an object`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function string(value, name) {
|
|
14
|
+
if (typeof value !== "string" || value.length === 0)
|
|
15
|
+
throw new TypeError(`${name} must be a non-empty string`);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
export function parseExecutionRuntimeProcessConfig(value) {
|
|
19
|
+
const input = object(value, "execution-runtime config");
|
|
20
|
+
const databasePath = resolve(string(input.databasePath, "databasePath"));
|
|
21
|
+
const projectRoot = resolve(string(input.projectRoot, "projectRoot"));
|
|
22
|
+
const artifactRoot = resolve(string(input.artifactRoot, "artifactRoot"));
|
|
23
|
+
if (![databasePath, projectRoot, artifactRoot].every(isAbsolute))
|
|
24
|
+
throw new TypeError("runtime paths must resolve to absolute paths");
|
|
25
|
+
const port = input.port === undefined ? 0 : Number(input.port);
|
|
26
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535)
|
|
27
|
+
throw new TypeError("port must be an integer from 0 through 65535");
|
|
28
|
+
const exactNetworkTargets = input.exactNetworkTargets ?? [];
|
|
29
|
+
const browserExecutorConfigPath = input.browserExecutorConfigPath === undefined
|
|
30
|
+
? undefined
|
|
31
|
+
: string(input.browserExecutorConfigPath, "browserExecutorConfigPath");
|
|
32
|
+
if (browserExecutorConfigPath !== undefined &&
|
|
33
|
+
!isAbsolute(browserExecutorConfigPath))
|
|
34
|
+
throw new TypeError("browserExecutorConfigPath must be absolute");
|
|
35
|
+
const transportCredentialFile = input.transportCredentialFile === undefined
|
|
36
|
+
? undefined
|
|
37
|
+
: string(input.transportCredentialFile, "transportCredentialFile");
|
|
38
|
+
if (transportCredentialFile !== undefined &&
|
|
39
|
+
!isAbsolute(transportCredentialFile))
|
|
40
|
+
throw new TypeError("transportCredentialFile must be absolute");
|
|
41
|
+
let modelDecision;
|
|
42
|
+
if (input.modelDecision !== undefined) {
|
|
43
|
+
const value = object(input.modelDecision, "modelDecision");
|
|
44
|
+
const endpoint = new URL(string(value.endpoint, "modelDecision.endpoint"));
|
|
45
|
+
if (endpoint.protocol !== "http:" ||
|
|
46
|
+
!new Set(["127.0.0.1", "localhost", "::1", "[::1]"]).has(endpoint.hostname) ||
|
|
47
|
+
endpoint.pathname !== "/" ||
|
|
48
|
+
endpoint.search ||
|
|
49
|
+
endpoint.hash)
|
|
50
|
+
throw new TypeError("modelDecision.endpoint must be loopback HTTP root");
|
|
51
|
+
const timeoutMs = value.timeoutMs === undefined ? undefined : Number(value.timeoutMs);
|
|
52
|
+
if (timeoutMs !== undefined &&
|
|
53
|
+
(!Number.isInteger(timeoutMs) || timeoutMs <= 0))
|
|
54
|
+
throw new TypeError("modelDecision.timeoutMs must be a positive integer");
|
|
55
|
+
const credentialFile = value.credentialFile === undefined
|
|
56
|
+
? undefined
|
|
57
|
+
: string(value.credentialFile, "modelDecision.credentialFile");
|
|
58
|
+
if (credentialFile !== undefined && !isAbsolute(credentialFile))
|
|
59
|
+
throw new TypeError("modelDecision.credentialFile must be absolute");
|
|
60
|
+
modelDecision = {
|
|
61
|
+
endpoint: endpoint.origin,
|
|
62
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
63
|
+
...(credentialFile === undefined ? {} : { credentialFile }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
let identity;
|
|
67
|
+
if (input.identity !== undefined) {
|
|
68
|
+
const value = object(input.identity, "identity");
|
|
69
|
+
const endpoint = new URL(string(value.endpoint, "identity.endpoint"));
|
|
70
|
+
if (endpoint.protocol !== "http:" ||
|
|
71
|
+
!new Set(["127.0.0.1", "localhost", "::1", "[::1]"]).has(endpoint.hostname) ||
|
|
72
|
+
endpoint.pathname !== "/" ||
|
|
73
|
+
endpoint.search ||
|
|
74
|
+
endpoint.hash)
|
|
75
|
+
throw new TypeError("identity.endpoint must be loopback HTTP root");
|
|
76
|
+
const tokenFile = string(value.tokenFile, "identity.tokenFile");
|
|
77
|
+
if (!isAbsolute(tokenFile))
|
|
78
|
+
throw new TypeError("identity.tokenFile must be absolute");
|
|
79
|
+
identity = { endpoint: endpoint.origin, tokenFile };
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(exactNetworkTargets) ||
|
|
82
|
+
exactNetworkTargets.some((item) => typeof item !== "string"))
|
|
83
|
+
throw new TypeError("exactNetworkTargets must be a string array");
|
|
84
|
+
return {
|
|
85
|
+
databasePath,
|
|
86
|
+
projectRoot,
|
|
87
|
+
artifactRoot,
|
|
88
|
+
host: input.host === undefined ? "127.0.0.1" : string(input.host, "host"),
|
|
89
|
+
port,
|
|
90
|
+
exactNetworkTargets: [...exactNetworkTargets],
|
|
91
|
+
...(browserExecutorConfigPath ? { browserExecutorConfigPath } : {}),
|
|
92
|
+
...(transportCredentialFile ? { transportCredentialFile } : {}),
|
|
93
|
+
...(identity ? { identity } : {}),
|
|
94
|
+
...(modelDecision ? { modelDecision } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export async function loadExecutionRuntimeProcessConfig(path) {
|
|
98
|
+
return parseExecutionRuntimeProcessConfig(JSON.parse(await readFile(resolve(path), "utf8")));
|
|
99
|
+
}
|
|
100
|
+
export async function createExecutionRuntimeProcess(input) {
|
|
101
|
+
if (input.transportCredential && input.transportCredential.length < 32)
|
|
102
|
+
throw new TypeError("transport credential must contain at least 32 characters");
|
|
103
|
+
let state = "STOPPED";
|
|
104
|
+
let accepting = false;
|
|
105
|
+
let server;
|
|
106
|
+
let runtime;
|
|
107
|
+
const active = new Set();
|
|
108
|
+
const log = (event, fields = {}) => input.log?.({
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
component: "execution-runtime-process",
|
|
111
|
+
event,
|
|
112
|
+
...fields,
|
|
113
|
+
});
|
|
114
|
+
const status = () => {
|
|
115
|
+
const browserExecutor = input.browserExecutor
|
|
116
|
+
? input.browserReadiness?.() === false
|
|
117
|
+
? "UNAVAILABLE"
|
|
118
|
+
: "READY"
|
|
119
|
+
: "UNAVAILABLE";
|
|
120
|
+
const browserRequirementSatisfied = input.config.browserExecutorConfigPath === undefined ||
|
|
121
|
+
browserExecutor === "READY";
|
|
122
|
+
const identity = input.identity && input.identityReadiness?.() !== false
|
|
123
|
+
? "READY"
|
|
124
|
+
: "UNAVAILABLE";
|
|
125
|
+
const identityRequirementSatisfied = input.config.identity === undefined || identity === "READY";
|
|
126
|
+
const transportAuth = input.transportCredential ? "READY" : "UNAVAILABLE";
|
|
127
|
+
const transportRequirementSatisfied = input.config.transportCredentialFile === undefined ||
|
|
128
|
+
transportAuth === "READY";
|
|
129
|
+
const modelDecision = input.modelDecision && input.modelDecisionReadiness?.() !== false
|
|
130
|
+
? "READY"
|
|
131
|
+
: "UNAVAILABLE";
|
|
132
|
+
const modelRequirementSatisfied = input.requireModelDecision !== true || modelDecision === "READY";
|
|
133
|
+
return {
|
|
134
|
+
process: state,
|
|
135
|
+
liveness: state === "STOPPED" ? "DOWN" : "UP",
|
|
136
|
+
readiness: state === "RUNNING" &&
|
|
137
|
+
accepting &&
|
|
138
|
+
runtime &&
|
|
139
|
+
browserRequirementSatisfied &&
|
|
140
|
+
identityRequirementSatisfied &&
|
|
141
|
+
transportRequirementSatisfied &&
|
|
142
|
+
modelRequirementSatisfied
|
|
143
|
+
? "READY"
|
|
144
|
+
: "NOT_READY",
|
|
145
|
+
accepting,
|
|
146
|
+
inFlight: active.size,
|
|
147
|
+
databasePath: input.config.databasePath,
|
|
148
|
+
localExecutor: runtime ? "READY" : "UNAVAILABLE",
|
|
149
|
+
browserExecutor,
|
|
150
|
+
identity,
|
|
151
|
+
transportAuth,
|
|
152
|
+
modelDecision,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
const respond = (response, code, value) => {
|
|
156
|
+
response.writeHead(code, {
|
|
157
|
+
"content-type": "application/json; charset=utf-8",
|
|
158
|
+
"cache-control": "no-store",
|
|
159
|
+
});
|
|
160
|
+
response.end(JSON.stringify(value));
|
|
161
|
+
};
|
|
162
|
+
const start = async () => {
|
|
163
|
+
if (state !== "STOPPED")
|
|
164
|
+
throw new Error("execution runtime is not stopped");
|
|
165
|
+
state = "STARTING";
|
|
166
|
+
try {
|
|
167
|
+
const localExecutor = await createLocalExecutorPort({
|
|
168
|
+
projectRoot: input.config.projectRoot,
|
|
169
|
+
artifactRoot: input.config.artifactRoot,
|
|
170
|
+
exactNetworkTargets: input.config.exactNetworkTargets,
|
|
171
|
+
});
|
|
172
|
+
runtime = await createExecutionRuntime({
|
|
173
|
+
databasePath: input.config.databasePath,
|
|
174
|
+
localExecutor,
|
|
175
|
+
...(input.browserExecutor
|
|
176
|
+
? { browserExecutor: input.browserExecutor }
|
|
177
|
+
: {}),
|
|
178
|
+
...(input.identity ? { identity: input.identity } : {}),
|
|
179
|
+
...(input.policy ? { policy: input.policy } : {}),
|
|
180
|
+
...(input.modelDecision ? { modelDecision: input.modelDecision } : {}),
|
|
181
|
+
...(input.approval ? { approval: input.approval } : {}),
|
|
182
|
+
});
|
|
183
|
+
server = createServer((request, response) => {
|
|
184
|
+
const work = (async () => {
|
|
185
|
+
const url = new URL(request.url ?? "/", "http://execution.local");
|
|
186
|
+
if (request.method === "GET" && url.pathname === "/health")
|
|
187
|
+
return respond(response, 200, { status: "UP" });
|
|
188
|
+
if (request.method === "GET" && url.pathname === "/ready") {
|
|
189
|
+
const current = status();
|
|
190
|
+
return respond(response, current.readiness === "READY" ? 200 : 503, current);
|
|
191
|
+
}
|
|
192
|
+
if (input.transportCredential) {
|
|
193
|
+
const supplied = request.headers.authorization?.startsWith("Bearer ")
|
|
194
|
+
? request.headers.authorization.slice(7)
|
|
195
|
+
: "";
|
|
196
|
+
const expectedBytes = Buffer.from(input.transportCredential);
|
|
197
|
+
const suppliedBytes = Buffer.from(supplied);
|
|
198
|
+
if (expectedBytes.length !== suppliedBytes.length ||
|
|
199
|
+
!timingSafeEqual(expectedBytes, suppliedBytes))
|
|
200
|
+
return respond(response, 401, { error: "AUTHENTICATION_FAILED" });
|
|
201
|
+
}
|
|
202
|
+
if (!accepting || !runtime)
|
|
203
|
+
return respond(response, 503, { error: "SERVICE_DRAINING" });
|
|
204
|
+
try {
|
|
205
|
+
if (request.method === "GET" && url.pathname === "/carrier/summary")
|
|
206
|
+
return respond(response, 200, input.carrierSummary?.() ?? {
|
|
207
|
+
online: false,
|
|
208
|
+
extensionInstanceId: null,
|
|
209
|
+
queuedCommands: 0,
|
|
210
|
+
pendingCommands: 0,
|
|
211
|
+
});
|
|
212
|
+
if (request.method === "GET" &&
|
|
213
|
+
url.pathname === "/artifacts/summary")
|
|
214
|
+
return respond(response, 200, runtime.getArtifactSummary());
|
|
215
|
+
const chunks = [];
|
|
216
|
+
let byteLength = 0;
|
|
217
|
+
for await (const chunk of request) {
|
|
218
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
219
|
+
? chunk
|
|
220
|
+
: Buffer.from(chunk);
|
|
221
|
+
byteLength += buffer.byteLength;
|
|
222
|
+
if (byteLength > 1_048_576)
|
|
223
|
+
throw new TypeError("REQUEST_BODY_TOO_LARGE");
|
|
224
|
+
chunks.push(buffer);
|
|
225
|
+
}
|
|
226
|
+
const body = chunks.length
|
|
227
|
+
? JSON.parse(Buffer.concat(chunks).toString("utf8"))
|
|
228
|
+
: undefined;
|
|
229
|
+
if (request.method === "POST" &&
|
|
230
|
+
url.pathname === "/artifacts/context-pack") {
|
|
231
|
+
const materialization = materializeContextPackRequestSchema.parse(body);
|
|
232
|
+
const execution = await runtime.executeCapability({
|
|
233
|
+
contract: "execution",
|
|
234
|
+
contractVersion: EXECUTION_CONTRACT_VERSION,
|
|
235
|
+
callerRef: materialization.callerRef,
|
|
236
|
+
idempotencyKey: materialization.idempotencyKey,
|
|
237
|
+
...(materialization.correlationId
|
|
238
|
+
? { correlationId: materialization.correlationId }
|
|
239
|
+
: {}),
|
|
240
|
+
taskId: materialization.taskId,
|
|
241
|
+
nodeId: materialization.nodeId,
|
|
242
|
+
...(materialization.roleRef
|
|
243
|
+
? { roleRef: materialization.roleRef }
|
|
244
|
+
: {}),
|
|
245
|
+
...(materialization.workerRef
|
|
246
|
+
? { workerRef: materialization.workerRef }
|
|
247
|
+
: {}),
|
|
248
|
+
projectRoot: input.config.projectRoot,
|
|
249
|
+
capability: "artifact.context-pack.materialize",
|
|
250
|
+
input: {
|
|
251
|
+
entries: materialization.entries,
|
|
252
|
+
...(materialization.secrets
|
|
253
|
+
? { secrets: materialization.secrets }
|
|
254
|
+
: {}),
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
if (execution.status !== "SUCCEEDED" ||
|
|
258
|
+
execution.result?.capability !==
|
|
259
|
+
"artifact.context-pack.materialize")
|
|
260
|
+
return respond(response, 409, execution);
|
|
261
|
+
const artifact = runtime.getArtifactRecord({
|
|
262
|
+
artifactRef: execution.result.data.artifactRef,
|
|
263
|
+
callerRef: materialization.callerRef,
|
|
264
|
+
taskId: materialization.taskId,
|
|
265
|
+
nodeId: materialization.nodeId,
|
|
266
|
+
...(materialization.roleRef
|
|
267
|
+
? { roleRef: materialization.roleRef }
|
|
268
|
+
: {}),
|
|
269
|
+
...(materialization.workerRef
|
|
270
|
+
? { workerRef: materialization.workerRef }
|
|
271
|
+
: {}),
|
|
272
|
+
});
|
|
273
|
+
return respond(response, 200, artifactMaterializationResponseSchema.parse({
|
|
274
|
+
contract: "execution.artifact-materialization",
|
|
275
|
+
contractVersion: EXECUTION_CONTRACT_VERSION,
|
|
276
|
+
executionRef: execution.executionRef,
|
|
277
|
+
artifact,
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
if (request.method === "POST" &&
|
|
281
|
+
url.pathname === "/artifacts/patch-proposal") {
|
|
282
|
+
const materialization = materializePatchProposalRequestSchema.parse(body);
|
|
283
|
+
const execution = await runtime.executeCapability({
|
|
284
|
+
contract: "execution",
|
|
285
|
+
contractVersion: EXECUTION_CONTRACT_VERSION,
|
|
286
|
+
callerRef: materialization.callerRef,
|
|
287
|
+
idempotencyKey: materialization.idempotencyKey,
|
|
288
|
+
...(materialization.correlationId
|
|
289
|
+
? { correlationId: materialization.correlationId }
|
|
290
|
+
: {}),
|
|
291
|
+
taskId: materialization.taskId,
|
|
292
|
+
nodeId: materialization.nodeId,
|
|
293
|
+
...(materialization.roleRef
|
|
294
|
+
? { roleRef: materialization.roleRef }
|
|
295
|
+
: {}),
|
|
296
|
+
...(materialization.workerRef
|
|
297
|
+
? { workerRef: materialization.workerRef }
|
|
298
|
+
: {}),
|
|
299
|
+
projectRoot: input.config.projectRoot,
|
|
300
|
+
capability: "artifact.patch-proposal.materialize",
|
|
301
|
+
input: { proposal: materialization.proposal },
|
|
302
|
+
});
|
|
303
|
+
if (execution.status !== "SUCCEEDED" ||
|
|
304
|
+
execution.result?.capability !==
|
|
305
|
+
"artifact.patch-proposal.materialize")
|
|
306
|
+
return respond(response, 409, execution);
|
|
307
|
+
const artifact = runtime.getArtifactRecord({
|
|
308
|
+
artifactRef: execution.result.data.artifactRef,
|
|
309
|
+
callerRef: materialization.callerRef,
|
|
310
|
+
taskId: materialization.taskId,
|
|
311
|
+
nodeId: materialization.nodeId,
|
|
312
|
+
...(materialization.roleRef
|
|
313
|
+
? { roleRef: materialization.roleRef }
|
|
314
|
+
: {}),
|
|
315
|
+
...(materialization.workerRef
|
|
316
|
+
? { workerRef: materialization.workerRef }
|
|
317
|
+
: {}),
|
|
318
|
+
});
|
|
319
|
+
return respond(response, 200, artifactMaterializationResponseSchema.parse({
|
|
320
|
+
contract: "execution.artifact-materialization",
|
|
321
|
+
contractVersion: EXECUTION_CONTRACT_VERSION,
|
|
322
|
+
executionRef: execution.executionRef,
|
|
323
|
+
artifact,
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
if (request.method === "POST" &&
|
|
327
|
+
url.pathname === "/external-files/materialize") {
|
|
328
|
+
const materialization = materializeExternalFilesRequestSchema.parse(body);
|
|
329
|
+
const execution = await runtime.executeCapability({
|
|
330
|
+
contract: "execution",
|
|
331
|
+
contractVersion: EXECUTION_CONTRACT_VERSION,
|
|
332
|
+
callerRef: materialization.callerRef,
|
|
333
|
+
idempotencyKey: materialization.idempotencyKey,
|
|
334
|
+
...(materialization.correlationId
|
|
335
|
+
? { correlationId: materialization.correlationId }
|
|
336
|
+
: {}),
|
|
337
|
+
...(materialization.taskId
|
|
338
|
+
? { taskId: materialization.taskId }
|
|
339
|
+
: {}),
|
|
340
|
+
...(materialization.nodeId
|
|
341
|
+
? { nodeId: materialization.nodeId }
|
|
342
|
+
: {}),
|
|
343
|
+
...(materialization.roleRef
|
|
344
|
+
? { roleRef: materialization.roleRef }
|
|
345
|
+
: {}),
|
|
346
|
+
...(materialization.workerRef
|
|
347
|
+
? { workerRef: materialization.workerRef }
|
|
348
|
+
: {}),
|
|
349
|
+
projectRoot: input.config.projectRoot,
|
|
350
|
+
capability: "artifact.external-file.materialize",
|
|
351
|
+
input: { files: materialization.files },
|
|
352
|
+
});
|
|
353
|
+
if (execution.status !== "SUCCEEDED" ||
|
|
354
|
+
execution.result?.capability !==
|
|
355
|
+
"artifact.external-file.materialize")
|
|
356
|
+
return respond(response, 409, execution);
|
|
357
|
+
return respond(response, 200, materializeExternalFilesResponseSchema.parse({
|
|
358
|
+
contract: "execution.external-file-materialization",
|
|
359
|
+
contractVersion: EXECUTION_CONTRACT_VERSION,
|
|
360
|
+
executionRef: execution.executionRef,
|
|
361
|
+
files: execution.result.data.files,
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
if (request.method === "POST" &&
|
|
365
|
+
url.pathname === "/approvals/request")
|
|
366
|
+
return respond(response, 200, runtime.requestExecutionApproval(body));
|
|
367
|
+
if (request.method === "POST" &&
|
|
368
|
+
url.pathname === "/approvals/decide")
|
|
369
|
+
return respond(response, 200, runtime.decideExecutionApproval(body));
|
|
370
|
+
if (request.method === "POST" &&
|
|
371
|
+
url.pathname === "/approvals/revoke")
|
|
372
|
+
return respond(response, 200, runtime.revokeExecutionApproval(body));
|
|
373
|
+
if (request.method === "POST" && url.pathname === "/approvals/list")
|
|
374
|
+
return respond(response, 200, {
|
|
375
|
+
approvals: runtime.listExecutionApprovals(object(body ?? {}, "approval list input")),
|
|
376
|
+
});
|
|
377
|
+
if (request.method === "GET" &&
|
|
378
|
+
url.pathname.startsWith("/approvals/"))
|
|
379
|
+
return respond(response, 200, runtime.getExecutionApproval(decodeURIComponent(url.pathname.slice(11))));
|
|
380
|
+
if (request.method === "POST" && url.pathname === "/executions")
|
|
381
|
+
return respond(response, 200, await runtime.executeCapability(body));
|
|
382
|
+
if (request.method === "POST" &&
|
|
383
|
+
url.pathname === "/executions/lookup")
|
|
384
|
+
return respond(response, 200, runtime.lookupExecutionIntent(body));
|
|
385
|
+
if (request.method === "POST" &&
|
|
386
|
+
url.pathname === "/observer-signals/list") {
|
|
387
|
+
const value = object(body ?? {}, "observer signal list input");
|
|
388
|
+
return respond(response, 200, {
|
|
389
|
+
signals: runtime.listExecutionObserverSignals(typeof value.limit === "number" ? value.limit : 50),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (request.method === "POST" &&
|
|
393
|
+
url.pathname === "/observer-signals/ack") {
|
|
394
|
+
const value = object(body, "observer signal ack input");
|
|
395
|
+
return respond(response, 200, runtime.acknowledgeExecutionObserverSignal(string(value.signalRef, "signalRef")));
|
|
396
|
+
}
|
|
397
|
+
if (request.method === "POST" &&
|
|
398
|
+
url.pathname === "/executions/cancel")
|
|
399
|
+
return respond(response, 200, await runtime.cancelExecution(body));
|
|
400
|
+
if (request.method === "POST" &&
|
|
401
|
+
url.pathname === "/executions/output") {
|
|
402
|
+
const callerRef = request.headers["x-proflow-caller-ref"];
|
|
403
|
+
if (typeof callerRef !== "string" || callerRef.length === 0)
|
|
404
|
+
throw new Error("EXECUTION_CALLER_REQUIRED");
|
|
405
|
+
return respond(response, 200, await runtime.readExecutionOutputForCaller(body, callerRef));
|
|
406
|
+
}
|
|
407
|
+
if (request.method === "GET" &&
|
|
408
|
+
url.pathname.startsWith("/executions/")) {
|
|
409
|
+
const callerRef = request.headers["x-proflow-caller-ref"];
|
|
410
|
+
if (typeof callerRef !== "string" || callerRef.length === 0)
|
|
411
|
+
throw new Error("EXECUTION_CALLER_REQUIRED");
|
|
412
|
+
return respond(response, 200, runtime.getExecutionForCaller({
|
|
413
|
+
executionRef: parseExecutionRef(decodeURIComponent(url.pathname.slice(12))),
|
|
414
|
+
callerRef,
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
respond(response, 404, { error: "NOT_FOUND" });
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
respond(response, 400, {
|
|
421
|
+
error: error instanceof Error ? error.message : "INVALID_REQUEST",
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
})();
|
|
425
|
+
active.add(work);
|
|
426
|
+
void work.finally(() => active.delete(work));
|
|
427
|
+
});
|
|
428
|
+
await new Promise((resolveStart, reject) => {
|
|
429
|
+
server?.once("error", reject);
|
|
430
|
+
server?.listen(input.config.port, input.config.host, resolveStart);
|
|
431
|
+
});
|
|
432
|
+
accepting = true;
|
|
433
|
+
state = "RUNNING";
|
|
434
|
+
const address = server.address();
|
|
435
|
+
if (!address || typeof address === "string")
|
|
436
|
+
throw new Error("missing address");
|
|
437
|
+
log("SERVICE_STARTED", { host: input.config.host, port: address.port });
|
|
438
|
+
return { host: input.config.host, port: address.port };
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
state = "STOPPED";
|
|
442
|
+
accepting = false;
|
|
443
|
+
runtime?.close();
|
|
444
|
+
runtime = undefined;
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
const stop = async () => {
|
|
449
|
+
if (state === "STOPPED")
|
|
450
|
+
return;
|
|
451
|
+
state = "DRAINING";
|
|
452
|
+
accepting = false;
|
|
453
|
+
const running = server;
|
|
454
|
+
server = undefined;
|
|
455
|
+
if (running)
|
|
456
|
+
await new Promise((resolveStop, reject) => running.close((error) => (error ? reject(error) : resolveStop())));
|
|
457
|
+
await Promise.allSettled([...active]);
|
|
458
|
+
runtime?.close();
|
|
459
|
+
runtime = undefined;
|
|
460
|
+
state = "STOPPED";
|
|
461
|
+
log("SERVICE_STOPPED");
|
|
462
|
+
};
|
|
463
|
+
return Object.freeze({
|
|
464
|
+
start,
|
|
465
|
+
stop,
|
|
466
|
+
async restart() {
|
|
467
|
+
await stop();
|
|
468
|
+
return start();
|
|
469
|
+
},
|
|
470
|
+
status,
|
|
471
|
+
stateDirectory: dirname(input.config.databasePath),
|
|
472
|
+
});
|
|
473
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tomflow/proflow-execution-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/src/index.js",
|
|
10
|
+
"./service": "./dist/src/service.js",
|
|
11
|
+
"./deployment/adapter": "./dist/deployment/adapter.js",
|
|
12
|
+
"./deployment/descriptor": "./dist/deployment/descriptor.js",
|
|
13
|
+
"./cli": "./dist/src/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"proflow-execution-runtime": "./dist/src/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"proflow.module.json",
|
|
21
|
+
"conformance.json",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@tomflow/proflow-execution-contracts": "^0.1.0",
|
|
26
|
+
"@tomflow/proflow-execution-browser-extension": "^0.1.0",
|
|
27
|
+
"@tomflow/proflow-execution-local": "^0.1.0",
|
|
28
|
+
"@tomflow/proflow-model-contracts": "^0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@tomflow/proflow-deployment-conformance": "^0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"description": "Durable, policy-controlled Execution Core orchestration over SQLite and controlled executor ports.",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"proflow",
|
|
36
|
+
"proflow-module",
|
|
37
|
+
"execution"
|
|
38
|
+
],
|
|
39
|
+
"proflow": {
|
|
40
|
+
"module": true,
|
|
41
|
+
"installClass": "core",
|
|
42
|
+
"descriptor": "./dist/deployment/descriptor.js",
|
|
43
|
+
"installRequires": [
|
|
44
|
+
"@tomflow/proflow-execution-browser-extension",
|
|
45
|
+
"@tomflow/proflow-execution-contracts",
|
|
46
|
+
"@tomflow/proflow-execution-local",
|
|
47
|
+
"@tomflow/proflow-model-contracts",
|
|
48
|
+
"@tomflow/proflow-platform-host"
|
|
49
|
+
],
|
|
50
|
+
"manifest": "./proflow.module.json"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"test": "node --conditions=development --test tests/**/*.test.ts",
|
|
54
|
+
"typecheck": "tsc --noEmit"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contract": "module",
|
|
3
|
+
"contractVersion": "1.0.0",
|
|
4
|
+
"moduleRef": "execution-runtime",
|
|
5
|
+
"packageName": "@tomflow/proflow-execution-runtime",
|
|
6
|
+
"moduleVersion": "0.1.0",
|
|
7
|
+
"kind": "service",
|
|
8
|
+
"templateVersion": "1.0.0",
|
|
9
|
+
"platformCompatibility": ">=1.0.0 <2.0.0",
|
|
10
|
+
"installClass": "core",
|
|
11
|
+
"identity": {
|
|
12
|
+
"domain": "execution",
|
|
13
|
+
"summary": "Durable, policy-controlled Execution Core orchestration over SQLite and controlled executor ports."
|
|
14
|
+
},
|
|
15
|
+
"provides": [
|
|
16
|
+
{
|
|
17
|
+
"contractRef": "execution",
|
|
18
|
+
"version": "1.0.0"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"requires": [
|
|
22
|
+
{
|
|
23
|
+
"contractRef": "execution-local",
|
|
24
|
+
"versionRange": ">=1.0.0 <2.0.0"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"requirements": [
|
|
28
|
+
{
|
|
29
|
+
"kind": "runtime",
|
|
30
|
+
"runtime": "node",
|
|
31
|
+
"versionRange": ">=24.19.0"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"kind": "filesystem",
|
|
35
|
+
"path": ".proflow",
|
|
36
|
+
"access": "read-write"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"configSlots": [
|
|
40
|
+
{
|
|
41
|
+
"key": "databasePath",
|
|
42
|
+
"type": "path",
|
|
43
|
+
"required": true,
|
|
44
|
+
"description": "Execution SQLite database"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"key": "projectRoot",
|
|
48
|
+
"type": "path",
|
|
49
|
+
"required": true,
|
|
50
|
+
"description": "Canonical project boundary consumed by the local executor"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"key": "artifactRoot",
|
|
54
|
+
"type": "path",
|
|
55
|
+
"required": true,
|
|
56
|
+
"description": "Execution-owned Artifact/Evidence filesystem root"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"key": "browserExecutorConfigPath",
|
|
60
|
+
"type": "path",
|
|
61
|
+
"required": true,
|
|
62
|
+
"description": "Absolute configuration path for the formal Browser Reality Bridge composition"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"key": "transportCredentialFile",
|
|
66
|
+
"type": "path",
|
|
67
|
+
"required": true,
|
|
68
|
+
"sensitive": true,
|
|
69
|
+
"description": "File containing the Execution Runtime transport credential"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"key": "identity.endpoint",
|
|
73
|
+
"type": "url",
|
|
74
|
+
"required": true,
|
|
75
|
+
"description": "Loopback platform-host execution identity endpoint"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"key": "identity.tokenFile",
|
|
79
|
+
"type": "path",
|
|
80
|
+
"required": true,
|
|
81
|
+
"sensitive": true,
|
|
82
|
+
"description": "File containing the Execution-to-platform-host identity transport credential"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"key": "modelDecision.endpoint",
|
|
86
|
+
"type": "url",
|
|
87
|
+
"required": true,
|
|
88
|
+
"description": "Loopback Model Runtime endpoint used by execution.command-risk.v1"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"key": "modelDecision.credentialFile",
|
|
92
|
+
"type": "path",
|
|
93
|
+
"required": true,
|
|
94
|
+
"sensitive": true,
|
|
95
|
+
"description": "File containing the Execution-to-Model transport credential"
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
"lifecycle": {
|
|
99
|
+
"supported": [
|
|
100
|
+
"describe",
|
|
101
|
+
"preflight",
|
|
102
|
+
"status",
|
|
103
|
+
"verify",
|
|
104
|
+
"doctor",
|
|
105
|
+
"start",
|
|
106
|
+
"stop",
|
|
107
|
+
"restart",
|
|
108
|
+
"uninstall"
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"verification": {
|
|
112
|
+
"checks": [
|
|
113
|
+
{
|
|
114
|
+
"id": "execution-runtime-critical-proofs",
|
|
115
|
+
"description": "Execution Runtime critical proofs pass",
|
|
116
|
+
"lifecycle": "verify"
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"effects": [
|
|
121
|
+
{
|
|
122
|
+
"kind": "process",
|
|
123
|
+
"description": "Runs the single backend Execution Runtime service",
|
|
124
|
+
"retention": "remove"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"kind": "filesystem",
|
|
128
|
+
"description": "Persists execution state, output references and evidence in WAL SQLite",
|
|
129
|
+
"retention": "preserve"
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
"documentation": [
|
|
133
|
+
{
|
|
134
|
+
"id": "overview",
|
|
135
|
+
"path": "./README.md",
|
|
136
|
+
"description": "Execution Runtime package overview"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|