@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,278 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { EXECUTION_CONTRACT_VERSION, parseDecideExecutionApproval, parseExecutionApprovalRecord, parseExecutionArtifactRecord, parseRequestExecutionApproval, parseRevokeExecutionApproval, } from "@tomflow/proflow-execution-contracts";
|
|
4
|
+
function canonical(value) {
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return value.map(canonical);
|
|
7
|
+
if (typeof value !== "object" || value === null)
|
|
8
|
+
return value;
|
|
9
|
+
return Object.fromEntries(Object.entries(value)
|
|
10
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
11
|
+
.map(([key, item]) => [key, canonical(item)]));
|
|
12
|
+
}
|
|
13
|
+
function sha(value) {
|
|
14
|
+
return `sha256:${createHash("sha256")
|
|
15
|
+
.update(JSON.stringify(canonical(value)))
|
|
16
|
+
.digest("hex")}`;
|
|
17
|
+
}
|
|
18
|
+
export class ExecutionApprovalOwnerError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "ExecutionApprovalOwnerError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function createExecutionApprovalOwner(input) {
|
|
27
|
+
const database = new DatabaseSync(input.databasePath);
|
|
28
|
+
database.exec("PRAGMA foreign_keys=ON; PRAGMA journal_mode=WAL; PRAGMA busy_timeout=2500;");
|
|
29
|
+
database.exec(`
|
|
30
|
+
CREATE TABLE IF NOT EXISTS execution_approvals (
|
|
31
|
+
approval_ref TEXT PRIMARY KEY,
|
|
32
|
+
execution_ref TEXT NOT NULL,
|
|
33
|
+
record_json TEXT NOT NULL,
|
|
34
|
+
created_at TEXT NOT NULL,
|
|
35
|
+
FOREIGN KEY(execution_ref) REFERENCES executions(execution_ref)
|
|
36
|
+
);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS execution_approvals_execution_idx
|
|
38
|
+
ON execution_approvals(execution_ref, created_at);
|
|
39
|
+
`);
|
|
40
|
+
const now = input.now ?? (() => new Date());
|
|
41
|
+
const idFactory = input.idFactory ?? randomUUID;
|
|
42
|
+
const getRow = (approvalRef) => database
|
|
43
|
+
.prepare("SELECT * FROM execution_approvals WHERE approval_ref=?")
|
|
44
|
+
.get(approvalRef);
|
|
45
|
+
const getExecutionRow = (executionRef) => database
|
|
46
|
+
.prepare("SELECT * FROM executions WHERE execution_ref=?")
|
|
47
|
+
.get(executionRef);
|
|
48
|
+
const patchArtifactBinding = (request) => {
|
|
49
|
+
if (request.capability !== "patch.apply")
|
|
50
|
+
return undefined;
|
|
51
|
+
const row = database
|
|
52
|
+
.prepare("SELECT record_json FROM artifact_registry WHERE artifact_ref=?")
|
|
53
|
+
.get(request.input.artifactRef);
|
|
54
|
+
if (!row)
|
|
55
|
+
throw new ExecutionApprovalOwnerError("ARTIFACT_NOT_FOUND", "patch approval requires the durable patch Artifact");
|
|
56
|
+
const artifact = parseExecutionArtifactRecord(JSON.parse(String(row.record_json)));
|
|
57
|
+
if (artifact.kind !== "patch-proposal")
|
|
58
|
+
throw new ExecutionApprovalOwnerError("ARTIFACT_KIND_INVALID", "patch approval requires a patch-proposal Artifact");
|
|
59
|
+
return {
|
|
60
|
+
artifactRef: artifact.artifactRef,
|
|
61
|
+
hash: artifact.hash,
|
|
62
|
+
ownerCallerRef: artifact.ownerCallerRef,
|
|
63
|
+
taskId: artifact.taskId,
|
|
64
|
+
nodeId: artifact.nodeId,
|
|
65
|
+
roleRef: artifact.roleRef,
|
|
66
|
+
workerRef: artifact.workerRef,
|
|
67
|
+
baseHash: artifact.metadata?.baseHash,
|
|
68
|
+
baseRef: artifact.metadata?.baseRef,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const approvalEffectFingerprint = (request, inputFingerprint) => sha({
|
|
72
|
+
capability: request.capability,
|
|
73
|
+
inputFingerprint,
|
|
74
|
+
projectRoot: request.projectRoot,
|
|
75
|
+
patchArtifact: patchArtifactBinding(request),
|
|
76
|
+
});
|
|
77
|
+
const fromRow = (row) => parseExecutionApprovalRecord(JSON.parse(String(row.record_json)));
|
|
78
|
+
const save = (record) => {
|
|
79
|
+
database
|
|
80
|
+
.prepare("UPDATE execution_approvals SET record_json=? WHERE approval_ref=?")
|
|
81
|
+
.run(JSON.stringify(record), record.approvalRef);
|
|
82
|
+
};
|
|
83
|
+
const listApprovals = (filter = {}) => {
|
|
84
|
+
const rows = filter.executionRef
|
|
85
|
+
? database
|
|
86
|
+
.prepare("SELECT * FROM execution_approvals WHERE execution_ref=? ORDER BY created_at, approval_ref")
|
|
87
|
+
.all(filter.executionRef)
|
|
88
|
+
: database
|
|
89
|
+
.prepare("SELECT * FROM execution_approvals ORDER BY created_at, approval_ref")
|
|
90
|
+
.all();
|
|
91
|
+
return rows
|
|
92
|
+
.map((row) => get(String(row.approval_ref)))
|
|
93
|
+
.filter((record) => !filter.status || record.status === filter.status);
|
|
94
|
+
};
|
|
95
|
+
const get = (approvalRef) => {
|
|
96
|
+
const row = getRow(approvalRef);
|
|
97
|
+
if (!row)
|
|
98
|
+
throw new ExecutionApprovalOwnerError("APPROVAL_NOT_FOUND", "approval was not found");
|
|
99
|
+
let record = fromRow(row);
|
|
100
|
+
if (record.status === "APPROVED" &&
|
|
101
|
+
Date.parse(record.expiresAt) <= now().getTime()) {
|
|
102
|
+
record = parseExecutionApprovalRecord({
|
|
103
|
+
...record,
|
|
104
|
+
status: "EXPIRED",
|
|
105
|
+
version: record.version + 1,
|
|
106
|
+
});
|
|
107
|
+
save(record);
|
|
108
|
+
}
|
|
109
|
+
return record;
|
|
110
|
+
};
|
|
111
|
+
const ensurePendingApproval = (value) => {
|
|
112
|
+
const executionRow = getExecutionRow(value.executionRef);
|
|
113
|
+
if (!executionRow)
|
|
114
|
+
throw new ExecutionApprovalOwnerError("EXECUTION_NOT_FOUND", "execution was not found");
|
|
115
|
+
const inputFingerprint = String(executionRow.input_fingerprint);
|
|
116
|
+
const existing = database
|
|
117
|
+
.prepare("SELECT * FROM execution_approvals WHERE execution_ref=? ORDER BY created_at DESC")
|
|
118
|
+
.all(value.executionRef)
|
|
119
|
+
.map(fromRow)
|
|
120
|
+
.find((record) => record.status === "PENDING" &&
|
|
121
|
+
record.inputFingerprint === inputFingerprint &&
|
|
122
|
+
Date.parse(record.expiresAt) > now().getTime());
|
|
123
|
+
if (existing)
|
|
124
|
+
return existing;
|
|
125
|
+
return requestApproval({
|
|
126
|
+
contract: "execution.approval",
|
|
127
|
+
contractVersion: EXECUTION_CONTRACT_VERSION,
|
|
128
|
+
executionRef: value.executionRef,
|
|
129
|
+
actorRef: value.actorRef,
|
|
130
|
+
expiresAt: value.expiresAt,
|
|
131
|
+
...(value.precondition
|
|
132
|
+
? { preconditionFingerprint: sha(value.precondition) }
|
|
133
|
+
: {}),
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
const requestApproval = (value) => {
|
|
137
|
+
const request = parseRequestExecutionApproval(value);
|
|
138
|
+
if (Date.parse(request.expiresAt) <= now().getTime())
|
|
139
|
+
throw new ExecutionApprovalOwnerError("APPROVAL_EXPIRY_INVALID", "approval expiry must be in the future");
|
|
140
|
+
const executionRow = getExecutionRow(request.executionRef);
|
|
141
|
+
if (!executionRow)
|
|
142
|
+
throw new ExecutionApprovalOwnerError("EXECUTION_NOT_FOUND", "execution was not found");
|
|
143
|
+
const executionRequest = JSON.parse(String(executionRow.request_json));
|
|
144
|
+
const executionRecord = JSON.parse(String(executionRow.record_json));
|
|
145
|
+
const inputFingerprint = String(executionRow.input_fingerprint);
|
|
146
|
+
const stamp = now().toISOString();
|
|
147
|
+
const record = parseExecutionApprovalRecord({
|
|
148
|
+
contract: "execution.approval",
|
|
149
|
+
contractVersion: EXECUTION_CONTRACT_VERSION,
|
|
150
|
+
approvalRef: `approval:${idFactory()}`,
|
|
151
|
+
executionRef: request.executionRef,
|
|
152
|
+
callerRef: executionRequest.callerRef,
|
|
153
|
+
...(executionRequest.taskId ? { taskId: executionRequest.taskId } : {}),
|
|
154
|
+
...(executionRequest.nodeId ? { nodeId: executionRequest.nodeId } : {}),
|
|
155
|
+
...(executionRequest.roleRef
|
|
156
|
+
? { roleRef: executionRequest.roleRef }
|
|
157
|
+
: {}),
|
|
158
|
+
...(executionRequest.workerRef
|
|
159
|
+
? { workerRef: executionRequest.workerRef }
|
|
160
|
+
: {}),
|
|
161
|
+
capability: executionRequest.capability,
|
|
162
|
+
inputFingerprint,
|
|
163
|
+
effectFingerprint: approvalEffectFingerprint(executionRequest, inputFingerprint),
|
|
164
|
+
scopeFingerprint: sha({
|
|
165
|
+
callerRef: executionRequest.callerRef,
|
|
166
|
+
taskId: executionRequest.taskId,
|
|
167
|
+
nodeId: executionRequest.nodeId,
|
|
168
|
+
roleRef: executionRequest.roleRef,
|
|
169
|
+
workerRef: executionRequest.workerRef,
|
|
170
|
+
projectRoot: executionRequest.projectRoot,
|
|
171
|
+
}),
|
|
172
|
+
...(request.preconditionFingerprint
|
|
173
|
+
? { preconditionFingerprint: request.preconditionFingerprint }
|
|
174
|
+
: {}),
|
|
175
|
+
status: "PENDING",
|
|
176
|
+
requestedByActorRef: request.actorRef,
|
|
177
|
+
requestedAt: stamp,
|
|
178
|
+
expiresAt: request.expiresAt,
|
|
179
|
+
version: 1,
|
|
180
|
+
});
|
|
181
|
+
database
|
|
182
|
+
.prepare("INSERT INTO execution_approvals VALUES (?, ?, ?, ?)")
|
|
183
|
+
.run(record.approvalRef, record.executionRef, JSON.stringify(record), stamp);
|
|
184
|
+
void executionRecord;
|
|
185
|
+
return record;
|
|
186
|
+
};
|
|
187
|
+
const decideApproval = (value) => {
|
|
188
|
+
const request = parseDecideExecutionApproval(value);
|
|
189
|
+
const current = get(request.approvalRef);
|
|
190
|
+
if (current.version !== request.expectedVersion)
|
|
191
|
+
throw new ExecutionApprovalOwnerError("APPROVAL_VERSION_CONFLICT", "approval version changed");
|
|
192
|
+
if (current.status !== "PENDING")
|
|
193
|
+
throw new ExecutionApprovalOwnerError("APPROVAL_STATE_INVALID", "only PENDING approval may be decided");
|
|
194
|
+
const stamp = now().toISOString();
|
|
195
|
+
const next = parseExecutionApprovalRecord({
|
|
196
|
+
...current,
|
|
197
|
+
status: request.decision === "ALLOW" ? "APPROVED" : "DENIED",
|
|
198
|
+
decidedByActorRef: request.actorRef,
|
|
199
|
+
...(request.reason ? { reason: request.reason } : {}),
|
|
200
|
+
decidedAt: stamp,
|
|
201
|
+
version: current.version + 1,
|
|
202
|
+
});
|
|
203
|
+
save(next);
|
|
204
|
+
return next;
|
|
205
|
+
};
|
|
206
|
+
const revokeApproval = (value) => {
|
|
207
|
+
const request = parseRevokeExecutionApproval(value);
|
|
208
|
+
const current = get(request.approvalRef);
|
|
209
|
+
if (current.version !== request.expectedVersion)
|
|
210
|
+
throw new ExecutionApprovalOwnerError("APPROVAL_VERSION_CONFLICT", "approval version changed");
|
|
211
|
+
if (current.status !== "PENDING" && current.status !== "APPROVED")
|
|
212
|
+
throw new ExecutionApprovalOwnerError("APPROVAL_STATE_INVALID", "approval cannot be revoked from current state");
|
|
213
|
+
const next = parseExecutionApprovalRecord({
|
|
214
|
+
...current,
|
|
215
|
+
status: "REVOKED",
|
|
216
|
+
decidedByActorRef: request.actorRef,
|
|
217
|
+
reason: request.reason,
|
|
218
|
+
revokedAt: now().toISOString(),
|
|
219
|
+
version: current.version + 1,
|
|
220
|
+
});
|
|
221
|
+
save(next);
|
|
222
|
+
return next;
|
|
223
|
+
};
|
|
224
|
+
const validate = (value) => {
|
|
225
|
+
const record = get(value.approvalRef);
|
|
226
|
+
if (record.status !== "APPROVED")
|
|
227
|
+
return false;
|
|
228
|
+
if (record.executionRef !== value.executionRef ||
|
|
229
|
+
record.callerRef !== value.callerRef ||
|
|
230
|
+
record.capability !== value.capability ||
|
|
231
|
+
record.inputFingerprint !== value.inputFingerprint)
|
|
232
|
+
return false;
|
|
233
|
+
if ((record.taskId ?? undefined) !== (value.taskId ?? undefined))
|
|
234
|
+
return false;
|
|
235
|
+
const effectFingerprint = approvalEffectFingerprint(value.request, value.inputFingerprint);
|
|
236
|
+
if (record.effectFingerprint !== effectFingerprint)
|
|
237
|
+
return false;
|
|
238
|
+
const scopeFingerprint = sha({
|
|
239
|
+
callerRef: value.request.callerRef,
|
|
240
|
+
taskId: value.request.taskId,
|
|
241
|
+
nodeId: value.request.nodeId,
|
|
242
|
+
roleRef: value.request.roleRef,
|
|
243
|
+
workerRef: value.request.workerRef,
|
|
244
|
+
projectRoot: value.request.projectRoot,
|
|
245
|
+
});
|
|
246
|
+
if (record.scopeFingerprint !== scopeFingerprint)
|
|
247
|
+
return false;
|
|
248
|
+
if (value.precondition && record.preconditionFingerprint) {
|
|
249
|
+
if (record.preconditionFingerprint !== sha(value.precondition))
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return Date.parse(record.expiresAt) > Date.parse(value.now);
|
|
253
|
+
};
|
|
254
|
+
const consume = (approvalRef) => {
|
|
255
|
+
const current = get(approvalRef);
|
|
256
|
+
if (current.status !== "APPROVED")
|
|
257
|
+
throw new ExecutionApprovalOwnerError("APPROVAL_STATE_INVALID", "only APPROVED approval may be consumed");
|
|
258
|
+
const next = parseExecutionApprovalRecord({
|
|
259
|
+
...current,
|
|
260
|
+
status: "CONSUMED",
|
|
261
|
+
consumedAt: now().toISOString(),
|
|
262
|
+
version: current.version + 1,
|
|
263
|
+
});
|
|
264
|
+
save(next);
|
|
265
|
+
return next;
|
|
266
|
+
};
|
|
267
|
+
return Object.freeze({
|
|
268
|
+
requestApproval,
|
|
269
|
+
ensurePendingApproval,
|
|
270
|
+
decideApproval,
|
|
271
|
+
revokeApproval,
|
|
272
|
+
getApproval: get,
|
|
273
|
+
listApprovals,
|
|
274
|
+
validate,
|
|
275
|
+
consume,
|
|
276
|
+
close: () => database.close(),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type BrowserVisionPort } from "@tomflow/proflow-execution-browser-extension";
|
|
2
|
+
/**
|
|
3
|
+
* Typed Browser Vision inference client. It talks to the same Model Runtime
|
|
4
|
+
* `infer` boundary as the command-risk decision client, but for the
|
|
5
|
+
* spec-ized `browser.page-vision.v1` REASON-only image observation. A Vision
|
|
6
|
+
* call returns an OBSERVED interpretation or a typed DEFERRED fail-safe; it
|
|
7
|
+
* never fabricates page state and never decides Task/Execution/Approval truth.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createExecutionBrowserVisionClient(config: {
|
|
10
|
+
endpoint: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
credential?: string;
|
|
13
|
+
}): Readonly<{
|
|
14
|
+
port: BrowserVisionPort;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { deferVisionObservation, } from "@tomflow/proflow-execution-browser-extension";
|
|
2
|
+
import { browserPageVisionObservationSchema, inferenceResultSchema, } from "@tomflow/proflow-model-contracts";
|
|
3
|
+
const loopbackHosts = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
4
|
+
const browserPageVisionSpecRef = "browser.page-vision.v1";
|
|
5
|
+
/**
|
|
6
|
+
* Typed Browser Vision inference client. It talks to the same Model Runtime
|
|
7
|
+
* `infer` boundary as the command-risk decision client, but for the
|
|
8
|
+
* spec-ized `browser.page-vision.v1` REASON-only image observation. A Vision
|
|
9
|
+
* call returns an OBSERVED interpretation or a typed DEFERRED fail-safe; it
|
|
10
|
+
* never fabricates page state and never decides Task/Execution/Approval truth.
|
|
11
|
+
*/
|
|
12
|
+
export function createExecutionBrowserVisionClient(config) {
|
|
13
|
+
const endpoint = new URL(config.endpoint);
|
|
14
|
+
if (endpoint.protocol !== "http:" ||
|
|
15
|
+
!loopbackHosts.has(endpoint.hostname) ||
|
|
16
|
+
endpoint.pathname !== "/" ||
|
|
17
|
+
endpoint.search ||
|
|
18
|
+
endpoint.hash)
|
|
19
|
+
throw new TypeError("modelDecision.endpoint must be loopback HTTP root");
|
|
20
|
+
if (config.credential && config.credential.length < 32)
|
|
21
|
+
throw new TypeError("modelDecision credential must contain at least 32 characters");
|
|
22
|
+
const timeoutMs = config.timeoutMs ?? 12_000;
|
|
23
|
+
const authorizationHeaders = config.credential
|
|
24
|
+
? { authorization: `Bearer ${config.credential}` }
|
|
25
|
+
: {};
|
|
26
|
+
const port = {
|
|
27
|
+
async inspect({ image, observationContext }) {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(`${endpoint.origin}/infer`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
...authorizationHeaders,
|
|
33
|
+
"content-type": "application/json",
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
contractVersion: "1.0.0",
|
|
37
|
+
specRef: browserPageVisionSpecRef,
|
|
38
|
+
mode: "reason",
|
|
39
|
+
priority: "business",
|
|
40
|
+
trace: { callerRef: "execution-runtime:browser-vision" },
|
|
41
|
+
payload: {
|
|
42
|
+
targetRef: observationContext.targetRef,
|
|
43
|
+
pageState: observationContext.pageState,
|
|
44
|
+
activityKind: observationContext.activityKind,
|
|
45
|
+
observedAt: observationContext.observedAt,
|
|
46
|
+
},
|
|
47
|
+
images: [{ mimeType: image.mimeType, data: image.base64 }],
|
|
48
|
+
timeoutMs,
|
|
49
|
+
}),
|
|
50
|
+
signal: AbortSignal.timeout(timeoutMs + 1_000),
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
throw new Error(`MODEL_INFER_HTTP_${response.status}`);
|
|
54
|
+
const result = inferenceResultSchema.parse(await response.json());
|
|
55
|
+
if (result.specRef !== browserPageVisionSpecRef ||
|
|
56
|
+
result.requestedMode !== "reason")
|
|
57
|
+
throw new Error("MODEL_INFERENCE_PROTOCOL_MISMATCH");
|
|
58
|
+
if (result.status !== "SUCCEEDED")
|
|
59
|
+
throw new Error(result.error?.code ?? result.status);
|
|
60
|
+
const data = browserPageVisionObservationSchema.parse(result.data);
|
|
61
|
+
const observation = {
|
|
62
|
+
status: "OBSERVED",
|
|
63
|
+
observationRef: result.inferenceRef,
|
|
64
|
+
pageState: data.pageState,
|
|
65
|
+
activityKind: data.activityKind,
|
|
66
|
+
confidence: data.confidence,
|
|
67
|
+
recommendedNext: data.recommendedNext,
|
|
68
|
+
reasonCode: data.reasonCode,
|
|
69
|
+
rationale: data.rationale,
|
|
70
|
+
};
|
|
71
|
+
return Object.freeze(observation);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return deferVisionObservation("VISION_INFERENCE_FAILED", error instanceof Error ? error.message : "vision inference failed");
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
return Object.freeze({ port });
|
|
79
|
+
}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createFormalExecutionRuntimeLifecycle } from "./formal-process.js";
|
|
4
|
+
import { loadExecutionRuntimeProcessConfig } from "./service.js";
|
|
5
|
+
function installSelf() {
|
|
6
|
+
const executable = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
7
|
+
const result = spawnSync(executable, [
|
|
8
|
+
"--yes",
|
|
9
|
+
"@tomflow/proflow-platform-cli",
|
|
10
|
+
"install",
|
|
11
|
+
"@tomflow/proflow-execution-runtime",
|
|
12
|
+
], { cwd: process.cwd(), env: process.env, stdio: "inherit" });
|
|
13
|
+
if (result.error)
|
|
14
|
+
throw result.error;
|
|
15
|
+
process.exit(result.status ?? 1);
|
|
16
|
+
}
|
|
17
|
+
async function main() {
|
|
18
|
+
const [command, configPath] = process.argv.slice(2);
|
|
19
|
+
if (command === "--help" || command === "-h") {
|
|
20
|
+
process.stdout.write("Usage: proflow-execution-runtime install | start /absolute/config.json\\n");
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
if (command === "install") {
|
|
24
|
+
if (configPath)
|
|
25
|
+
throw new Error("Usage: proflow-execution-runtime install");
|
|
26
|
+
installSelf();
|
|
27
|
+
}
|
|
28
|
+
if (command !== "start" || !configPath)
|
|
29
|
+
throw new Error("Usage: proflow-execution-runtime start /absolute/config.json");
|
|
30
|
+
const config = await loadExecutionRuntimeProcessConfig(configPath);
|
|
31
|
+
const service = createFormalExecutionRuntimeLifecycle({
|
|
32
|
+
config,
|
|
33
|
+
log: (entry) => process.stderr.write(`${JSON.stringify(entry)}\n`),
|
|
34
|
+
});
|
|
35
|
+
const address = await service.start();
|
|
36
|
+
process.stdout.write(`${JSON.stringify({ status: "RUNNING", ...address })}\n`);
|
|
37
|
+
let stopping = false;
|
|
38
|
+
const stop = () => {
|
|
39
|
+
if (stopping)
|
|
40
|
+
return;
|
|
41
|
+
stopping = true;
|
|
42
|
+
void service.stop().finally(() => process.exit(0));
|
|
43
|
+
};
|
|
44
|
+
process.on("SIGINT", stop);
|
|
45
|
+
process.on("SIGTERM", stop);
|
|
46
|
+
await new Promise(() => { });
|
|
47
|
+
}
|
|
48
|
+
if (import.meta.main) {
|
|
49
|
+
await main();
|
|
50
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility re-export. The structural Executor Port contract is owned by
|
|
3
|
+
* execution-contracts so concrete executors do not depend back on runtime.
|
|
4
|
+
*/
|
|
5
|
+
export type { ExecutionExecutorPort, ExecutorAdmission, ExecutorApprovalState, ExecutorArtifact, ExecutorArtifactRead, ExecutorDecisionPath, ExecutorInvocation, ExecutorPrecondition, ExecutorReconciliation, ExecutorResult, } from "@tomflow/proflow-execution-contracts";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { createLocalExecutor, type LocalExecutorOptions } from "@tomflow/proflow-execution-local";
|
|
2
|
+
import type { ExecutionExecutorPort } from "../executor-port.ts";
|
|
3
|
+
export declare function adaptLocalExecutor(executor: Awaited<ReturnType<typeof createLocalExecutor>>): ExecutionExecutorPort;
|
|
4
|
+
export declare function createLocalExecutorPort(options: LocalExecutorOptions): Promise<ExecutionExecutorPort>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { localCapabilityIds, } from "@tomflow/proflow-execution-contracts";
|
|
2
|
+
import { createLocalExecutor, } from "@tomflow/proflow-execution-local";
|
|
3
|
+
const localCapabilitySet = new Set(localCapabilityIds);
|
|
4
|
+
/**
|
|
5
|
+
* Adapts the concrete `execution-local` executor library to the owner-neutral
|
|
6
|
+
* Runtime Executor Port. `execution-local` keeps its own concrete public
|
|
7
|
+
* types; this boundary is the only place that translates them.
|
|
8
|
+
*/
|
|
9
|
+
function toLocalPrecondition(precondition) {
|
|
10
|
+
switch (precondition.kind) {
|
|
11
|
+
case "file.write":
|
|
12
|
+
case "patch.apply":
|
|
13
|
+
case "git.commit":
|
|
14
|
+
case "install-dependency":
|
|
15
|
+
case "process.start":
|
|
16
|
+
case "process.stop":
|
|
17
|
+
return precondition;
|
|
18
|
+
case "opaque": {
|
|
19
|
+
if (!localCapabilitySet.has(precondition.capability))
|
|
20
|
+
throw new TypeError("a non-local capability precondition reached the local adapter");
|
|
21
|
+
return {
|
|
22
|
+
kind: "opaque",
|
|
23
|
+
capability: precondition.capability,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
case "browser":
|
|
27
|
+
throw new TypeError("a browser precondition cannot be routed to the local executor");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function adaptLocalExecutor(executor) {
|
|
31
|
+
return {
|
|
32
|
+
bindPatchArtifactResolver(resolver) {
|
|
33
|
+
executor.bindPatchArtifactResolver(resolver);
|
|
34
|
+
},
|
|
35
|
+
async execute(invocation) {
|
|
36
|
+
const { onEffectStarted, ...rest } = invocation;
|
|
37
|
+
return executor.execute({
|
|
38
|
+
...rest,
|
|
39
|
+
...(onEffectStarted
|
|
40
|
+
? {
|
|
41
|
+
onEffectStarted: (precondition) => onEffectStarted(precondition),
|
|
42
|
+
}
|
|
43
|
+
: {}),
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
async reconcile(request, precondition) {
|
|
47
|
+
return executor.reconcile(request, toLocalPrecondition(precondition));
|
|
48
|
+
},
|
|
49
|
+
observePrecondition: executor.observePrecondition,
|
|
50
|
+
readArtifact: executor.readArtifact,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function createLocalExecutorPort(options) {
|
|
54
|
+
return adaptLocalExecutor(await createLocalExecutor(options));
|
|
55
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ExecutionRuntimeProcessConfig } from "./service.ts";
|
|
2
|
+
export declare function createFormalExecutionRuntimeLifecycle(input: {
|
|
3
|
+
config: ExecutionRuntimeProcessConfig;
|
|
4
|
+
log?: (entry: Record<string, unknown>) => void;
|
|
5
|
+
}): Readonly<{
|
|
6
|
+
status: () => import("./service.ts").ExecutionRuntimeProcessStatus | {
|
|
7
|
+
readiness: "NOT_READY";
|
|
8
|
+
};
|
|
9
|
+
start(): Promise<{
|
|
10
|
+
host: string;
|
|
11
|
+
port: number;
|
|
12
|
+
browserBridgeEndpoint: string;
|
|
13
|
+
}>;
|
|
14
|
+
stop(): Promise<void>;
|
|
15
|
+
restart(): Promise<{
|
|
16
|
+
host: string;
|
|
17
|
+
port: number;
|
|
18
|
+
browserBridgeEndpoint: string;
|
|
19
|
+
}>;
|
|
20
|
+
}>;
|