@tomflow/proflow-platform-host 0.1.28 → 0.1.30
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/dist/deployment/adapter.d.ts +10 -10
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/src/browser-permission-context.js +6 -3
- package/dist/src/browser-permission-policy.js +9 -1
- package/dist/src/index.js +20 -43
- package/dist/src/operation-observability.d.ts +73 -0
- package/dist/src/operation-observability.js +235 -0
- package/package.json +7 -7
- package/proflow.module.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tomflow/proflow-platform-host
|
|
2
2
|
|
|
3
|
+
## 0.1.30
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fail closed on ambiguous Task/Worker Permission identity and keep generic application-boundary observability from fabricating an authoritative side-effect state.
|
|
8
|
+
|
|
9
|
+
## 0.1.29
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Accept real ChatGPT Custom GPT conversation URLs whose GPT path segment includes the roleRef followed by the human-readable slug, while preserving exact Worker, host, query, and hash validation for Browser Permission policy.
|
|
14
|
+
|
|
3
15
|
## 0.1.28
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -6,7 +6,7 @@ export declare const behaviorAdapter: {
|
|
|
6
6
|
ok: true;
|
|
7
7
|
status: "SUCCEEDED";
|
|
8
8
|
moduleRef: "platform-host";
|
|
9
|
-
moduleVersion: "0.1.
|
|
9
|
+
moduleVersion: "0.1.30";
|
|
10
10
|
data: {
|
|
11
11
|
endpoint: string;
|
|
12
12
|
stateRoot: string;
|
|
@@ -24,7 +24,7 @@ export declare const behaviorAdapter: {
|
|
|
24
24
|
readonly ok: true;
|
|
25
25
|
readonly status: "SUCCEEDED";
|
|
26
26
|
readonly moduleRef: "platform-host";
|
|
27
|
-
readonly moduleVersion: "0.1.
|
|
27
|
+
readonly moduleVersion: "0.1.30";
|
|
28
28
|
};
|
|
29
29
|
observedEffects: string[];
|
|
30
30
|
}>;
|
|
@@ -34,7 +34,7 @@ export declare const behaviorAdapter: {
|
|
|
34
34
|
ok: true;
|
|
35
35
|
status: "SUCCEEDED";
|
|
36
36
|
moduleRef: "platform-host";
|
|
37
|
-
moduleVersion: "0.1.
|
|
37
|
+
moduleVersion: "0.1.30";
|
|
38
38
|
data: {
|
|
39
39
|
setupStatus: "READY";
|
|
40
40
|
runtimeStatus: "RUNNING" | "STOPPED";
|
|
@@ -48,7 +48,7 @@ export declare const behaviorAdapter: {
|
|
|
48
48
|
ok: true;
|
|
49
49
|
status: "SUCCEEDED";
|
|
50
50
|
moduleRef: "platform-host";
|
|
51
|
-
moduleVersion: "0.1.
|
|
51
|
+
moduleVersion: "0.1.30";
|
|
52
52
|
data: {
|
|
53
53
|
endpoint: string;
|
|
54
54
|
stateRoot: string;
|
|
@@ -66,7 +66,7 @@ export declare const behaviorAdapter: {
|
|
|
66
66
|
ok: true;
|
|
67
67
|
status: "SUCCEEDED";
|
|
68
68
|
moduleRef: "platform-host";
|
|
69
|
-
moduleVersion: "0.1.
|
|
69
|
+
moduleVersion: "0.1.30";
|
|
70
70
|
data: {
|
|
71
71
|
docs: string;
|
|
72
72
|
};
|
|
@@ -79,7 +79,7 @@ export declare const behaviorAdapter: {
|
|
|
79
79
|
ok: true;
|
|
80
80
|
status: "SUCCEEDED";
|
|
81
81
|
moduleRef: "platform-host";
|
|
82
|
-
moduleVersion: "0.1.
|
|
82
|
+
moduleVersion: "0.1.30";
|
|
83
83
|
data: {
|
|
84
84
|
host: string;
|
|
85
85
|
port: number;
|
|
@@ -90,7 +90,7 @@ export declare const behaviorAdapter: {
|
|
|
90
90
|
result: {
|
|
91
91
|
contract: "deployment.result.v1";
|
|
92
92
|
moduleRef: "platform-host";
|
|
93
|
-
moduleVersion: "0.1.
|
|
93
|
+
moduleVersion: "0.1.30";
|
|
94
94
|
ok: false;
|
|
95
95
|
status: "FAILED";
|
|
96
96
|
error: {
|
|
@@ -107,7 +107,7 @@ export declare const behaviorAdapter: {
|
|
|
107
107
|
readonly ok: true;
|
|
108
108
|
readonly status: "SUCCEEDED";
|
|
109
109
|
readonly moduleRef: "platform-host";
|
|
110
|
-
readonly moduleVersion: "0.1.
|
|
110
|
+
readonly moduleVersion: "0.1.30";
|
|
111
111
|
};
|
|
112
112
|
observedEffects: string[];
|
|
113
113
|
} | {
|
|
@@ -116,11 +116,11 @@ export declare const behaviorAdapter: {
|
|
|
116
116
|
readonly ok: true;
|
|
117
117
|
readonly status: "SUCCEEDED";
|
|
118
118
|
readonly moduleRef: "platform-host";
|
|
119
|
-
readonly moduleVersion: "0.1.
|
|
119
|
+
readonly moduleVersion: "0.1.30";
|
|
120
120
|
} | {
|
|
121
121
|
contract: "deployment.result.v1";
|
|
122
122
|
moduleRef: "platform-host";
|
|
123
|
-
moduleVersion: "0.1.
|
|
123
|
+
moduleVersion: "0.1.30";
|
|
124
124
|
ok: false;
|
|
125
125
|
status: "FAILED";
|
|
126
126
|
error: {
|
|
@@ -3,7 +3,7 @@ export declare const descriptor: {
|
|
|
3
3
|
readonly contractVersion: "1.0.0";
|
|
4
4
|
readonly moduleRef: "platform-host";
|
|
5
5
|
readonly packageName: "@tomflow/proflow-platform-host";
|
|
6
|
-
readonly moduleVersion: "0.1.
|
|
6
|
+
readonly moduleVersion: "0.1.30";
|
|
7
7
|
readonly kind: "service";
|
|
8
8
|
readonly templateVersion: "1.0.0";
|
|
9
9
|
readonly platformCompatibility: ">=1.0.0 <2.0.0";
|
|
@@ -3,7 +3,7 @@ export const descriptor = {
|
|
|
3
3
|
contractVersion: "1.0.0",
|
|
4
4
|
moduleRef: "platform-host",
|
|
5
5
|
packageName: "@tomflow/proflow-platform-host",
|
|
6
|
-
moduleVersion: "0.1.
|
|
6
|
+
moduleVersion: "0.1.30",
|
|
7
7
|
kind: "service",
|
|
8
8
|
templateVersion: "1.0.0",
|
|
9
9
|
platformCompatibility: ">=1.0.0 <2.0.0",
|
|
@@ -21,9 +21,12 @@ export function resolveBrowserPermissionTaskBinding(input) {
|
|
|
21
21
|
binding.workerRef !== input.workerRef ||
|
|
22
22
|
binding.conversationLocator !== input.conversationLocator)
|
|
23
23
|
return null;
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
24
|
+
// Worker/Conversation identity is Task-scoped. A second Task match is
|
|
25
|
+
// ambiguous even when every field is byte-identical; fail closed rather
|
|
26
|
+
// than authorizing a permission against the wrong Task occurrence.
|
|
27
|
+
if (match !== null)
|
|
28
|
+
return null;
|
|
29
|
+
match = binding;
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
return match;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { roleOperations } from "./role-operations.js";
|
|
2
2
|
const loopbackHosts = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
3
|
+
function validRoleConversationSegment(roleRef, segment) {
|
|
4
|
+
if (segment === roleRef)
|
|
5
|
+
return true;
|
|
6
|
+
if (!segment)
|
|
7
|
+
return false;
|
|
8
|
+
const slugPrefix = `${roleRef}-`;
|
|
9
|
+
return segment.startsWith(slugPrefix) && segment.length > slugPrefix.length;
|
|
10
|
+
}
|
|
3
11
|
function validWorkerConversation(role, context) {
|
|
4
12
|
const workerRef = context.workerRef;
|
|
5
13
|
if (!workerRef)
|
|
@@ -15,7 +23,7 @@ function validWorkerConversation(role, context) {
|
|
|
15
23
|
locator.hash === "" &&
|
|
16
24
|
segments.length === 4 &&
|
|
17
25
|
segments[0] === "g" &&
|
|
18
|
-
segments[1]
|
|
26
|
+
validRoleConversationSegment(role.roleRef, segments[1]) &&
|
|
19
27
|
segments[2] === "c" &&
|
|
20
28
|
segments[3] === workerRef);
|
|
21
29
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { browserStructuredLogSchema, createOperationSink, createHostOperationObserver, operationContext, incomingOperationRef } from "./operation-observability.js";
|
|
1
2
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
+
import { chmod, mkdir, readFile, stat, writeFile, } from "node:fs/promises";
|
|
4
5
|
import { createServer } from "node:http";
|
|
5
6
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
7
|
import { createAgentRuntime } from "@tomflow/proflow-agent-runtime";
|
|
@@ -41,32 +42,6 @@ const systemObserverReasonResultSchema = z
|
|
|
41
42
|
rationale: z.string().min(1),
|
|
42
43
|
})
|
|
43
44
|
.strict();
|
|
44
|
-
const browserStructuredLogSchema = z
|
|
45
|
-
.object({
|
|
46
|
-
timestamp: z.string().datetime(),
|
|
47
|
-
level: z.enum(["DEBUG", "INFO", "WARN", "ERROR"]),
|
|
48
|
-
component: z.string().min(1).max(100),
|
|
49
|
-
capability: z.string().min(1).max(160).optional(),
|
|
50
|
-
operation: z.string().min(1).max(160).optional(),
|
|
51
|
-
status: z.string().min(1).max(80).optional(),
|
|
52
|
-
errorCode: z.string().min(1).max(160).optional(),
|
|
53
|
-
correlationId: z.string().min(1).max(240).optional(),
|
|
54
|
-
taskId: z.string().min(1).max(240).optional(),
|
|
55
|
-
nodeId: z.string().min(1).max(240).optional(),
|
|
56
|
-
runNo: z.number().int().nonnegative().optional(),
|
|
57
|
-
agentPackageRef: z.string().min(1).max(240).optional(),
|
|
58
|
-
roleRef: z.string().min(1).max(240).optional(),
|
|
59
|
-
workerRef: z.string().min(1).max(240).optional(),
|
|
60
|
-
executionRef: z.string().min(1).max(240).optional(),
|
|
61
|
-
messageRef: z.string().min(1).max(240).optional(),
|
|
62
|
-
artifactRef: z.string().min(1).max(240).optional(),
|
|
63
|
-
evidenceRef: z.string().min(1).max(240).optional(),
|
|
64
|
-
conversationLocator: z.string().min(1).max(1_000).optional(),
|
|
65
|
-
operationRef: z.string().min(1).max(240).optional(),
|
|
66
|
-
attemptNo: z.number().int().nonnegative().optional(),
|
|
67
|
-
tabId: z.number().int().nonnegative().optional(),
|
|
68
|
-
})
|
|
69
|
-
.strict();
|
|
70
45
|
const taskDocumentFileSchema = z.object({
|
|
71
46
|
taskId: z.string().min(1),
|
|
72
47
|
documentType: z.string().min(1),
|
|
@@ -408,6 +383,8 @@ async function readDeploymentOwnerSummary(stateRoot) {
|
|
|
408
383
|
}
|
|
409
384
|
}
|
|
410
385
|
async function constructGraph(config, executionCredential, modelCredential, resolveConnection, resolveLocalToolConnection) {
|
|
386
|
+
const sink = createOperationSink(config.stateRoot);
|
|
387
|
+
const observer = createHostOperationObserver(entry => sink.write("platform-host", entry));
|
|
411
388
|
const databasePath = join(config.stateRoot, "state", "task.sqlite");
|
|
412
389
|
const migration = applyMigrations({ databasePath, migrations: taskMigrations });
|
|
413
390
|
if (!migration.ok)
|
|
@@ -468,8 +445,8 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
468
445
|
taskStore.close();
|
|
469
446
|
throw error;
|
|
470
447
|
}
|
|
471
|
-
const execution = createOwnerHttpClient("execution", config.executionBaseUrl, executionCredential, resolveConnection);
|
|
472
|
-
const model = createOwnerHttpClient("model", config.modelBaseUrl, modelCredential, resolveConnection);
|
|
448
|
+
const execution = observer.port("execution", createOwnerHttpClient("execution", config.executionBaseUrl, executionCredential, resolveConnection));
|
|
449
|
+
const model = observer.port("model", createOwnerHttpClient("model", config.modelBaseUrl, modelCredential, resolveConnection));
|
|
473
450
|
let reconciliationCoordinator;
|
|
474
451
|
const boundedSystemView = async (view) => {
|
|
475
452
|
if (view === "task") {
|
|
@@ -616,7 +593,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
616
593
|
throw Object.assign(new Error("TASK_WORKER_BINDING_MISMATCH"), { httpStatus: 403 });
|
|
617
594
|
return binding.workerRef;
|
|
618
595
|
};
|
|
619
|
-
const
|
|
596
|
+
const routeImpl = async (operationId, authenticatedRoleRef, rawInput, context) => {
|
|
620
597
|
const role = agent.getRegisteredRole(authenticatedRoleRef);
|
|
621
598
|
if (!roleOperations[role.agentPackageRef]?.has(operationId))
|
|
622
599
|
throw Object.assign(new Error("ROLE_OPERATION_DENIED"), { httpStatus: 403 });
|
|
@@ -642,7 +619,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
642
619
|
if (!connection)
|
|
643
620
|
throw Object.assign(new Error("LOCAL_TOOL_BRIDGE_UNAVAILABLE"), { httpStatus: 503 });
|
|
644
621
|
try {
|
|
645
|
-
return await createLocalToolBridgeHostClient({
|
|
622
|
+
return await observer.run("tool-invocation", toolOperation, () => createLocalToolBridgeHostClient({
|
|
646
623
|
endpoint: connection.endpoint,
|
|
647
624
|
token: connection.credential,
|
|
648
625
|
}).request({
|
|
@@ -652,7 +629,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
652
629
|
operation: toolOperation,
|
|
653
630
|
input: toolInput,
|
|
654
631
|
deadlineAt,
|
|
655
|
-
});
|
|
632
|
+
}));
|
|
656
633
|
}
|
|
657
634
|
catch (error) {
|
|
658
635
|
const code = error instanceof LocalToolBridgeError
|
|
@@ -722,6 +699,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
722
699
|
}
|
|
723
700
|
throw new Error("OPERATION_NOT_ROUTED");
|
|
724
701
|
};
|
|
702
|
+
const route = (...args) => observer.run("action", args[0], () => routeImpl(...args), args[2]);
|
|
725
703
|
const browserOwnerPorts = Object.freeze({
|
|
726
704
|
task: Object.freeze({
|
|
727
705
|
async getWorkerBinding(taskId, roleRef) {
|
|
@@ -966,7 +944,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
966
944
|
return null;
|
|
967
945
|
}
|
|
968
946
|
};
|
|
969
|
-
const roleManagement =
|
|
947
|
+
const roleManagement = observer.port("role", {
|
|
970
948
|
async invoke(operation, rawInput) {
|
|
971
949
|
const value = object(rawInput, "role management input");
|
|
972
950
|
if (operation === "role.register") {
|
|
@@ -1103,7 +1081,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
1103
1081
|
getProjection: (taskId) => taskDriverPorts.getTaskDriveProjection(taskId),
|
|
1104
1082
|
requestWake: requestTaskWake,
|
|
1105
1083
|
});
|
|
1106
|
-
const taskApplication =
|
|
1084
|
+
const taskApplication = observer.port("task", {
|
|
1107
1085
|
async invoke(operation, rawInput) {
|
|
1108
1086
|
const value = object(rawInput, "task application input");
|
|
1109
1087
|
if (operation === "task.create") {
|
|
@@ -1214,7 +1192,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
1214
1192
|
});
|
|
1215
1193
|
return context;
|
|
1216
1194
|
};
|
|
1217
|
-
const approvalApplication =
|
|
1195
|
+
const approvalApplication = observer.port("approval", {
|
|
1218
1196
|
async invoke(operation, rawInput) {
|
|
1219
1197
|
const value = object(rawInput, "approval application input");
|
|
1220
1198
|
if (operation === "approval.list")
|
|
@@ -1258,7 +1236,7 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
1258
1236
|
throw new Error("UNSUPPORTED_APPROVAL_APPLICATION_OPERATION");
|
|
1259
1237
|
},
|
|
1260
1238
|
});
|
|
1261
|
-
const observerApplication =
|
|
1239
|
+
const observerApplication = observer.port("observer", {
|
|
1262
1240
|
async invoke(operation, rawInput) {
|
|
1263
1241
|
const value = object(rawInput, "observer application input");
|
|
1264
1242
|
if (operation === "task.reconcileAll") {
|
|
@@ -1398,8 +1376,9 @@ async function constructGraph(config, executionCredential, modelCredential, reso
|
|
|
1398
1376
|
reconciliationCoordinator.start();
|
|
1399
1377
|
return Object.freeze({
|
|
1400
1378
|
route,
|
|
1379
|
+
operationSink: sink,
|
|
1401
1380
|
browserOwnerPorts,
|
|
1402
|
-
authorizeExecution,
|
|
1381
|
+
authorizeExecution: (...args) => observer.run("execution-identity", "authorize", () => authorizeExecution(...args)),
|
|
1403
1382
|
taskDriverPorts,
|
|
1404
1383
|
agentIdentityPorts,
|
|
1405
1384
|
roleManagement,
|
|
@@ -1684,7 +1663,7 @@ export function createPlatformHost(input) {
|
|
|
1684
1663
|
});
|
|
1685
1664
|
graph = await constructGraph(input.config, executionTransportCredential, modelTransportCredential, input.resolveOwnerConnection, input.resolveLocalToolConnection);
|
|
1686
1665
|
server = createServer((request, response) => {
|
|
1687
|
-
const work = (async () => {
|
|
1666
|
+
const work = operationContext.run({ operationRef: incomingOperationRef(request.headers["x-proflow-operation-ref"]) }, async () => {
|
|
1688
1667
|
const url = new URL(request.url ?? "/", "http://platform-host.local");
|
|
1689
1668
|
if (request.method === "GET" && url.pathname === "/health")
|
|
1690
1669
|
return respond(response, 200, {
|
|
@@ -1757,10 +1736,8 @@ export function createPlatformHost(input) {
|
|
|
1757
1736
|
}
|
|
1758
1737
|
try {
|
|
1759
1738
|
const entry = browserStructuredLogSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
await appendFile(logPath, `${JSON.stringify({ ...entry, receivedAt: new Date().toISOString() })}\n`, { mode: 0o600 });
|
|
1763
|
-
return respond(response, 200, { accepted: true });
|
|
1739
|
+
await graph.operationSink.write("browser-extension", entry);
|
|
1740
|
+
return respond(response, 200, { accepted: true, ...(entry.eventId ? { eventId: entry.eventId } : {}) });
|
|
1764
1741
|
}
|
|
1765
1742
|
catch (error) {
|
|
1766
1743
|
return respond(response, 400, {
|
|
@@ -1885,7 +1862,7 @@ export function createPlatformHost(input) {
|
|
|
1885
1862
|
error: error instanceof Error ? error.message : "INVALID_REQUEST",
|
|
1886
1863
|
});
|
|
1887
1864
|
}
|
|
1888
|
-
})
|
|
1865
|
+
});
|
|
1889
1866
|
active.add(work);
|
|
1890
1867
|
void work.finally(() => active.delete(work));
|
|
1891
1868
|
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export declare const operationContext: AsyncLocalStorage<{
|
|
4
|
+
operationRef: string;
|
|
5
|
+
parentOperationRef?: string;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function incomingOperationRef(value: unknown): string;
|
|
8
|
+
export declare const browserStructuredLogSchema: z.ZodObject<{
|
|
9
|
+
timestamp: z.ZodString;
|
|
10
|
+
contract: z.ZodOptional<z.ZodLiteral<"proflow.operation-event.v1">>;
|
|
11
|
+
eventId: z.ZodOptional<z.ZodString>;
|
|
12
|
+
sequenceNo: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
source: z.ZodOptional<z.ZodLiteral<"browser-extension">>;
|
|
14
|
+
extensionInstanceId: z.ZodOptional<z.ZodString>;
|
|
15
|
+
moduleVersion: z.ZodOptional<z.ZodString>;
|
|
16
|
+
event: z.ZodOptional<z.ZodString>;
|
|
17
|
+
phase: z.ZodOptional<z.ZodString>;
|
|
18
|
+
operationId: z.ZodOptional<z.ZodString>;
|
|
19
|
+
correlationKind: z.ZodOptional<z.ZodEnum<{
|
|
20
|
+
ADJACENT: "ADJACENT";
|
|
21
|
+
EXACT: "EXACT";
|
|
22
|
+
IDENTITY_MATCH: "IDENTITY_MATCH";
|
|
23
|
+
}>>;
|
|
24
|
+
decision: z.ZodOptional<z.ZodString>;
|
|
25
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
26
|
+
sideEffectState: z.ZodOptional<z.ZodEnum<{
|
|
27
|
+
APPLIED: "APPLIED";
|
|
28
|
+
NOT_APPLIED: "NOT_APPLIED";
|
|
29
|
+
NOT_STARTED: "NOT_STARTED";
|
|
30
|
+
STARTED: "STARTED";
|
|
31
|
+
UNKNOWN: "UNKNOWN";
|
|
32
|
+
}>>;
|
|
33
|
+
durationMs: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
contentInstanceId: z.ZodOptional<z.ZodString>;
|
|
35
|
+
browserSessionEpoch: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
level: z.ZodEnum<{
|
|
37
|
+
DEBUG: "DEBUG";
|
|
38
|
+
ERROR: "ERROR";
|
|
39
|
+
INFO: "INFO";
|
|
40
|
+
WARN: "WARN";
|
|
41
|
+
}>;
|
|
42
|
+
component: z.ZodString;
|
|
43
|
+
capability: z.ZodOptional<z.ZodString>;
|
|
44
|
+
operation: z.ZodOptional<z.ZodString>;
|
|
45
|
+
status: z.ZodOptional<z.ZodString>;
|
|
46
|
+
errorCode: z.ZodOptional<z.ZodString>;
|
|
47
|
+
correlationId: z.ZodOptional<z.ZodString>;
|
|
48
|
+
taskId: z.ZodOptional<z.ZodString>;
|
|
49
|
+
nodeId: z.ZodOptional<z.ZodString>;
|
|
50
|
+
runNo: z.ZodOptional<z.ZodNumber>;
|
|
51
|
+
agentPackageRef: z.ZodOptional<z.ZodString>;
|
|
52
|
+
roleRef: z.ZodOptional<z.ZodString>;
|
|
53
|
+
workerRef: z.ZodOptional<z.ZodString>;
|
|
54
|
+
executionRef: z.ZodOptional<z.ZodString>;
|
|
55
|
+
messageRef: z.ZodOptional<z.ZodString>;
|
|
56
|
+
artifactRef: z.ZodOptional<z.ZodString>;
|
|
57
|
+
evidenceRef: z.ZodOptional<z.ZodString>;
|
|
58
|
+
conversationLocator: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
59
|
+
operationRef: z.ZodOptional<z.ZodString>;
|
|
60
|
+
attemptNo: z.ZodOptional<z.ZodNumber>;
|
|
61
|
+
tabId: z.ZodOptional<z.ZodNumber>;
|
|
62
|
+
}, z.core.$strict>;
|
|
63
|
+
/** One Host append owner, bounded queue and two 5 MiB segments per source. */
|
|
64
|
+
export declare function createOperationSink(stateRoot: string): {
|
|
65
|
+
write: (source: "browser-extension" | "platform-host", entry: Record<string, unknown>) => Promise<void>;
|
|
66
|
+
drain: () => Promise<void>;
|
|
67
|
+
};
|
|
68
|
+
export declare function createHostOperationObserver(emit: (entry: Record<string, unknown>) => unknown): {
|
|
69
|
+
run: <T>(surface: string, operationId: string, invoke: () => Promise<T>, axesInput?: unknown) => Promise<T>;
|
|
70
|
+
port: <T extends {
|
|
71
|
+
invoke(operation: string, input: unknown): Promise<unknown>;
|
|
72
|
+
}>(surface: string, target: T) => T;
|
|
73
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { appendFile, mkdir, open, stat, rename, readFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
const safeRef = z.string().regex(/^[A-Za-z0-9_.:@/><-]{1,240}$/);
|
|
7
|
+
function sanitizeUrl(value) {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
11
|
+
return "[REDACTED_INVALID_LOCATOR]";
|
|
12
|
+
url.username = "";
|
|
13
|
+
url.password = "";
|
|
14
|
+
url.search = "";
|
|
15
|
+
url.hash = "";
|
|
16
|
+
return url.toString();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "[REDACTED_INVALID_LOCATOR]";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function retainedTail(path) {
|
|
23
|
+
const file = await open(path, "r");
|
|
24
|
+
try {
|
|
25
|
+
const size = (await file.stat()).size;
|
|
26
|
+
const start = Math.max(0, size - 5 * 1024 * 1024);
|
|
27
|
+
const buffer = Buffer.alloc(size - start);
|
|
28
|
+
await file.read(buffer, 0, buffer.length, start);
|
|
29
|
+
const text = buffer.toString("utf8");
|
|
30
|
+
return start ? text.slice(text.indexOf("\n") + 1) : text;
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await file.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export const operationContext = new AsyncLocalStorage();
|
|
37
|
+
export function incomingOperationRef(value) {
|
|
38
|
+
return typeof value === "string" && /^op:[0-9a-f-]{36}$/.test(value)
|
|
39
|
+
? value
|
|
40
|
+
: `op:${randomUUID()}`;
|
|
41
|
+
}
|
|
42
|
+
export const browserStructuredLogSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
timestamp: z.string().datetime(),
|
|
45
|
+
contract: z.literal("proflow.operation-event.v1").optional(),
|
|
46
|
+
eventId: safeRef.optional(),
|
|
47
|
+
sequenceNo: z.number().int().nonnegative().optional(),
|
|
48
|
+
source: z.literal("browser-extension").optional(),
|
|
49
|
+
extensionInstanceId: safeRef.optional(),
|
|
50
|
+
moduleVersion: safeRef.optional(),
|
|
51
|
+
event: safeRef.optional(),
|
|
52
|
+
phase: safeRef.optional(),
|
|
53
|
+
operationId: safeRef.optional(),
|
|
54
|
+
correlationKind: z.enum(["EXACT", "IDENTITY_MATCH", "ADJACENT"]).optional(),
|
|
55
|
+
decision: safeRef.optional(),
|
|
56
|
+
reason: safeRef.optional(),
|
|
57
|
+
sideEffectState: z
|
|
58
|
+
.enum(["NOT_STARTED", "STARTED", "APPLIED", "NOT_APPLIED", "UNKNOWN"])
|
|
59
|
+
.optional(),
|
|
60
|
+
durationMs: z.number().nonnegative().optional(),
|
|
61
|
+
contentInstanceId: safeRef.optional(),
|
|
62
|
+
browserSessionEpoch: z.number().int().nonnegative().optional(),
|
|
63
|
+
level: z.enum(["DEBUG", "INFO", "WARN", "ERROR"]),
|
|
64
|
+
component: safeRef,
|
|
65
|
+
capability: safeRef.optional(),
|
|
66
|
+
operation: safeRef.optional(),
|
|
67
|
+
status: safeRef.optional(),
|
|
68
|
+
errorCode: safeRef.optional(),
|
|
69
|
+
correlationId: safeRef.optional(),
|
|
70
|
+
taskId: safeRef.optional(),
|
|
71
|
+
nodeId: safeRef.optional(),
|
|
72
|
+
runNo: z.number().int().nonnegative().optional(),
|
|
73
|
+
agentPackageRef: safeRef.optional(),
|
|
74
|
+
roleRef: safeRef.optional(),
|
|
75
|
+
workerRef: safeRef.optional(),
|
|
76
|
+
executionRef: safeRef.optional(),
|
|
77
|
+
messageRef: safeRef.optional(),
|
|
78
|
+
artifactRef: safeRef.optional(),
|
|
79
|
+
evidenceRef: safeRef.optional(),
|
|
80
|
+
conversationLocator: z
|
|
81
|
+
.string()
|
|
82
|
+
.max(1_000)
|
|
83
|
+
.transform(sanitizeUrl)
|
|
84
|
+
.optional(),
|
|
85
|
+
operationRef: safeRef.optional(),
|
|
86
|
+
attemptNo: z.number().int().nonnegative().optional(),
|
|
87
|
+
tabId: z.number().int().nonnegative().optional(),
|
|
88
|
+
})
|
|
89
|
+
.strict();
|
|
90
|
+
/** One Host append owner, bounded queue and two 5 MiB segments per source. */
|
|
91
|
+
export function createOperationSink(stateRoot) {
|
|
92
|
+
let tail = Promise.resolve();
|
|
93
|
+
let pending = 0;
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
let restored = false;
|
|
96
|
+
const write = (source, entry) => {
|
|
97
|
+
if (pending >= 1000)
|
|
98
|
+
return Promise.reject(new Error("LOG_QUEUE_FULL"));
|
|
99
|
+
pending++;
|
|
100
|
+
const run = tail.then(async () => {
|
|
101
|
+
const path = join(stateRoot, "logs", source, "events.jsonl");
|
|
102
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
103
|
+
if (source === "browser-extension" && !restored) {
|
|
104
|
+
for (const file of [path + ".1", path]) {
|
|
105
|
+
try {
|
|
106
|
+
for (const line of (await retainedTail(file)).split("\n")) {
|
|
107
|
+
try {
|
|
108
|
+
const v = JSON.parse(line);
|
|
109
|
+
if (v &&
|
|
110
|
+
typeof v === "object" &&
|
|
111
|
+
typeof Reflect.get(v, "eventId") === "string")
|
|
112
|
+
seen.add(String(Reflect.get(v, "eventId")));
|
|
113
|
+
if (seen.size > 20000)
|
|
114
|
+
seen.delete(seen.values().next().value ?? "");
|
|
115
|
+
}
|
|
116
|
+
catch { }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch { }
|
|
120
|
+
}
|
|
121
|
+
restored = true;
|
|
122
|
+
}
|
|
123
|
+
const eventId = typeof entry.eventId === "string" ? entry.eventId : undefined;
|
|
124
|
+
if (eventId && seen.has(eventId))
|
|
125
|
+
return;
|
|
126
|
+
const info = await stat(path).catch(() => null);
|
|
127
|
+
if (info &&
|
|
128
|
+
(info.size >= 5 * 1024 * 1024 ||
|
|
129
|
+
Date.now() - info.mtimeMs > 7 * 86400_000))
|
|
130
|
+
await rename(path, path + ".1");
|
|
131
|
+
await appendFile(path, JSON.stringify({ ...entry, receivedAt: new Date().toISOString() }) +
|
|
132
|
+
"\n", { mode: 0o600 });
|
|
133
|
+
if (eventId) {
|
|
134
|
+
seen.add(eventId);
|
|
135
|
+
if (seen.size > 20000)
|
|
136
|
+
seen.delete(seen.values().next().value ?? "");
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
tail = run
|
|
140
|
+
.catch(() => undefined)
|
|
141
|
+
.finally(() => {
|
|
142
|
+
pending--;
|
|
143
|
+
});
|
|
144
|
+
return run;
|
|
145
|
+
};
|
|
146
|
+
return { write, drain: () => tail };
|
|
147
|
+
}
|
|
148
|
+
function code(value) {
|
|
149
|
+
return typeof value === "string" && /^[A-Z][A-Z0-9_.:-]{0,159}$/.test(value)
|
|
150
|
+
? value
|
|
151
|
+
: undefined;
|
|
152
|
+
}
|
|
153
|
+
export function createHostOperationObserver(emit) {
|
|
154
|
+
const run = async (surface, operationId, invoke, axesInput) => {
|
|
155
|
+
const parent = operationContext.getStore();
|
|
156
|
+
const operationRef = `op:${randomUUID()}`;
|
|
157
|
+
const started = performance.now();
|
|
158
|
+
const record = (value, error) => {
|
|
159
|
+
const axes = {};
|
|
160
|
+
for (const candidate of [axesInput, value]) {
|
|
161
|
+
if (!candidate || typeof candidate !== "object")
|
|
162
|
+
continue;
|
|
163
|
+
for (const key of ["taskId", "nodeId", "executionRef", "workerRef", "roleRef", "evidenceRef", "artifactRef"]) {
|
|
164
|
+
const v = Reflect.get(candidate, key);
|
|
165
|
+
if (safeRef.safeParse(v).success)
|
|
166
|
+
axes[key] = String(v);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const result = value && typeof value === "object" ? value : {};
|
|
170
|
+
const failed = error !== undefined || Reflect.get(result, "ok") === false;
|
|
171
|
+
const errorValue = error ?? Reflect.get(result, "error");
|
|
172
|
+
const errorCode = errorValue && typeof errorValue === "object"
|
|
173
|
+
? code(Reflect.get(errorValue, "code")) ?? code(Reflect.get(errorValue, "message"))
|
|
174
|
+
: undefined;
|
|
175
|
+
const sideEffectState = Reflect.get(result, "sideEffectState");
|
|
176
|
+
const hasAuthoritativeSideEffectState = typeof sideEffectState === "string" &&
|
|
177
|
+
["NOT_STARTED", "STARTED", "APPLIED", "NOT_APPLIED", "UNKNOWN"].includes(sideEffectState);
|
|
178
|
+
const entry = {
|
|
179
|
+
contract: "proflow.operation-boundary.v1",
|
|
180
|
+
eventId: `host-event:${randomUUID()}`,
|
|
181
|
+
timestamp: new Date().toISOString(),
|
|
182
|
+
source: "platform-host",
|
|
183
|
+
component: "platform-host-application-boundary",
|
|
184
|
+
event: "HOST_APPLICATION",
|
|
185
|
+
phase: surface,
|
|
186
|
+
operationId: safeRef.safeParse(operationId).success
|
|
187
|
+
? operationId
|
|
188
|
+
: "INVALID_OPERATION",
|
|
189
|
+
operationRef,
|
|
190
|
+
...(parent
|
|
191
|
+
? {
|
|
192
|
+
parentOperationRef: parent.operationRef,
|
|
193
|
+
correlationId: parent.parentOperationRef ?? parent.operationRef,
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
196
|
+
correlationKind: "EXACT",
|
|
197
|
+
...axes,
|
|
198
|
+
decision: code(Reflect.get(result, "decision")), reason: code(Reflect.get(result, "reason")),
|
|
199
|
+
status: failed ? "FAILED" : Reflect.get(result, "decision") === "HUMAN_REQUIRED" ? "BLOCKED" : Reflect.get(result, "decision") === "DEFER" ? "DEFERRED" : "SUCCEEDED",
|
|
200
|
+
...(hasAuthoritativeSideEffectState ? { sideEffectState } : {}),
|
|
201
|
+
...(failed
|
|
202
|
+
? { errorCode: errorCode ?? "HOST_APPLICATION_FAILED" }
|
|
203
|
+
: {}),
|
|
204
|
+
durationMs: performance.now() - started,
|
|
205
|
+
};
|
|
206
|
+
try {
|
|
207
|
+
void Promise.resolve(emit(entry)).catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
catch { }
|
|
210
|
+
};
|
|
211
|
+
return operationContext.run({
|
|
212
|
+
operationRef,
|
|
213
|
+
...(parent
|
|
214
|
+
? {
|
|
215
|
+
parentOperationRef: parent.parentOperationRef ?? parent.operationRef,
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
218
|
+
}, async () => {
|
|
219
|
+
try {
|
|
220
|
+
const value = await invoke();
|
|
221
|
+
record(value);
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
record(undefined, error);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
const port = (surface, target) => Object.freeze({
|
|
231
|
+
...target,
|
|
232
|
+
invoke: (operation, input) => run(surface, operation, () => target.invoke(operation, input), input),
|
|
233
|
+
});
|
|
234
|
+
return { run, port };
|
|
235
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomflow/proflow-platform-host",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"zod": "4.1.12",
|
|
27
|
-
"@tomflow/proflow-
|
|
28
|
-
"@tomflow/proflow-task-orchestration": "^0.1.11",
|
|
27
|
+
"@tomflow/proflow-execution-browser-extension": "^0.1.62",
|
|
29
28
|
"@tomflow/proflow-task-migration-runner": "^0.1.11",
|
|
29
|
+
"@tomflow/proflow-task-orchestration": "^0.1.11",
|
|
30
30
|
"@tomflow/proflow-task-store-sqlite": "^0.1.12",
|
|
31
|
-
"@tomflow/proflow-
|
|
31
|
+
"@tomflow/proflow-agent-runtime": "^0.1.17",
|
|
32
32
|
"@tomflow/proflow-module-contract": "^0.1.13",
|
|
33
|
-
"@tomflow/proflow-execution-
|
|
33
|
+
"@tomflow/proflow-execution-contracts": "^0.1.11"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@tomflow/proflow-deployment-conformance": "^0.1.13",
|
|
37
|
-
"@tomflow/proflow-
|
|
37
|
+
"@tomflow/proflow-execution-runtime": "^0.1.19",
|
|
38
38
|
"@tomflow/proflow-model-runtime": "^0.1.25",
|
|
39
|
-
"@tomflow/proflow-
|
|
39
|
+
"@tomflow/proflow-agent-gateway": "^0.1.18"
|
|
40
40
|
},
|
|
41
41
|
"description": "Provides the ProFlow local application composition root that binds Task, Agent, Execution and Model owner transports.",
|
|
42
42
|
"keywords": [
|
package/proflow.module.json
CHANGED