@tomflow/proflow-execution-runtime 0.1.18 → 0.1.20
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/CHANGELOG.md +12 -0
- package/DOCS.md +3 -3
- package/dist/deployment/adapter.d.ts +10 -10
- package/dist/deployment/adapter.js +29 -52
- package/dist/deployment/descriptor.d.ts +4 -1
- package/dist/deployment/descriptor.js +5 -1
- package/dist/src/formal-process.d.ts +5 -0
- package/dist/src/formal-process.js +81 -38
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +160 -17
- package/dist/src/service.d.ts +4 -0
- package/dist/src/service.js +88 -24
- package/package.json +5 -5
- package/proflow.module.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tomflow/proflow-execution-runtime
|
|
2
2
|
|
|
3
|
+
## 0.1.20
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix(execution-runtime): unify formal effect identity
|
|
8
|
+
|
|
9
|
+
## 0.1.19
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Close the Real-3 runtime reconciliation, health, browser effect-result boundary, observer pagination, and standalone execution-runtime configuration gaps for final human acceptance.
|
|
14
|
+
|
|
3
15
|
## 0.1.18
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/DOCS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 模块定位与作用
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
接收内部 durable Execution Intent,选择受控 Local/Browser Executor,管理执行、恢复和 Evidence;GPT-facing Repomix / Local Dev / CodeGraph Direct Tools 不进入该 lifecycle。
|
|
6
6
|
|
|
7
7
|
## 主要能力
|
|
8
8
|
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
## 依赖的 Module、Contract 和外部资源
|
|
19
19
|
|
|
20
|
-
- 依赖 `execution-local
|
|
21
|
-
-
|
|
20
|
+
- 依赖 `execution-browser-executor` 与 `execution-local`,兼容版本均为 `>=1.0.0 <2.0.0`。
|
|
21
|
+
- `execution-browser-executor` 由 execution-browser-extension 发布;Execution Runtime 只消费该 Browser lane client/readiness,不拥有或关闭 Extension bridge listener。
|
|
22
22
|
|
|
23
23
|
## 运行形态与生命周期
|
|
24
24
|
|
|
@@ -6,7 +6,7 @@ export declare const behaviorAdapter: {
|
|
|
6
6
|
ok: true;
|
|
7
7
|
status: "SUCCEEDED";
|
|
8
8
|
moduleRef: "execution-runtime";
|
|
9
|
-
moduleVersion: "0.1.
|
|
9
|
+
moduleVersion: "0.1.20";
|
|
10
10
|
data: {
|
|
11
11
|
endpoint: string;
|
|
12
12
|
transportCredentialFile: string;
|
|
@@ -23,7 +23,7 @@ export declare const behaviorAdapter: {
|
|
|
23
23
|
readonly ok: true;
|
|
24
24
|
readonly status: "SUCCEEDED";
|
|
25
25
|
readonly moduleRef: "execution-runtime";
|
|
26
|
-
readonly moduleVersion: "0.1.
|
|
26
|
+
readonly moduleVersion: "0.1.20";
|
|
27
27
|
};
|
|
28
28
|
observedEffects: ("Persists execution state, output references and evidence in WAL SQLite" | "Runs the single backend Execution Runtime service")[];
|
|
29
29
|
}>;
|
|
@@ -33,7 +33,7 @@ export declare const behaviorAdapter: {
|
|
|
33
33
|
ok: true;
|
|
34
34
|
status: "SUCCEEDED";
|
|
35
35
|
moduleRef: "execution-runtime";
|
|
36
|
-
moduleVersion: "0.1.
|
|
36
|
+
moduleVersion: "0.1.20";
|
|
37
37
|
data: {
|
|
38
38
|
setupStatus: "READY";
|
|
39
39
|
runtimeStatus: "RUNNING" | "STOPPED";
|
|
@@ -54,7 +54,7 @@ export declare const behaviorAdapter: {
|
|
|
54
54
|
ok: true;
|
|
55
55
|
status: "SUCCEEDED";
|
|
56
56
|
moduleRef: "execution-runtime";
|
|
57
|
-
moduleVersion: "0.1.
|
|
57
|
+
moduleVersion: "0.1.20";
|
|
58
58
|
data: {
|
|
59
59
|
endpoint: string;
|
|
60
60
|
transportCredentialFile: string;
|
|
@@ -71,7 +71,7 @@ export declare const behaviorAdapter: {
|
|
|
71
71
|
ok: true;
|
|
72
72
|
status: "SUCCEEDED";
|
|
73
73
|
moduleRef: "execution-runtime";
|
|
74
|
-
moduleVersion: "0.1.
|
|
74
|
+
moduleVersion: "0.1.20";
|
|
75
75
|
data: {
|
|
76
76
|
docs: string;
|
|
77
77
|
};
|
|
@@ -84,7 +84,7 @@ export declare const behaviorAdapter: {
|
|
|
84
84
|
ok: true;
|
|
85
85
|
status: "SUCCEEDED";
|
|
86
86
|
moduleRef: "execution-runtime";
|
|
87
|
-
moduleVersion: "0.1.
|
|
87
|
+
moduleVersion: "0.1.20";
|
|
88
88
|
data: unknown;
|
|
89
89
|
};
|
|
90
90
|
observedEffects: ("Persists execution state, output references and evidence in WAL SQLite" | "Runs the single backend Execution Runtime service")[];
|
|
@@ -92,7 +92,7 @@ export declare const behaviorAdapter: {
|
|
|
92
92
|
result: {
|
|
93
93
|
contract: "deployment.result.v1";
|
|
94
94
|
moduleRef: "execution-runtime";
|
|
95
|
-
moduleVersion: "0.1.
|
|
95
|
+
moduleVersion: "0.1.20";
|
|
96
96
|
ok: false;
|
|
97
97
|
status: "FAILED";
|
|
98
98
|
error: {
|
|
@@ -109,7 +109,7 @@ export declare const behaviorAdapter: {
|
|
|
109
109
|
readonly ok: true;
|
|
110
110
|
readonly status: "SUCCEEDED";
|
|
111
111
|
readonly moduleRef: "execution-runtime";
|
|
112
|
-
readonly moduleVersion: "0.1.
|
|
112
|
+
readonly moduleVersion: "0.1.20";
|
|
113
113
|
};
|
|
114
114
|
observedEffects: ("Persists execution state, output references and evidence in WAL SQLite" | "Runs the single backend Execution Runtime service")[];
|
|
115
115
|
} | {
|
|
@@ -118,11 +118,11 @@ export declare const behaviorAdapter: {
|
|
|
118
118
|
readonly ok: true;
|
|
119
119
|
readonly status: "SUCCEEDED";
|
|
120
120
|
readonly moduleRef: "execution-runtime";
|
|
121
|
-
readonly moduleVersion: "0.1.
|
|
121
|
+
readonly moduleVersion: "0.1.20";
|
|
122
122
|
} | {
|
|
123
123
|
contract: "deployment.result.v1";
|
|
124
124
|
moduleRef: "execution-runtime";
|
|
125
|
-
moduleVersion: "0.1.
|
|
125
|
+
moduleVersion: "0.1.20";
|
|
126
126
|
ok: false;
|
|
127
127
|
status: "FAILED";
|
|
128
128
|
error: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { mkdir
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { deterministicLoopbackPort, ensureModuleSecretFile, moduleWorkspaceStateDirectory, readModuleSharedFacts, writeModuleSharedFacts, } from "@tomflow/proflow-module-contract";
|
|
5
5
|
import { descriptor } from "./descriptor.js";
|
|
@@ -48,45 +48,18 @@ async function readOwnFacts(context) {
|
|
|
48
48
|
}
|
|
49
49
|
: undefined;
|
|
50
50
|
}
|
|
51
|
-
async function
|
|
52
|
-
const host = await readModuleSharedFacts(context, "platform-host");
|
|
53
|
-
const model = await readModuleSharedFacts(context, "model-runtime");
|
|
54
|
-
const identityEndpoint = factString(host, "endpoint");
|
|
55
|
-
const identityTokenFile = factString(host, "identityTokenFile");
|
|
56
|
-
const modelEndpoint = factString(model, "endpoint");
|
|
57
|
-
const modelCredentialFile = factString(model, "transportCredentialFile");
|
|
58
|
-
return identityEndpoint &&
|
|
59
|
-
identityTokenFile &&
|
|
60
|
-
modelEndpoint &&
|
|
61
|
-
modelCredentialFile
|
|
62
|
-
? {
|
|
63
|
-
identityEndpoint,
|
|
64
|
-
identityTokenFile,
|
|
65
|
-
modelEndpoint,
|
|
66
|
-
modelCredentialFile,
|
|
67
|
-
}
|
|
68
|
-
: undefined;
|
|
69
|
-
}
|
|
70
|
-
async function dependencies(context) {
|
|
71
|
-
const base = await baseDependencies(context);
|
|
51
|
+
async function startupDependencies(context) {
|
|
72
52
|
const browser = await readModuleSharedFacts(context, "execution-browser-extension");
|
|
73
53
|
const browserExecutorConfigPath = factString(browser, "browserExecutorConfigPath");
|
|
74
|
-
return
|
|
75
|
-
? {
|
|
76
|
-
...base,
|
|
77
|
-
browserExecutorConfigPath,
|
|
78
|
-
}
|
|
79
|
-
: undefined;
|
|
54
|
+
return browserExecutorConfigPath ? { browserExecutorConfigPath } : undefined;
|
|
80
55
|
}
|
|
81
56
|
async function running(context) {
|
|
82
57
|
const own = await readOwnFacts(context);
|
|
83
58
|
if (!own)
|
|
84
59
|
return false;
|
|
85
60
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
headers: { authorization: `Bearer ${token}` },
|
|
89
|
-
signal: AbortSignal.timeout(500),
|
|
61
|
+
return (await fetch(`${own.endpoint}/health`, {
|
|
62
|
+
signal: AbortSignal.timeout(2_000),
|
|
90
63
|
})).ok;
|
|
91
64
|
}
|
|
92
65
|
catch {
|
|
@@ -95,31 +68,39 @@ async function running(context) {
|
|
|
95
68
|
}
|
|
96
69
|
async function compose(context) {
|
|
97
70
|
const own = await ownFacts(context);
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
71
|
+
const browser = await readModuleSharedFacts(context, "execution-browser-extension");
|
|
72
|
+
const browserExecutorConfigPath = factString(browser, "browserExecutorConfigPath");
|
|
73
|
+
if (!browserExecutorConfigPath)
|
|
74
|
+
throw new Error("Browser Executor shared facts are unavailable");
|
|
101
75
|
const [{ createFormalExecutionRuntimeLifecycle }, { parseExecutionRuntimeProcessConfig },] = await Promise.all([
|
|
102
76
|
import("../src/formal-process.js"),
|
|
103
77
|
import("../src/service.js"),
|
|
104
78
|
]);
|
|
105
79
|
const url = new URL(own.endpoint);
|
|
106
80
|
return createFormalExecutionRuntimeLifecycle({
|
|
81
|
+
resolveDependencies: async () => {
|
|
82
|
+
const host = await readModuleSharedFacts(context, "platform-host");
|
|
83
|
+
const model = await readModuleSharedFacts(context, "model-runtime");
|
|
84
|
+
const endpoint = factString(host, "endpoint"), tokenFile = factString(host, "identityTokenFile"), taskToken = factString(host, "taskApplicationTokenFile");
|
|
85
|
+
const modelEndpoint = factString(model, "endpoint"), credentialFile = factString(model, "transportCredentialFile");
|
|
86
|
+
return {
|
|
87
|
+
...(endpoint && tokenFile ? { identity: { endpoint, tokenFile } } : {}),
|
|
88
|
+
...(endpoint && taskToken
|
|
89
|
+
? { platformHost: { endpoint, tokenFile: taskToken } }
|
|
90
|
+
: {}),
|
|
91
|
+
...(modelEndpoint && credentialFile
|
|
92
|
+
? { modelDecision: { endpoint: modelEndpoint, credentialFile } }
|
|
93
|
+
: {}),
|
|
94
|
+
};
|
|
95
|
+
},
|
|
107
96
|
config: parseExecutionRuntimeProcessConfig({
|
|
108
97
|
host: url.hostname,
|
|
109
98
|
port: Number(url.port),
|
|
110
99
|
databasePath: own.databasePath,
|
|
111
100
|
projectRoot: own.projectRoot,
|
|
112
101
|
artifactRoot: own.artifactRoot,
|
|
113
|
-
browserExecutorConfigPath
|
|
102
|
+
browserExecutorConfigPath,
|
|
114
103
|
transportCredentialFile: own.transportCredentialFile,
|
|
115
|
-
identity: {
|
|
116
|
-
endpoint: deps.identityEndpoint,
|
|
117
|
-
tokenFile: deps.identityTokenFile,
|
|
118
|
-
},
|
|
119
|
-
modelDecision: {
|
|
120
|
-
endpoint: deps.modelEndpoint,
|
|
121
|
-
credentialFile: deps.modelCredentialFile,
|
|
122
|
-
},
|
|
123
104
|
exactNetworkTargets: [],
|
|
124
105
|
}),
|
|
125
106
|
});
|
|
@@ -149,7 +130,7 @@ export const behaviorAdapter = {
|
|
|
149
130
|
};
|
|
150
131
|
},
|
|
151
132
|
status: async (context) => {
|
|
152
|
-
const
|
|
133
|
+
const startup = await startupDependencies(context);
|
|
153
134
|
return {
|
|
154
135
|
result: {
|
|
155
136
|
...base,
|
|
@@ -158,18 +139,14 @@ export const behaviorAdapter = {
|
|
|
158
139
|
runtimeStatus: (await running(context))
|
|
159
140
|
? "RUNNING"
|
|
160
141
|
: "STOPPED",
|
|
161
|
-
...(!
|
|
142
|
+
...(!startup
|
|
162
143
|
? {
|
|
163
144
|
issues: [
|
|
164
145
|
{
|
|
165
146
|
scope: "RUNTIME",
|
|
166
147
|
code: "UPSTREAM_NOT_READY",
|
|
167
|
-
message: "等待
|
|
168
|
-
relatedModuleRefs: [
|
|
169
|
-
"platform-host",
|
|
170
|
-
"model-runtime",
|
|
171
|
-
"execution-browser-extension",
|
|
172
|
-
],
|
|
148
|
+
message: "等待 execution-browser-extension 发布 Browser Executor client 配置",
|
|
149
|
+
relatedModuleRefs: ["execution-browser-extension"],
|
|
173
150
|
nextCommand: "platform setup",
|
|
174
151
|
},
|
|
175
152
|
],
|
|
@@ -3,7 +3,7 @@ export declare const descriptor: {
|
|
|
3
3
|
readonly contractVersion: "1.0.0";
|
|
4
4
|
readonly moduleRef: "execution-runtime";
|
|
5
5
|
readonly packageName: "@tomflow/proflow-execution-runtime";
|
|
6
|
-
readonly moduleVersion: "0.1.
|
|
6
|
+
readonly moduleVersion: "0.1.20";
|
|
7
7
|
readonly kind: "service";
|
|
8
8
|
readonly templateVersion: "1.0.0";
|
|
9
9
|
readonly platformCompatibility: ">=1.0.0 <2.0.0";
|
|
@@ -16,6 +16,9 @@ export declare const descriptor: {
|
|
|
16
16
|
readonly version: "1.0.0";
|
|
17
17
|
}];
|
|
18
18
|
readonly requires: readonly [{
|
|
19
|
+
readonly contractRef: "execution-browser-executor";
|
|
20
|
+
readonly versionRange: ">=1.0.0 <2.0.0";
|
|
21
|
+
}, {
|
|
19
22
|
readonly contractRef: "execution-local";
|
|
20
23
|
readonly versionRange: ">=1.0.0 <2.0.0";
|
|
21
24
|
}];
|
|
@@ -3,7 +3,7 @@ export const descriptor = {
|
|
|
3
3
|
contractVersion: "1.0.0",
|
|
4
4
|
moduleRef: "execution-runtime",
|
|
5
5
|
packageName: "@tomflow/proflow-execution-runtime",
|
|
6
|
-
moduleVersion: "0.1.
|
|
6
|
+
moduleVersion: "0.1.20",
|
|
7
7
|
kind: "service",
|
|
8
8
|
templateVersion: "1.0.0",
|
|
9
9
|
platformCompatibility: ">=1.0.0 <2.0.0",
|
|
@@ -18,6 +18,10 @@ export const descriptor = {
|
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
20
|
requires: [
|
|
21
|
+
{
|
|
22
|
+
contractRef: "execution-browser-executor",
|
|
23
|
+
versionRange: ">=1.0.0 <2.0.0",
|
|
24
|
+
},
|
|
21
25
|
{
|
|
22
26
|
contractRef: "execution-local",
|
|
23
27
|
versionRange: ">=1.0.0 <2.0.0",
|
|
@@ -2,6 +2,11 @@ import { type ExecutionRuntimeProcessConfig } from "./service.ts";
|
|
|
2
2
|
export declare function createFormalExecutionRuntimeLifecycle(input: {
|
|
3
3
|
config: ExecutionRuntimeProcessConfig;
|
|
4
4
|
log?: (entry: Record<string, unknown>) => void;
|
|
5
|
+
resolveDependencies?: () => Promise<{
|
|
6
|
+
identity?: ExecutionRuntimeProcessConfig["identity"];
|
|
7
|
+
modelDecision?: ExecutionRuntimeProcessConfig["modelDecision"];
|
|
8
|
+
platformHost?: ExecutionRuntimeProcessConfig["platformHost"];
|
|
9
|
+
}>;
|
|
5
10
|
}): Readonly<{
|
|
6
11
|
status: () => import("./service.ts").ExecutionRuntimeProcessStatus | {
|
|
7
12
|
readiness: "NOT_READY";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { createBrowserExecutorClientComposition } from "@tomflow/proflow-execution-browser-extension/runtime-composition";
|
|
3
3
|
import { createExecutionBrowserVisionClient } from "./browser-vision-client.js";
|
|
4
4
|
import { createExecutionModelDecisionClient } from "./model-decision-client.js";
|
|
5
5
|
import { createExecutionRuntimeProcess, } from "./service.js";
|
|
@@ -28,7 +28,6 @@ async function createIdentityClient(config) {
|
|
|
28
28
|
}
|
|
29
29
|
return ready;
|
|
30
30
|
};
|
|
31
|
-
await probe();
|
|
32
31
|
return {
|
|
33
32
|
port: {
|
|
34
33
|
async authorize(request) {
|
|
@@ -56,45 +55,81 @@ async function createIdentityClient(config) {
|
|
|
56
55
|
};
|
|
57
56
|
}
|
|
58
57
|
function assertFormalConfig(config) {
|
|
59
|
-
if (!config.identity)
|
|
60
|
-
throw new Error("formal execution-runtime requires identity configuration");
|
|
61
58
|
if (!config.transportCredentialFile)
|
|
62
59
|
throw new Error("formal execution-runtime requires transportCredentialFile");
|
|
63
60
|
if (!config.browserExecutorConfigPath)
|
|
64
61
|
throw new Error("formal execution-runtime requires browserExecutorConfigPath");
|
|
65
|
-
if (!config.modelDecision)
|
|
66
|
-
throw new Error("formal execution-runtime requires modelDecision configuration");
|
|
67
|
-
if (!config.modelDecision.credentialFile)
|
|
68
|
-
throw new Error("formal execution-runtime requires modelDecision.credentialFile");
|
|
69
62
|
return {
|
|
70
63
|
identity: config.identity,
|
|
71
64
|
transportCredentialFile: config.transportCredentialFile,
|
|
72
65
|
browserExecutorConfigPath: config.browserExecutorConfigPath,
|
|
73
|
-
modelDecision: {
|
|
74
|
-
...config.modelDecision,
|
|
75
|
-
credentialFile: config.modelDecision.credentialFile,
|
|
76
|
-
},
|
|
77
66
|
};
|
|
78
67
|
}
|
|
79
68
|
export function createFormalExecutionRuntimeLifecycle(input) {
|
|
80
69
|
const required = assertFormalConfig(input.config);
|
|
70
|
+
if (!input.resolveDependencies && !input.config.platformHost)
|
|
71
|
+
throw new Error("formal execution-runtime requires platformHost or resolveDependencies");
|
|
81
72
|
let current;
|
|
82
73
|
const build = async () => {
|
|
83
|
-
const
|
|
74
|
+
const dependencies = input.resolveDependencies ??
|
|
75
|
+
(async () => ({
|
|
76
|
+
identity: input.config.identity,
|
|
77
|
+
modelDecision: input.config.modelDecision,
|
|
78
|
+
platformHost: input.config.platformHost,
|
|
79
|
+
}));
|
|
80
|
+
let identityReady = false;
|
|
81
|
+
let modelReady = false;
|
|
82
|
+
const resolveIdentity = async () => {
|
|
83
|
+
const config = (await dependencies()).identity;
|
|
84
|
+
if (!config)
|
|
85
|
+
throw new Error("EXECUTION_IDENTITY_UNAVAILABLE");
|
|
86
|
+
return createIdentityClient(config);
|
|
87
|
+
};
|
|
88
|
+
const modelConfig = async () => {
|
|
89
|
+
const config = (await dependencies()).modelDecision;
|
|
90
|
+
if (!config?.credentialFile)
|
|
91
|
+
throw new Error("MODEL_DECISION_UNAVAILABLE");
|
|
92
|
+
return {
|
|
93
|
+
...config,
|
|
94
|
+
credential: await readSecret(config.credentialFile, "model credential"),
|
|
95
|
+
};
|
|
96
|
+
};
|
|
84
97
|
const transportCredential = await readSecret(required.transportCredentialFile, "execution transport credential");
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
const browserComposition = await createBrowserExecutorClientComposition({
|
|
99
|
+
configPath: required.browserExecutorConfigPath,
|
|
100
|
+
platformHost: async () => {
|
|
101
|
+
const config = (await dependencies()).platformHost;
|
|
102
|
+
if (!config)
|
|
103
|
+
throw new Error("PLATFORM_HOST_APPLICATION_UNAVAILABLE");
|
|
104
|
+
return {
|
|
105
|
+
endpoint: config.endpoint,
|
|
106
|
+
token: await readSecret(config.tokenFile, "task application credential"),
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
vision: {
|
|
110
|
+
inspect: async (request) => createExecutionBrowserVisionClient(await modelConfig()).port.inspect(request),
|
|
111
|
+
},
|
|
92
112
|
});
|
|
93
113
|
let dependencyRefresh;
|
|
94
114
|
const refreshDependencies = () => {
|
|
95
115
|
dependencyRefresh ??= Promise.allSettled([
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
resolveIdentity()
|
|
117
|
+
.then((client) => client.probe())
|
|
118
|
+
.then((ready) => {
|
|
119
|
+
identityReady = ready;
|
|
120
|
+
})
|
|
121
|
+
.catch(() => {
|
|
122
|
+
identityReady = false;
|
|
123
|
+
}),
|
|
124
|
+
modelConfig()
|
|
125
|
+
.then((config) => createExecutionModelDecisionClient(config).probe())
|
|
126
|
+
.then((ready) => {
|
|
127
|
+
modelReady = ready;
|
|
128
|
+
})
|
|
129
|
+
.catch(() => {
|
|
130
|
+
modelReady = false;
|
|
131
|
+
}),
|
|
132
|
+
browserComposition.refreshStatus(),
|
|
98
133
|
])
|
|
99
134
|
.then(() => undefined)
|
|
100
135
|
.finally(() => {
|
|
@@ -102,27 +137,35 @@ export function createFormalExecutionRuntimeLifecycle(input) {
|
|
|
102
137
|
});
|
|
103
138
|
return dependencyRefresh;
|
|
104
139
|
};
|
|
105
|
-
await refreshDependencies();
|
|
106
|
-
const browserVisionClient = createExecutionBrowserVisionClient({
|
|
107
|
-
endpoint: required.modelDecision.endpoint,
|
|
108
|
-
...(required.modelDecision.timeoutMs === undefined
|
|
109
|
-
? {}
|
|
110
|
-
: { timeoutMs: required.modelDecision.timeoutMs }),
|
|
111
|
-
credential: modelDecisionCredential,
|
|
112
|
-
});
|
|
113
|
-
const browserComposition = await createBrowserExecutorComposition({
|
|
114
|
-
...(await loadBrowserExecutorCompositionConfig(required.browserExecutorConfigPath)),
|
|
115
|
-
vision: browserVisionClient.port,
|
|
116
|
-
});
|
|
117
140
|
try {
|
|
118
141
|
const service = await createExecutionRuntimeProcess({
|
|
119
142
|
config: input.config,
|
|
120
|
-
identity:
|
|
121
|
-
|
|
143
|
+
identity: {
|
|
144
|
+
authorize: async (request) => {
|
|
145
|
+
try {
|
|
146
|
+
const client = await resolveIdentity();
|
|
147
|
+
const allowed = await client.port.authorize(request);
|
|
148
|
+
identityReady = client.readiness();
|
|
149
|
+
return allowed;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
identityReady = false;
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
identityReadiness: () => identityReady,
|
|
122
158
|
transportCredential,
|
|
123
159
|
requireModelDecision: true,
|
|
124
|
-
modelDecision:
|
|
125
|
-
|
|
160
|
+
modelDecision: {
|
|
161
|
+
decide: async (request, context) => {
|
|
162
|
+
const client = createExecutionModelDecisionClient(await modelConfig());
|
|
163
|
+
await client.probe();
|
|
164
|
+
modelReady = client.readiness();
|
|
165
|
+
return client.port.decide(request, context);
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
modelDecisionReadiness: () => modelReady,
|
|
126
169
|
refreshDependencies,
|
|
127
170
|
browserExecutor: browserComposition.browserExecutor,
|
|
128
171
|
browserReadiness: () => browserComposition.bridgeStatus().online,
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ExecuteCapabilityRequest, type ExecutionArtifactRecord, type ExecutionErrorCode, type ExecutionRecord, type ReadExecutionOutputResponse } from "@tomflow/proflow-execution-contracts";
|
|
1
|
+
import { type ExecuteCapabilityRequest, type ExecutionArtifactRecord, type ExecutionErrorCode, type ExecutionRecord, type MonitorEffectReceipt, type ReadExecutionOutputResponse } from "@tomflow/proflow-execution-contracts";
|
|
2
2
|
import type { ExecutionExecutorPort, ExecutorPrecondition } from "./executor-port.ts";
|
|
3
3
|
export declare const moduleRef: "execution-runtime";
|
|
4
4
|
type MaybePromise<Value> = Value | Promise<Value>;
|
|
@@ -62,6 +62,7 @@ export declare class ExecutionRuntimeError extends Error {
|
|
|
62
62
|
export declare function executionInputFingerprint(input: unknown): string;
|
|
63
63
|
export declare function createExecutionRuntime(options: ExecutionRuntimeOptions): Promise<Readonly<{
|
|
64
64
|
executeCapability: (input: unknown) => Promise<ExecutionRecord>;
|
|
65
|
+
executeEffect: (input: unknown) => Promise<MonitorEffectReceipt>;
|
|
65
66
|
lookupExecutionIntent: (input: unknown) => ExecutionRecord;
|
|
66
67
|
listExecutionObserverSignals: (limit?: number) => {
|
|
67
68
|
signalRef: string;
|
package/dist/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
|
-
import { browserCapabilityIds, EXECUTION_CONTRACT_VERSION, executionErrorCodes, parseCancelExecutionRequest, parseExecuteCapabilityRequest, parseExecutionArtifactRecord, parseExecutionRecord, parseExecutionRef, parseReadExecutionOutputRequest, } from "@tomflow/proflow-execution-contracts";
|
|
5
|
+
import { browserCapabilityIds, EXECUTION_CONTRACT_VERSION, executionErrorCodes, parseCancelExecutionRequest, parseExecuteCapabilityRequest, parseExecutionArtifactRecord, parseExecutionRecord, parseMonitorEffectRequest, parseMonitorEffectReceipt, parseExecutionRef, parseReadExecutionOutputRequest, } from "@tomflow/proflow-execution-contracts";
|
|
6
6
|
import { createExecutionApprovalOwner } from "./approval-owner.js";
|
|
7
7
|
export const moduleRef = "execution-runtime";
|
|
8
8
|
export class ExecutionRuntimeError extends Error {
|
|
@@ -17,6 +17,12 @@ export class ExecutionRuntimeError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
const sensitive = /(?:authorization|bearer|api[_-]?key|token|password|cookie|private[_-]?key|secret)/i;
|
|
19
19
|
const browserIds = new Set(browserCapabilityIds);
|
|
20
|
+
const formalEffectCapabilities = new Set([
|
|
21
|
+
"monitor.chat.create",
|
|
22
|
+
"monitor.chat.submit",
|
|
23
|
+
"browser.action",
|
|
24
|
+
]);
|
|
25
|
+
const isFormalEffectCapability = (capability) => formalEffectCapabilities.has(capability);
|
|
20
26
|
function canonical(value) {
|
|
21
27
|
if (Array.isArray(value))
|
|
22
28
|
return value.map(canonical);
|
|
@@ -39,7 +45,7 @@ function scrubEmbeddedSecrets(value) {
|
|
|
39
45
|
.replace(/((?:authorization|api[_-]?key|token|password|cookie|secret|private[_-]?key)\s*[:=]\s*)([^\s,;"'}]+)/gi, (_match, prefix, secretValue) => typeof prefix === "string" && typeof secretValue === "string"
|
|
40
46
|
? `${prefix}[SECRET_REF:${sha(secretValue)}]`
|
|
41
47
|
: _match)
|
|
42
|
-
.replace(/("(?:password|token|secret|api[_-]?key|authorization|cookie|private[_-]?key)"\s*:\s*")((?:[^"\\]|\\.)*)(")/gi, (_match, prefix, secretValue, suffix) => typeof prefix === "string" &&
|
|
48
|
+
.replace(/("[A-Za-z0-9_-]*(?:password|token|secret|api[_-]?key|authorization|cookie|private[_-]?key)[A-Za-z0-9_-]*"\s*:\s*")((?:[^"\\]|\\.)*)(")/gi, (_match, prefix, secretValue, suffix) => typeof prefix === "string" &&
|
|
43
49
|
typeof secretValue === "string" &&
|
|
44
50
|
typeof suffix === "string"
|
|
45
51
|
? `${prefix}[SECRET_REF:${sha(secretValue)}]${suffix}`
|
|
@@ -62,13 +68,11 @@ function protectedValue(value, key = "") {
|
|
|
62
68
|
continue;
|
|
63
69
|
}
|
|
64
70
|
if (flag[1] !== undefined && flag[1].length > 0) {
|
|
65
|
-
// `--token=value` — keep the flag, hash the value.
|
|
66
71
|
items.push(`${item.slice(0, item.length - flag[1].length)}[SECRET_REF:${sha(flag[1])}]`);
|
|
67
72
|
}
|
|
68
73
|
else {
|
|
69
74
|
items.push(item);
|
|
70
75
|
if (i + 1 < value.length) {
|
|
71
|
-
// `--token value` — the following positional arg is the secret.
|
|
72
76
|
items.push(`[SECRET_REF:${sha(value[i + 1])}]`);
|
|
73
77
|
i++;
|
|
74
78
|
}
|
|
@@ -84,11 +88,20 @@ function protectedValue(value, key = "") {
|
|
|
84
88
|
protectedValue(item, childKey),
|
|
85
89
|
]));
|
|
86
90
|
}
|
|
91
|
+
function stableEffectInput(request) {
|
|
92
|
+
if (request.capability !== "monitor.chat.submit" && request.capability !== "monitor.chat.create")
|
|
93
|
+
return request.input;
|
|
94
|
+
const effect = { ...request.input };
|
|
95
|
+
// Legacy revision fences are admission facts, not physical-effect identity.
|
|
96
|
+
delete effect.expectedRunVersion;
|
|
97
|
+
delete effect.configRevision;
|
|
98
|
+
return effect;
|
|
99
|
+
}
|
|
87
100
|
export function executionInputFingerprint(input) {
|
|
88
101
|
const request = parseExecuteCapabilityRequest(input);
|
|
89
102
|
return sha({
|
|
90
103
|
capability: request.capability,
|
|
91
|
-
input: protectedValue(request
|
|
104
|
+
input: protectedValue(stableEffectInput(request)),
|
|
92
105
|
callerRef: request.callerRef,
|
|
93
106
|
taskId: request.taskId,
|
|
94
107
|
nodeId: request.nodeId,
|
|
@@ -118,6 +131,20 @@ function compatibleDurableInputFingerprint(row, request, currentFingerprint) {
|
|
|
118
131
|
try {
|
|
119
132
|
const durableRequest = parseExecuteCapabilityRequest(JSON.parse(String(row.request_json)));
|
|
120
133
|
const durableRecord = parseExecutionRecord(JSON.parse(String(row.record_json)));
|
|
134
|
+
if ((request.capability === "monitor.chat.submit" || request.capability === "monitor.chat.create") &&
|
|
135
|
+
durableRequest.capability === request.capability &&
|
|
136
|
+
sha({
|
|
137
|
+
capability: durableRequest.capability,
|
|
138
|
+
input: stableEffectInput(durableRequest),
|
|
139
|
+
callerRef: durableRequest.callerRef,
|
|
140
|
+
taskId: durableRequest.taskId,
|
|
141
|
+
nodeId: durableRequest.nodeId,
|
|
142
|
+
runNo: durableRequest.runNo,
|
|
143
|
+
roleRef: durableRequest.roleRef,
|
|
144
|
+
workerRef: durableRequest.workerRef,
|
|
145
|
+
projectRoot: durableRequest.projectRoot,
|
|
146
|
+
}) === currentFingerprint)
|
|
147
|
+
return durableFingerprint;
|
|
121
148
|
if (durableFingerprint === legacyExecutionInputFingerprint(request) &&
|
|
122
149
|
durableRequest.runNo === request.runNo &&
|
|
123
150
|
durableRecord.runNo === request.runNo)
|
|
@@ -153,6 +180,10 @@ function runtimeError(error) {
|
|
|
153
180
|
}
|
|
154
181
|
const defaultPolicy = {
|
|
155
182
|
decide(request) {
|
|
183
|
+
const trustedSystemEffect = request.capability === "product.discussion.bootstrap.deliver" ||
|
|
184
|
+
request.capability === "product.review.deliver" ||
|
|
185
|
+
request.capability === "monitor.chat.submit" ||
|
|
186
|
+
request.capability === "monitor.chat.create" || request.capability === "browser.action";
|
|
156
187
|
const readOnly = request.capability === "file.read" ||
|
|
157
188
|
request.capability === "file.searchText" ||
|
|
158
189
|
request.capability === "artifact.external-file.materialize" ||
|
|
@@ -169,14 +200,12 @@ const defaultPolicy = {
|
|
|
169
200
|
request.capability === "browser.verify" ||
|
|
170
201
|
(request.capability === "network.request" &&
|
|
171
202
|
(request.input.method === "GET" || request.input.method === "HEAD"));
|
|
172
|
-
if (readOnly)
|
|
203
|
+
if (readOnly || trustedSystemEffect)
|
|
173
204
|
return {
|
|
174
205
|
decision: "ALLOW",
|
|
175
206
|
decisionPath: "deterministic",
|
|
176
207
|
approvalRequired: false,
|
|
177
208
|
};
|
|
178
|
-
// Frozen policy: every ordinary real side effect is semantically judged by
|
|
179
|
-
// FAST first. REASON/Human are escalation paths, never a blanket gate.
|
|
180
209
|
return {
|
|
181
210
|
decision: "REVIEW",
|
|
182
211
|
decisionPath: "fast",
|
|
@@ -220,6 +249,9 @@ export async function createExecutionRuntime(options) {
|
|
|
220
249
|
record_json TEXT NOT NULL, precondition_json TEXT, created_at TEXT NOT NULL,
|
|
221
250
|
UNIQUE(caller_ref, capability, idempotency_key)
|
|
222
251
|
);
|
|
252
|
+
CREATE UNIQUE INDEX IF NOT EXISTS formal_effect_identity
|
|
253
|
+
ON executions(idempotency_key)
|
|
254
|
+
WHERE capability IN ('monitor.chat.create', 'monitor.chat.submit', 'browser.action');
|
|
223
255
|
CREATE TABLE IF NOT EXISTS execution_artifacts (
|
|
224
256
|
execution_ref TEXT NOT NULL, artifact_ref TEXT NOT NULL, stream TEXT NOT NULL, path TEXT NOT NULL,
|
|
225
257
|
PRIMARY KEY(execution_ref, artifact_ref), FOREIGN KEY(execution_ref) REFERENCES executions(execution_ref)
|
|
@@ -324,6 +356,12 @@ export async function createExecutionRuntime(options) {
|
|
|
324
356
|
updatedAt: now().toISOString(),
|
|
325
357
|
});
|
|
326
358
|
async function admit(request, inputFingerprint, executionRef) {
|
|
359
|
+
const trustedSystemEffect = request.capability === "product.discussion.bootstrap.deliver" ||
|
|
360
|
+
request.capability === "product.review.deliver" ||
|
|
361
|
+
request.capability === "monitor.chat.submit" ||
|
|
362
|
+
request.capability === "monitor.chat.create" || request.capability === "browser.action";
|
|
363
|
+
if (trustedSystemEffect && !options.identity)
|
|
364
|
+
throw new ExecutionRuntimeError("IDENTITY_INVALID", "trusted system effect requires an authoritative identity port");
|
|
327
365
|
if (options.identity && !(await options.identity.authorize(request)))
|
|
328
366
|
throw new ExecutionRuntimeError("IDENTITY_INVALID", "caller, Task, Agent, workspace, or Browser identity is not authoritative");
|
|
329
367
|
const policy = options.policy ?? defaultPolicy;
|
|
@@ -532,10 +570,96 @@ export async function createExecutionRuntime(options) {
|
|
|
532
570
|
throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "execution intent lookup input does not match durable input");
|
|
533
571
|
return fromRow(row);
|
|
534
572
|
}
|
|
573
|
+
async function reconcileUnknownReplay(request, row, prior) {
|
|
574
|
+
const executor = browserIds.has(request.capability)
|
|
575
|
+
? options.browserExecutor
|
|
576
|
+
: options.localExecutor;
|
|
577
|
+
const preconditionRaw = row.precondition_json;
|
|
578
|
+
if (!executor || !preconditionRaw) {
|
|
579
|
+
await log(prior, "UNKNOWN_REPLAY_UNRESOLVED", prior.error?.code);
|
|
580
|
+
return prior;
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
const reconciliation = await executor.reconcile(request, JSON.parse(String(preconditionRaw)));
|
|
584
|
+
let reconciled;
|
|
585
|
+
if (reconciliation.state === "NOT_APPLIED") {
|
|
586
|
+
const failed = failRecord(prior, new ExecutionRuntimeError("EXECUTION_FAILED", "replay reconciliation verified no effect"));
|
|
587
|
+
reconciled = parseExecutionRecord({
|
|
588
|
+
...failed,
|
|
589
|
+
evidence: reconciliation.evidence,
|
|
590
|
+
evidenceRefs: reconciliation.evidence.map((item) => item.evidenceRef),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
else if (reconciliation.state === "APPLIED" && reconciliation.result) {
|
|
594
|
+
const { error: _previousError, ...recoverable } = prior;
|
|
595
|
+
reconciled = parseExecutionRecord({
|
|
596
|
+
...recoverable,
|
|
597
|
+
status: "SUCCEEDED",
|
|
598
|
+
sideEffectState: "APPLIED",
|
|
599
|
+
retryable: false,
|
|
600
|
+
result: reconciliation.result,
|
|
601
|
+
evidence: reconciliation.evidence,
|
|
602
|
+
evidenceRefs: reconciliation.evidence.map((item) => item.evidenceRef),
|
|
603
|
+
finishedAt: now().toISOString(),
|
|
604
|
+
updatedAt: now().toISOString(),
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
await log(prior, "UNKNOWN_REPLAY_UNRESOLVED", prior.error?.code);
|
|
609
|
+
return prior;
|
|
610
|
+
}
|
|
611
|
+
const updated = database.prepare("UPDATE executions SET record_json=? WHERE execution_ref=? AND record_json=?").run(JSON.stringify(reconciled), prior.executionRef, String(row.record_json));
|
|
612
|
+
if (updated.changes !== 1) {
|
|
613
|
+
const current = getRow(prior.executionRef);
|
|
614
|
+
return current ? fromRow(current) : prior;
|
|
615
|
+
}
|
|
616
|
+
await log(reconciled, "UNKNOWN_REPLAY_RECONCILED", reconciled.error?.code);
|
|
617
|
+
emitObserverSignal(reconciled, "RECOVERY_RESUME");
|
|
618
|
+
return reconciled;
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
await log(prior, "UNKNOWN_REPLAY_UNRESOLVED", prior.error?.code);
|
|
622
|
+
return prior;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async function executeEffect(input) {
|
|
626
|
+
const { effectId, action, request } = parseMonitorEffectRequest(input);
|
|
627
|
+
const existingEffect = database
|
|
628
|
+
.prepare("SELECT * FROM executions WHERE idempotency_key=? AND capability IN ('monitor.chat.create', 'monitor.chat.submit', 'browser.action') ORDER BY created_at, execution_ref LIMIT 1")
|
|
629
|
+
.get(effectId);
|
|
630
|
+
let ownerRequest = request;
|
|
631
|
+
if (existingEffect) {
|
|
632
|
+
if (String(existingEffect.capability) !== action)
|
|
633
|
+
throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "effectId already belongs to another action");
|
|
634
|
+
if (String(existingEffect.caller_ref) !== request.callerRef)
|
|
635
|
+
ownerRequest = parseExecuteCapabilityRequest({
|
|
636
|
+
...request,
|
|
637
|
+
callerRef: String(existingEffect.caller_ref),
|
|
638
|
+
});
|
|
639
|
+
if (compatibleDurableInputFingerprint(existingEffect, ownerRequest, executionInputFingerprint(ownerRequest)) === undefined)
|
|
640
|
+
throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "effectId already belongs to different critical input");
|
|
641
|
+
}
|
|
642
|
+
const record = await executeCapability(ownerRequest);
|
|
643
|
+
const state = record.sideEffectState === "APPLIED"
|
|
644
|
+
? "APPLIED"
|
|
645
|
+
: record.sideEffectState === "NOT_APPLIED"
|
|
646
|
+
? "NOT_APPLIED"
|
|
647
|
+
: "PENDING";
|
|
648
|
+
return parseMonitorEffectReceipt({
|
|
649
|
+
effectId,
|
|
650
|
+
action,
|
|
651
|
+
state,
|
|
652
|
+
executionRef: record.executionRef,
|
|
653
|
+
...(state === "APPLIED" && record.result
|
|
654
|
+
? { result: record.result }
|
|
655
|
+
: {}),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
535
658
|
async function executeCapability(input) {
|
|
536
659
|
if (closed)
|
|
537
660
|
throw new ExecutionRuntimeError("EXECUTOR_UNAVAILABLE", "execution runtime is closed");
|
|
538
661
|
const request = parseExecuteCapabilityRequest(input);
|
|
662
|
+
const monitorEffect = isFormalEffectCapability(request.capability);
|
|
539
663
|
assertPatchArtifactScope(request);
|
|
540
664
|
let inputFingerprint = executionInputFingerprint(request);
|
|
541
665
|
const existing = database
|
|
@@ -548,8 +672,16 @@ export async function createExecutionRuntime(options) {
|
|
|
548
672
|
throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "idempotency key was already used with different critical input");
|
|
549
673
|
inputFingerprint = durableFingerprint;
|
|
550
674
|
const prior = fromRow(existing);
|
|
551
|
-
|
|
552
|
-
|
|
675
|
+
// Existing PENDING and APPLIED are read-only. Only proven absence permits retry.
|
|
676
|
+
if (monitorEffect && prior.sideEffectState !== "NOT_APPLIED")
|
|
677
|
+
return prior;
|
|
678
|
+
if (prior.status === "UNKNOWN" && prior.sideEffectState === "UNKNOWN" &&
|
|
679
|
+
(request.capability === "product.discussion.bootstrap.deliver" ||
|
|
680
|
+
request.capability === "product.review.deliver"))
|
|
681
|
+
return reconcileUnknownReplay(request, existing, prior);
|
|
682
|
+
const safelyResumable = (monitorEffect && prior.sideEffectState === "NOT_APPLIED") ||
|
|
683
|
+
(prior.status === "PENDING" &&
|
|
684
|
+
prior.sideEffectState === "NOT_STARTED") ||
|
|
553
685
|
(prior.status === "FAILED" && prior.sideEffectState === "NOT_APPLIED");
|
|
554
686
|
if (!safelyResumable)
|
|
555
687
|
return prior;
|
|
@@ -561,9 +693,11 @@ export async function createExecutionRuntime(options) {
|
|
|
561
693
|
retryable: false,
|
|
562
694
|
updatedAt: now().toISOString(),
|
|
563
695
|
});
|
|
564
|
-
database
|
|
565
|
-
.prepare("UPDATE executions SET request_json=?, record_json=?, precondition_json=NULL WHERE execution_ref=?")
|
|
566
|
-
.run(JSON.stringify(protectedValue(request)), JSON.stringify(record), record.executionRef);
|
|
696
|
+
const claimed = database
|
|
697
|
+
.prepare("UPDATE executions SET request_json=?, record_json=?, precondition_json=NULL WHERE execution_ref=? AND record_json=?")
|
|
698
|
+
.run(JSON.stringify(protectedValue(request)), JSON.stringify(record), record.executionRef, String(existing.record_json));
|
|
699
|
+
if (claimed.changes === 0)
|
|
700
|
+
return fromRow(getRow(record.executionRef));
|
|
567
701
|
await log(record, "EXECUTION_REDECISION_REQUESTED");
|
|
568
702
|
}
|
|
569
703
|
else {
|
|
@@ -658,9 +792,6 @@ export async function createExecutionRuntime(options) {
|
|
|
658
792
|
throw new ExecutionRuntimeError(timedOut ? "TIMEOUT" : "CANCELLED", timedOut
|
|
659
793
|
? "execution runtime timeout elapsed during admission"
|
|
660
794
|
: "execution was cancelled during admission");
|
|
661
|
-
// CAS: only transition to RUNNING if the record is still PENDING.
|
|
662
|
-
// A concurrent cancel/timeout finalizes PENDING records, so a
|
|
663
|
-
// stale RUNNING write would silently resurrect a cancelled intent.
|
|
664
795
|
const casRow = getRow(record.executionRef);
|
|
665
796
|
if (casRow) {
|
|
666
797
|
const casRecord = fromRow(casRow);
|
|
@@ -812,7 +943,12 @@ export async function createExecutionRuntime(options) {
|
|
|
812
943
|
const row = getRow(record.executionRef);
|
|
813
944
|
if (row)
|
|
814
945
|
record = fromRow(row);
|
|
815
|
-
if (record.sideEffectState === "
|
|
946
|
+
if (monitorEffect && record.sideEffectState === "APPLIED")
|
|
947
|
+
return record;
|
|
948
|
+
if (monitorEffect && record.sideEffectState === "STARTED") {
|
|
949
|
+
record = failRecord(record, failure, "UNKNOWN");
|
|
950
|
+
}
|
|
951
|
+
else if (record.sideEffectState === "STARTED") {
|
|
816
952
|
const executor = browserIds.has(request.capability)
|
|
817
953
|
? options.browserExecutor
|
|
818
954
|
: options.localExecutor;
|
|
@@ -914,6 +1050,10 @@ export async function createExecutionRuntime(options) {
|
|
|
914
1050
|
let record = getExecutionRecord(request.executionRef);
|
|
915
1051
|
if (record.callerRef !== request.callerRef)
|
|
916
1052
|
throw new ExecutionRuntimeError("IDENTITY_INVALID", "only the bound caller may cancel execution");
|
|
1053
|
+
// Admission may still be in flight. Cancellation cannot prove it has stopped,
|
|
1054
|
+
// so it must not expose NOT_APPLIED and let another caller claim this row.
|
|
1055
|
+
if (isFormalEffectCapability(record.capability) && record.status === "PENDING")
|
|
1056
|
+
throw new ExecutionRuntimeError("INVALID_REQUEST", "Monitor PENDING must settle through effect.execute");
|
|
917
1057
|
controllers.get(request.executionRef)?.abort();
|
|
918
1058
|
if (record.status === "PENDING") {
|
|
919
1059
|
record = failRecord(record, new ExecutionRuntimeError("CANCELLED", request.reason));
|
|
@@ -926,6 +1066,8 @@ export async function createExecutionRuntime(options) {
|
|
|
926
1066
|
const rows = database.prepare("SELECT * FROM executions").all();
|
|
927
1067
|
for (const row of rows) {
|
|
928
1068
|
let record = fromRow(row);
|
|
1069
|
+
if (isFormalEffectCapability(record.capability))
|
|
1070
|
+
continue;
|
|
929
1071
|
if (record.status !== "PENDING" &&
|
|
930
1072
|
record.status !== "RUNNING" &&
|
|
931
1073
|
record.status !== "UNKNOWN")
|
|
@@ -986,6 +1128,7 @@ export async function createExecutionRuntime(options) {
|
|
|
986
1128
|
await recoverIncomplete();
|
|
987
1129
|
return Object.freeze({
|
|
988
1130
|
executeCapability,
|
|
1131
|
+
executeEffect,
|
|
989
1132
|
lookupExecutionIntent,
|
|
990
1133
|
listExecutionObserverSignals,
|
|
991
1134
|
acknowledgeExecutionObserverSignal,
|
package/dist/src/service.d.ts
CHANGED
package/dist/src/service.js
CHANGED
|
@@ -2,6 +2,7 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
5
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
6
|
import { artifactMaterializationResponseSchema, EXECUTION_CONTRACT_VERSION, materializeContextPackRequestSchema, materializeExternalFilesRequestSchema, materializeExternalFilesResponseSchema, materializePatchProposalRequestSchema, parseExecutionRef, } from "@tomflow/proflow-execution-contracts";
|
|
6
7
|
import { createLocalExecutorPort } from "./executors/local-adapter.js";
|
|
7
8
|
import { createExecutionRuntime, } from "./index.js";
|
|
@@ -15,6 +16,22 @@ function string(value, name) {
|
|
|
15
16
|
throw new TypeError(`${name} must be a non-empty string`);
|
|
16
17
|
return value;
|
|
17
18
|
}
|
|
19
|
+
function parseLoopbackOwner(value, name) {
|
|
20
|
+
const input = object(value, name);
|
|
21
|
+
const endpoint = new URL(string(input.endpoint, `${name}.endpoint`));
|
|
22
|
+
if (endpoint.protocol !== "http:" ||
|
|
23
|
+
!new Set(["127.0.0.1", "localhost", "::1", "[::1]"]).has(endpoint.hostname) ||
|
|
24
|
+
endpoint.pathname !== "/" ||
|
|
25
|
+
endpoint.username ||
|
|
26
|
+
endpoint.password ||
|
|
27
|
+
endpoint.search ||
|
|
28
|
+
endpoint.hash)
|
|
29
|
+
throw new TypeError(`${name}.endpoint must be loopback HTTP root`);
|
|
30
|
+
const tokenFile = string(input.tokenFile, `${name}.tokenFile`);
|
|
31
|
+
if (!isAbsolute(tokenFile))
|
|
32
|
+
throw new TypeError(`${name}.tokenFile must be absolute`);
|
|
33
|
+
return { endpoint: endpoint.origin, tokenFile };
|
|
34
|
+
}
|
|
18
35
|
export function parseExecutionRuntimeProcessConfig(value) {
|
|
19
36
|
const input = object(value, "execution-runtime config");
|
|
20
37
|
const databasePath = resolve(string(input.databasePath, "databasePath"));
|
|
@@ -63,21 +80,12 @@ export function parseExecutionRuntimeProcessConfig(value) {
|
|
|
63
80
|
...(credentialFile === undefined ? {} : { credentialFile }),
|
|
64
81
|
};
|
|
65
82
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
}
|
|
83
|
+
const identity = input.identity === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: parseLoopbackOwner(input.identity, "identity");
|
|
86
|
+
const platformHost = input.platformHost === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: parseLoopbackOwner(input.platformHost, "platformHost");
|
|
81
89
|
if (!Array.isArray(exactNetworkTargets) ||
|
|
82
90
|
exactNetworkTargets.some((item) => typeof item !== "string"))
|
|
83
91
|
throw new TypeError("exactNetworkTargets must be a string array");
|
|
@@ -91,12 +99,57 @@ export function parseExecutionRuntimeProcessConfig(value) {
|
|
|
91
99
|
...(browserExecutorConfigPath ? { browserExecutorConfigPath } : {}),
|
|
92
100
|
...(transportCredentialFile ? { transportCredentialFile } : {}),
|
|
93
101
|
...(identity ? { identity } : {}),
|
|
102
|
+
...(platformHost ? { platformHost } : {}),
|
|
94
103
|
...(modelDecision ? { modelDecision } : {}),
|
|
95
104
|
};
|
|
96
105
|
}
|
|
97
106
|
export async function loadExecutionRuntimeProcessConfig(path) {
|
|
98
107
|
return parseExecutionRuntimeProcessConfig(JSON.parse(await readFile(resolve(path), "utf8")));
|
|
99
108
|
}
|
|
109
|
+
function createObserverSignalPager(databasePath) {
|
|
110
|
+
const database = new DatabaseSync(databasePath, { readOnly: true });
|
|
111
|
+
let cursor;
|
|
112
|
+
let upperBound;
|
|
113
|
+
const page = (limit, after) => {
|
|
114
|
+
if (!upperBound)
|
|
115
|
+
return [];
|
|
116
|
+
return database.prepare(`SELECT record_json, created_at, signal_ref FROM execution_observer_signals
|
|
117
|
+
WHERE acknowledged_at IS NULL
|
|
118
|
+
AND (created_at, signal_ref) <= (?, ?)
|
|
119
|
+
${after ? "AND (created_at, signal_ref) > (?, ?)" : ""}
|
|
120
|
+
ORDER BY created_at, signal_ref LIMIT ?`).all(upperBound.createdAt, upperBound.signalRef, ...(after ? [after.createdAt, after.signalRef] : []), limit);
|
|
121
|
+
};
|
|
122
|
+
const beginCycle = () => {
|
|
123
|
+
cursor = undefined;
|
|
124
|
+
const last = database.prepare("SELECT created_at, signal_ref FROM execution_observer_signals WHERE acknowledged_at IS NULL ORDER BY created_at DESC, signal_ref DESC LIMIT 1").get();
|
|
125
|
+
upperBound = last ? {
|
|
126
|
+
createdAt: String(last.created_at), signalRef: String(last.signal_ref),
|
|
127
|
+
} : undefined;
|
|
128
|
+
};
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
list(limit = 50) {
|
|
131
|
+
const normalized = Number.isFinite(limit) ? Math.trunc(limit) : 50;
|
|
132
|
+
const bounded = Math.max(1, Math.min(100, normalized));
|
|
133
|
+
if (!upperBound)
|
|
134
|
+
beginCycle();
|
|
135
|
+
let rows = page(bounded, cursor);
|
|
136
|
+
if (rows.length === 0) {
|
|
137
|
+
beginCycle();
|
|
138
|
+
rows = page(bounded);
|
|
139
|
+
}
|
|
140
|
+
const last = rows.at(-1);
|
|
141
|
+
if (last)
|
|
142
|
+
cursor = {
|
|
143
|
+
createdAt: String(last.created_at),
|
|
144
|
+
signalRef: String(last.signal_ref),
|
|
145
|
+
};
|
|
146
|
+
return rows.map((row) => JSON.parse(String(row.record_json)));
|
|
147
|
+
},
|
|
148
|
+
close() {
|
|
149
|
+
database.close();
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
100
153
|
export async function createExecutionRuntimeProcess(input) {
|
|
101
154
|
if (input.transportCredential && input.transportCredential.length < 32)
|
|
102
155
|
throw new TypeError("transport credential must contain at least 32 characters");
|
|
@@ -104,6 +157,7 @@ export async function createExecutionRuntimeProcess(input) {
|
|
|
104
157
|
let accepting = false;
|
|
105
158
|
let server;
|
|
106
159
|
let runtime;
|
|
160
|
+
let observerSignals;
|
|
107
161
|
const active = new Set();
|
|
108
162
|
const log = (event, fields = {}) => input.log?.({
|
|
109
163
|
timestamp: new Date().toISOString(),
|
|
@@ -117,29 +171,29 @@ export async function createExecutionRuntimeProcess(input) {
|
|
|
117
171
|
? "UNAVAILABLE"
|
|
118
172
|
: "READY"
|
|
119
173
|
: "UNAVAILABLE";
|
|
120
|
-
const browserRequirementSatisfied = input.config.browserExecutorConfigPath === undefined ||
|
|
121
|
-
browserExecutor === "READY";
|
|
122
174
|
const identity = input.identity && input.identityReadiness?.() !== false
|
|
123
175
|
? "READY"
|
|
124
176
|
: "UNAVAILABLE";
|
|
125
|
-
const identityRequirementSatisfied = input.config.identity === undefined || identity === "READY";
|
|
126
177
|
const transportAuth = input.transportCredential ? "READY" : "UNAVAILABLE";
|
|
127
178
|
const transportRequirementSatisfied = input.config.transportCredentialFile === undefined ||
|
|
128
179
|
transportAuth === "READY";
|
|
129
180
|
const modelDecision = input.modelDecision && input.modelDecisionReadiness?.() !== false
|
|
130
181
|
? "READY"
|
|
131
182
|
: "UNAVAILABLE";
|
|
132
|
-
const
|
|
183
|
+
const browserRequirementSatisfied = input.browserExecutor === undefined || browserExecutor === "READY";
|
|
184
|
+
const identityRequirementSatisfied = (input.identity === undefined && input.config.identity === undefined) ||
|
|
185
|
+
identity === "READY";
|
|
186
|
+
const modelDecisionRequirementSatisfied = input.requireModelDecision !== true || modelDecision === "READY";
|
|
133
187
|
return {
|
|
134
188
|
process: state,
|
|
135
189
|
liveness: state === "STOPPED" ? "DOWN" : "UP",
|
|
136
190
|
readiness: state === "RUNNING" &&
|
|
137
191
|
accepting &&
|
|
138
192
|
runtime &&
|
|
193
|
+
transportRequirementSatisfied &&
|
|
139
194
|
browserRequirementSatisfied &&
|
|
140
195
|
identityRequirementSatisfied &&
|
|
141
|
-
|
|
142
|
-
modelRequirementSatisfied
|
|
196
|
+
modelDecisionRequirementSatisfied
|
|
143
197
|
? "READY"
|
|
144
198
|
: "NOT_READY",
|
|
145
199
|
accepting,
|
|
@@ -180,6 +234,7 @@ export async function createExecutionRuntimeProcess(input) {
|
|
|
180
234
|
...(input.modelDecision ? { modelDecision: input.modelDecision } : {}),
|
|
181
235
|
...(input.approval ? { approval: input.approval } : {}),
|
|
182
236
|
});
|
|
237
|
+
observerSignals = createObserverSignalPager(input.config.databasePath);
|
|
183
238
|
server = createServer((request, response) => {
|
|
184
239
|
const work = (async () => {
|
|
185
240
|
const url = new URL(request.url ?? "/", "http://execution.local");
|
|
@@ -187,12 +242,12 @@ export async function createExecutionRuntimeProcess(input) {
|
|
|
187
242
|
return respond(response, 200, { status: "UP" });
|
|
188
243
|
if (request.method === "GET" && url.pathname === "/ready") {
|
|
189
244
|
let current = status();
|
|
190
|
-
if (
|
|
245
|
+
if (input.refreshDependencies) {
|
|
191
246
|
try {
|
|
192
247
|
await input.refreshDependencies();
|
|
193
248
|
}
|
|
194
249
|
catch {
|
|
195
|
-
//
|
|
250
|
+
// Owner readiness is reported separately from process readiness.
|
|
196
251
|
}
|
|
197
252
|
current = status();
|
|
198
253
|
}
|
|
@@ -386,6 +441,8 @@ export async function createExecutionRuntimeProcess(input) {
|
|
|
386
441
|
if (request.method === "GET" &&
|
|
387
442
|
url.pathname.startsWith("/approvals/"))
|
|
388
443
|
return respond(response, 200, runtime.getExecutionApproval(decodeURIComponent(url.pathname.slice(11))));
|
|
444
|
+
if (request.method === "POST" && url.pathname === "/effects/execute")
|
|
445
|
+
return respond(response, 200, await runtime.executeEffect(body));
|
|
389
446
|
if (request.method === "POST" && url.pathname === "/executions")
|
|
390
447
|
return respond(response, 200, await runtime.executeCapability(body));
|
|
391
448
|
if (request.method === "POST" &&
|
|
@@ -394,8 +451,11 @@ export async function createExecutionRuntimeProcess(input) {
|
|
|
394
451
|
if (request.method === "POST" &&
|
|
395
452
|
url.pathname === "/observer-signals/list") {
|
|
396
453
|
const value = object(body ?? {}, "observer signal list input");
|
|
454
|
+
const limit = typeof value.limit === "number" ? value.limit : 50;
|
|
397
455
|
return respond(response, 200, {
|
|
398
|
-
signals:
|
|
456
|
+
signals: value.consumer === "task-reconciliation"
|
|
457
|
+
? observerSignals?.list(limit) ?? []
|
|
458
|
+
: runtime.listExecutionObserverSignals(limit),
|
|
399
459
|
});
|
|
400
460
|
}
|
|
401
461
|
if (request.method === "POST" &&
|
|
@@ -449,6 +509,8 @@ export async function createExecutionRuntimeProcess(input) {
|
|
|
449
509
|
catch (error) {
|
|
450
510
|
state = "STOPPED";
|
|
451
511
|
accepting = false;
|
|
512
|
+
observerSignals?.close();
|
|
513
|
+
observerSignals = undefined;
|
|
452
514
|
runtime?.close();
|
|
453
515
|
runtime = undefined;
|
|
454
516
|
throw error;
|
|
@@ -464,6 +526,8 @@ export async function createExecutionRuntimeProcess(input) {
|
|
|
464
526
|
if (running)
|
|
465
527
|
await new Promise((resolveStop, reject) => running.close((error) => (error ? reject(error) : resolveStop())));
|
|
466
528
|
await Promise.allSettled([...active]);
|
|
529
|
+
observerSignals?.close();
|
|
530
|
+
observerSignals = undefined;
|
|
467
531
|
runtime?.close();
|
|
468
532
|
runtime = undefined;
|
|
469
533
|
state = "STOPPED";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomflow/proflow-execution-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"SETUP.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@tomflow/proflow-execution-
|
|
28
|
-
"@tomflow/proflow-execution-
|
|
29
|
-
"@tomflow/proflow-model-contracts": "^0.1.10",
|
|
27
|
+
"@tomflow/proflow-execution-contracts": "^0.1.12",
|
|
28
|
+
"@tomflow/proflow-execution-local": "^0.1.16",
|
|
30
29
|
"@tomflow/proflow-module-contract": "^0.1.13",
|
|
31
|
-
"@tomflow/proflow-
|
|
30
|
+
"@tomflow/proflow-model-contracts": "^0.1.10",
|
|
31
|
+
"@tomflow/proflow-execution-browser-extension": "^0.1.74"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@tomflow/proflow-deployment-conformance": "^0.1.13"
|
package/proflow.module.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"contractVersion": "1.0.0",
|
|
4
4
|
"moduleRef": "execution-runtime",
|
|
5
5
|
"packageName": "@tomflow/proflow-execution-runtime",
|
|
6
|
-
"moduleVersion": "0.1.
|
|
6
|
+
"moduleVersion": "0.1.20",
|
|
7
7
|
"kind": "service",
|
|
8
8
|
"templateVersion": "1.0.0",
|
|
9
9
|
"platformCompatibility": ">=1.0.0 <2.0.0",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
"requires": [
|
|
21
|
+
{
|
|
22
|
+
"contractRef": "execution-browser-executor",
|
|
23
|
+
"versionRange": ">=1.0.0 <2.0.0"
|
|
24
|
+
},
|
|
21
25
|
{
|
|
22
26
|
"contractRef": "execution-local",
|
|
23
27
|
"versionRange": ">=1.0.0 <2.0.0"
|