@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,10 @@
|
|
|
1
|
+
import type { ExecutionModelDecisionPort } from "./index.ts";
|
|
2
|
+
export declare function createExecutionModelDecisionClient(config: {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
credential?: string;
|
|
6
|
+
}): Readonly<{
|
|
7
|
+
port: ExecutionModelDecisionPort;
|
|
8
|
+
probe: () => Promise<boolean>;
|
|
9
|
+
readiness: () => boolean;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { inferenceResultSchema, modelRuntimeStatusSchema, } from "@tomflow/proflow-model-contracts";
|
|
2
|
+
const loopbackHosts = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
3
|
+
const sensitiveKey = /(?:authorization|bearer|api[_-]?key|token|password|cookie|private[_-]?key|secret|body|content|env|value)/i;
|
|
4
|
+
function boundedString(value, max = 600) {
|
|
5
|
+
return value.length <= max ? value : `${value.slice(0, max)}…`;
|
|
6
|
+
}
|
|
7
|
+
function sanitizeUrl(value) {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
url.username = "";
|
|
11
|
+
url.password = "";
|
|
12
|
+
url.search = "";
|
|
13
|
+
url.hash = "";
|
|
14
|
+
return boundedString(url.toString());
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return boundedString(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function sanitizeArgumentString(value) {
|
|
21
|
+
if (/^(?:https?:)?\/\//i.test(value))
|
|
22
|
+
return { value: sanitizeUrl(value), redactNext: false };
|
|
23
|
+
const assignment = value.match(/^([^=:\s]+)([=:])(.*)$/);
|
|
24
|
+
if (assignment) {
|
|
25
|
+
const [, name, separator] = assignment;
|
|
26
|
+
const normalizedName = name?.replace(/^-+/, "") ?? "";
|
|
27
|
+
if (sensitiveKey.test(normalizedName))
|
|
28
|
+
return {
|
|
29
|
+
value: `${name}${separator}[REDACTED]`,
|
|
30
|
+
redactNext: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const normalizedFlag = value.replace(/^-+/, "");
|
|
34
|
+
if (value.startsWith("-") && sensitiveKey.test(normalizedFlag))
|
|
35
|
+
return { value: boundedString(value, 240), redactNext: true };
|
|
36
|
+
if (/^bearer\s+/i.test(value))
|
|
37
|
+
return { value: "Bearer [REDACTED]", redactNext: false };
|
|
38
|
+
return { value: boundedString(value, 240), redactNext: false };
|
|
39
|
+
}
|
|
40
|
+
function sanitizeArgumentArray(raw) {
|
|
41
|
+
let redactNext = false;
|
|
42
|
+
return raw.slice(0, 24).map((item) => {
|
|
43
|
+
if (redactNext) {
|
|
44
|
+
redactNext = false;
|
|
45
|
+
return "[REDACTED]";
|
|
46
|
+
}
|
|
47
|
+
if (item === null || typeof item === "number" || typeof item === "boolean")
|
|
48
|
+
return item;
|
|
49
|
+
if (typeof item !== "string")
|
|
50
|
+
return "[OBJECT]";
|
|
51
|
+
const sanitized = sanitizeArgumentString(item);
|
|
52
|
+
redactNext = sanitized.redactNext;
|
|
53
|
+
return sanitized.value;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function flattenOperation(input, budgetBytes) {
|
|
57
|
+
const output = {};
|
|
58
|
+
let fields = 0;
|
|
59
|
+
let truncated = false;
|
|
60
|
+
const fits = (candidate) => Buffer.byteLength(JSON.stringify(candidate)) <= budgetBytes;
|
|
61
|
+
for (const [key, raw] of Object.entries(input)) {
|
|
62
|
+
if (fields >= 96) {
|
|
63
|
+
truncated = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
let value;
|
|
67
|
+
if (sensitiveKey.test(key))
|
|
68
|
+
value = "[REDACTED]";
|
|
69
|
+
else if (raw === null ||
|
|
70
|
+
typeof raw === "number" ||
|
|
71
|
+
typeof raw === "boolean")
|
|
72
|
+
value = raw;
|
|
73
|
+
else if (typeof raw === "string")
|
|
74
|
+
value = /^(?:https?:)?\/\//i.test(raw)
|
|
75
|
+
? sanitizeUrl(raw)
|
|
76
|
+
: boundedString(raw);
|
|
77
|
+
else if (Array.isArray(raw)) {
|
|
78
|
+
value = sanitizeArgumentArray(raw);
|
|
79
|
+
if (raw.length > 24)
|
|
80
|
+
truncated = true;
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
value = "[OBJECT]";
|
|
84
|
+
const candidate = { ...output, [key]: value };
|
|
85
|
+
if (!fits(candidate)) {
|
|
86
|
+
truncated = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
output[key] = value;
|
|
90
|
+
fields += 1;
|
|
91
|
+
}
|
|
92
|
+
if (truncated && fits({ ...output, _truncated: true }))
|
|
93
|
+
output._truncated = true;
|
|
94
|
+
return output;
|
|
95
|
+
}
|
|
96
|
+
function commandRiskPayload(request, inputFingerprint, budgetBytes = 6_000) {
|
|
97
|
+
return {
|
|
98
|
+
capability: request.capability,
|
|
99
|
+
inputFingerprint,
|
|
100
|
+
callerRef: request.callerRef,
|
|
101
|
+
...(request.taskId ? { taskId: request.taskId } : {}),
|
|
102
|
+
...(request.nodeId ? { nodeId: request.nodeId } : {}),
|
|
103
|
+
...(request.roleRef ? { roleRef: request.roleRef } : {}),
|
|
104
|
+
...(request.workerRef ? { workerRef: request.workerRef } : {}),
|
|
105
|
+
...(request.projectRoot
|
|
106
|
+
? { projectRoot: boundedString(request.projectRoot, 1_000) }
|
|
107
|
+
: {}),
|
|
108
|
+
operation: flattenOperation(request.input, budgetBytes),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function outputData(value) {
|
|
112
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
113
|
+
throw new TypeError("command-risk result data must be an object");
|
|
114
|
+
const record = value;
|
|
115
|
+
if (!new Set(["ALLOW", "DENY", "ESCALATE"]).has(String(record.decision)))
|
|
116
|
+
throw new TypeError("command-risk decision is invalid");
|
|
117
|
+
if (typeof record.rationale !== "string" || record.rationale.length === 0)
|
|
118
|
+
throw new TypeError("command-risk rationale is required");
|
|
119
|
+
return {
|
|
120
|
+
decision: record.decision,
|
|
121
|
+
rationale: record.rationale,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function createExecutionModelDecisionClient(config) {
|
|
125
|
+
const endpoint = new URL(config.endpoint);
|
|
126
|
+
if (endpoint.protocol !== "http:" ||
|
|
127
|
+
!loopbackHosts.has(endpoint.hostname) ||
|
|
128
|
+
endpoint.pathname !== "/" ||
|
|
129
|
+
endpoint.search ||
|
|
130
|
+
endpoint.hash)
|
|
131
|
+
throw new TypeError("modelDecision.endpoint must be loopback HTTP root");
|
|
132
|
+
if (config.credential && config.credential.length < 32)
|
|
133
|
+
throw new TypeError("modelDecision credential must contain at least 32 characters");
|
|
134
|
+
const timeoutMs = config.timeoutMs ?? 12_000;
|
|
135
|
+
const authorizationHeaders = config.credential
|
|
136
|
+
? { authorization: `Bearer ${config.credential}` }
|
|
137
|
+
: {};
|
|
138
|
+
let ready = false;
|
|
139
|
+
const status = async () => {
|
|
140
|
+
const response = await fetch(`${endpoint.origin}/status`, {
|
|
141
|
+
headers: authorizationHeaders,
|
|
142
|
+
signal: AbortSignal.timeout(Math.min(timeoutMs, 3_000)),
|
|
143
|
+
});
|
|
144
|
+
if (!response.ok)
|
|
145
|
+
throw new Error(`MODEL_STATUS_HTTP_${response.status}`);
|
|
146
|
+
return modelRuntimeStatusSchema.parse(await response.json());
|
|
147
|
+
};
|
|
148
|
+
const probe = async () => {
|
|
149
|
+
try {
|
|
150
|
+
const current = await status();
|
|
151
|
+
ready =
|
|
152
|
+
current.runtime !== "UNAVAILABLE" &&
|
|
153
|
+
current.fast === "READY" &&
|
|
154
|
+
current.reason === "READY";
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
ready = false;
|
|
158
|
+
}
|
|
159
|
+
return ready;
|
|
160
|
+
};
|
|
161
|
+
const infer = async (request, context, compact = false) => {
|
|
162
|
+
const payload = commandRiskPayload(request, context?.inputFingerprint ?? "sha256:unknown", compact ? 1_800 : 6_000);
|
|
163
|
+
const response = await fetch(`${endpoint.origin}/infer`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: {
|
|
166
|
+
...authorizationHeaders,
|
|
167
|
+
"content-type": "application/json",
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
contractVersion: "1.0.0",
|
|
171
|
+
specRef: "execution.command-risk.v1",
|
|
172
|
+
mode: "auto",
|
|
173
|
+
priority: "business",
|
|
174
|
+
trace: {
|
|
175
|
+
callerRef: "execution-runtime:model-decision",
|
|
176
|
+
...(request.correlationId
|
|
177
|
+
? { correlationId: request.correlationId }
|
|
178
|
+
: {}),
|
|
179
|
+
...(request.taskId ? { taskId: request.taskId } : {}),
|
|
180
|
+
...(request.nodeId ? { nodeId: request.nodeId } : {}),
|
|
181
|
+
...(context?.executionRef
|
|
182
|
+
? { executionRef: context.executionRef }
|
|
183
|
+
: {}),
|
|
184
|
+
},
|
|
185
|
+
payload,
|
|
186
|
+
timeoutMs,
|
|
187
|
+
}),
|
|
188
|
+
signal: AbortSignal.timeout(timeoutMs + 1_000),
|
|
189
|
+
});
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
ready = false;
|
|
192
|
+
throw new Error(`MODEL_INFER_HTTP_${response.status}`);
|
|
193
|
+
}
|
|
194
|
+
const result = inferenceResultSchema.parse(await response.json());
|
|
195
|
+
if (result.specRef !== "execution.command-risk.v1" ||
|
|
196
|
+
result.requestedMode !== "auto") {
|
|
197
|
+
ready = false;
|
|
198
|
+
throw new Error("MODEL_INFERENCE_PROTOCOL_MISMATCH");
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
};
|
|
202
|
+
const port = {
|
|
203
|
+
async decide(request, context) {
|
|
204
|
+
if (!(await probe()))
|
|
205
|
+
throw new Error("DECISION_UNRESOLVED: model decision roles are unavailable");
|
|
206
|
+
let result;
|
|
207
|
+
try {
|
|
208
|
+
result = await infer(request, context, false);
|
|
209
|
+
if (result.status !== "SUCCEEDED" &&
|
|
210
|
+
result.error?.code === "CONTEXT_TOO_LARGE")
|
|
211
|
+
result = await infer(request, context, true);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
throw new Error(`DECISION_UNRESOLVED: ${error instanceof Error ? error.message : "model inference failed"}`);
|
|
215
|
+
}
|
|
216
|
+
if (result.status !== "SUCCEEDED") {
|
|
217
|
+
if (result.error?.code !== "CONTEXT_TOO_LARGE")
|
|
218
|
+
ready = false;
|
|
219
|
+
throw new Error(`DECISION_UNRESOLVED: ${result.error?.code ?? result.status}`);
|
|
220
|
+
}
|
|
221
|
+
const data = outputData(result.data);
|
|
222
|
+
if (data.decision === "DENY")
|
|
223
|
+
return {
|
|
224
|
+
decision: "DENY",
|
|
225
|
+
decisionPath: result.actualMode ?? "fast",
|
|
226
|
+
reason: data.rationale,
|
|
227
|
+
};
|
|
228
|
+
if (data.decision === "ESCALATE")
|
|
229
|
+
return {
|
|
230
|
+
decision: "ALLOW",
|
|
231
|
+
decisionPath: result.actualMode ?? "reason",
|
|
232
|
+
approvalRequired: true,
|
|
233
|
+
reason: data.rationale,
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
decision: "ALLOW",
|
|
237
|
+
decisionPath: result.actualMode ?? "fast",
|
|
238
|
+
approvalRequired: false,
|
|
239
|
+
reason: data.rationale,
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
return Object.freeze({ port, probe, readiness: () => ready });
|
|
244
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ExecutionRuntimeOptions } from "./index.ts";
|
|
2
|
+
export type ExecutionRuntimeProcessConfig = {
|
|
3
|
+
databasePath: string;
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
artifactRoot: string;
|
|
6
|
+
host: string;
|
|
7
|
+
port: number;
|
|
8
|
+
exactNetworkTargets: string[];
|
|
9
|
+
browserExecutorConfigPath?: string;
|
|
10
|
+
transportCredentialFile?: string;
|
|
11
|
+
identity?: {
|
|
12
|
+
endpoint: string;
|
|
13
|
+
tokenFile: string;
|
|
14
|
+
};
|
|
15
|
+
modelDecision?: {
|
|
16
|
+
endpoint: string;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
credentialFile?: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export type ExecutionRuntimeProcessStatus = {
|
|
22
|
+
process: "STOPPED" | "STARTING" | "RUNNING" | "DRAINING";
|
|
23
|
+
liveness: "UP" | "DOWN";
|
|
24
|
+
readiness: "READY" | "NOT_READY";
|
|
25
|
+
accepting: boolean;
|
|
26
|
+
inFlight: number;
|
|
27
|
+
databasePath: string;
|
|
28
|
+
localExecutor: "READY" | "UNAVAILABLE";
|
|
29
|
+
browserExecutor: "READY" | "UNAVAILABLE";
|
|
30
|
+
identity: "READY" | "UNAVAILABLE";
|
|
31
|
+
transportAuth: "READY" | "UNAVAILABLE";
|
|
32
|
+
modelDecision: "READY" | "UNAVAILABLE";
|
|
33
|
+
};
|
|
34
|
+
export declare function parseExecutionRuntimeProcessConfig(value: unknown): ExecutionRuntimeProcessConfig;
|
|
35
|
+
export declare function loadExecutionRuntimeProcessConfig(path: string): Promise<ExecutionRuntimeProcessConfig>;
|
|
36
|
+
export declare function createExecutionRuntimeProcess(input: {
|
|
37
|
+
config: ExecutionRuntimeProcessConfig;
|
|
38
|
+
browserExecutor?: ExecutionRuntimeOptions["browserExecutor"];
|
|
39
|
+
browserReadiness?: () => boolean;
|
|
40
|
+
carrierSummary?: () => {
|
|
41
|
+
online: boolean;
|
|
42
|
+
extensionInstanceId: string | null;
|
|
43
|
+
queuedCommands: number;
|
|
44
|
+
pendingCommands: number;
|
|
45
|
+
};
|
|
46
|
+
identityReadiness?: () => boolean;
|
|
47
|
+
identity?: ExecutionRuntimeOptions["identity"];
|
|
48
|
+
policy?: ExecutionRuntimeOptions["policy"];
|
|
49
|
+
modelDecision?: ExecutionRuntimeOptions["modelDecision"];
|
|
50
|
+
modelDecisionReadiness?: () => boolean;
|
|
51
|
+
approval?: ExecutionRuntimeOptions["approval"];
|
|
52
|
+
log?: (entry: Record<string, unknown>) => void;
|
|
53
|
+
transportCredential?: string;
|
|
54
|
+
requireModelDecision?: boolean;
|
|
55
|
+
}): Promise<Readonly<{
|
|
56
|
+
start: () => Promise<{
|
|
57
|
+
host: string;
|
|
58
|
+
port: number;
|
|
59
|
+
}>;
|
|
60
|
+
stop: () => Promise<void>;
|
|
61
|
+
restart(): Promise<{
|
|
62
|
+
host: string;
|
|
63
|
+
port: number;
|
|
64
|
+
}>;
|
|
65
|
+
status: () => ExecutionRuntimeProcessStatus;
|
|
66
|
+
stateDirectory: string;
|
|
67
|
+
}>>;
|