@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 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.28";
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.28";
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.28";
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.28";
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.28";
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.28";
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.28";
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.28";
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.28";
119
+ readonly moduleVersion: "0.1.30";
120
120
  } | {
121
121
  contract: "deployment.result.v1";
122
122
  moduleRef: "platform-host";
123
- moduleVersion: "0.1.28";
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.28";
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.28",
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
- // The same durable Worker/Conversation may be reused by multiple Tasks.
25
- // Exact duplicates strengthen the same identity; only disagreement is ambiguous.
26
- match ??= binding;
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] === role.roleRef &&
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 { appendFile, chmod, mkdir, readFile, stat, writeFile, } from "node:fs/promises";
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 route = async (operationId, authenticatedRoleRef, rawInput, context) => {
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 = Object.freeze({
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 = Object.freeze({
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 = Object.freeze({
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 = Object.freeze({
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
- const logPath = join(input.config.stateRoot, "logs", "browser-extension", "events.jsonl");
1761
- await mkdir(dirname(logPath), { recursive: true, mode: 0o700 });
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.28",
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-agent-runtime": "^0.1.17",
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-execution-contracts": "^0.1.11",
31
+ "@tomflow/proflow-agent-runtime": "^0.1.17",
32
32
  "@tomflow/proflow-module-contract": "^0.1.13",
33
- "@tomflow/proflow-execution-browser-extension": "^0.1.58"
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-agent-gateway": "^0.1.18",
37
+ "@tomflow/proflow-execution-runtime": "^0.1.19",
38
38
  "@tomflow/proflow-model-runtime": "^0.1.25",
39
- "@tomflow/proflow-execution-runtime": "^0.1.19"
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": [
@@ -3,7 +3,7 @@
3
3
  "contractVersion": "1.0.0",
4
4
  "moduleRef": "platform-host",
5
5
  "packageName": "@tomflow/proflow-platform-host",
6
- "moduleVersion": "0.1.28",
6
+ "moduleVersion": "0.1.30",
7
7
  "kind": "service",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",