@tomflow/proflow-execution-browser-extension 0.1.39 → 0.1.41

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 ADDED
@@ -0,0 +1,13 @@
1
+ # @tomflow/proflow-execution-browser-extension
2
+
3
+ ## 0.1.41
4
+
5
+ ### Patch Changes
6
+
7
+ - Resume the existing Task Observer after successful Tasks web mutations so READY nodes receive their normal NODE_READY or REOPEN wake without manual recovery.
8
+
9
+ ## 0.1.40
10
+
11
+ ### Patch Changes
12
+
13
+ - Make the authenticated ProFlow Tasks web app self-contained so its published browser module no longer escapes the `/tasks` session surface through a runtime relative import.
@@ -47,7 +47,7 @@ export declare const behaviorAdapter: {
47
47
  ok: true;
48
48
  status: "SUCCEEDED";
49
49
  moduleRef: "execution-browser-extension";
50
- moduleVersion: "0.1.39";
50
+ moduleVersion: "0.1.41";
51
51
  data: {
52
52
  loadDir: string;
53
53
  };
@@ -60,7 +60,7 @@ export declare const behaviorAdapter: {
60
60
  readonly ok: true;
61
61
  readonly status: "SUCCEEDED";
62
62
  readonly moduleRef: "execution-browser-extension";
63
- readonly moduleVersion: "0.1.39";
63
+ readonly moduleVersion: "0.1.41";
64
64
  };
65
65
  observedEffects: never[];
66
66
  }>;
@@ -70,7 +70,7 @@ export declare const behaviorAdapter: {
70
70
  ok: true;
71
71
  status: "SUCCEEDED";
72
72
  moduleRef: "execution-browser-extension";
73
- moduleVersion: "0.1.39";
73
+ moduleVersion: "0.1.41";
74
74
  data: {
75
75
  setupStatus: "ACTION_REQUIRED" | "READY";
76
76
  runtimeStatus: "NOT_APPLICABLE";
@@ -95,7 +95,7 @@ export declare const behaviorAdapter: {
95
95
  result: {
96
96
  contract: "deployment.result.v1";
97
97
  moduleRef: "execution-browser-extension";
98
- moduleVersion: "0.1.39";
98
+ moduleVersion: "0.1.41";
99
99
  ok: false;
100
100
  status: "ACTION_REQUIRED";
101
101
  data: {
@@ -127,14 +127,14 @@ export declare const behaviorAdapter: {
127
127
  readonly ok: true;
128
128
  readonly status: "SUCCEEDED";
129
129
  readonly moduleRef: "execution-browser-extension";
130
- readonly moduleVersion: "0.1.39";
130
+ readonly moduleVersion: "0.1.41";
131
131
  };
132
132
  observedEffects: string[];
133
133
  } | {
134
134
  result: {
135
135
  contract: "deployment.result.v1";
136
136
  moduleRef: "execution-browser-extension";
137
- moduleVersion: "0.1.39";
137
+ moduleVersion: "0.1.41";
138
138
  ok: false;
139
139
  status: "FAILED";
140
140
  error: {
@@ -167,7 +167,7 @@ export declare const behaviorAdapter: {
167
167
  ok: true;
168
168
  status: "SUCCEEDED";
169
169
  moduleRef: "execution-browser-extension";
170
- moduleVersion: "0.1.39";
170
+ moduleVersion: "0.1.41";
171
171
  data: {
172
172
  docs: string;
173
173
  };
@@ -180,11 +180,11 @@ export declare const behaviorAdapter: {
180
180
  readonly ok: true;
181
181
  readonly status: "SUCCEEDED";
182
182
  readonly moduleRef: "execution-browser-extension";
183
- readonly moduleVersion: "0.1.39";
183
+ readonly moduleVersion: "0.1.41";
184
184
  } | {
185
185
  contract: "deployment.result.v1";
186
186
  moduleRef: "execution-browser-extension";
187
- moduleVersion: "0.1.39";
187
+ moduleVersion: "0.1.41";
188
188
  ok: false;
189
189
  status: "FAILED";
190
190
  error: {
@@ -201,7 +201,7 @@ export declare const behaviorAdapter: {
201
201
  readonly ok: true;
202
202
  readonly status: "SUCCEEDED";
203
203
  readonly moduleRef: "execution-browser-extension";
204
- readonly moduleVersion: "0.1.39";
204
+ readonly moduleVersion: "0.1.41";
205
205
  };
206
206
  observedEffects: never[];
207
207
  }>;
@@ -3,7 +3,7 @@ export declare const descriptor: {
3
3
  readonly contractVersion: "1.0.0";
4
4
  readonly moduleRef: "execution-browser-extension";
5
5
  readonly packageName: "@tomflow/proflow-execution-browser-extension";
6
- readonly moduleVersion: "0.1.39";
6
+ readonly moduleVersion: "0.1.41";
7
7
  readonly kind: "browser-extension";
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: "execution-browser-extension",
5
5
  packageName: "@tomflow/proflow-execution-browser-extension",
6
- moduleVersion: "0.1.39",
6
+ moduleVersion: "0.1.41",
7
7
  kind: "browser-extension",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
@@ -132,6 +132,63 @@ function createCarrierContinuationControl(initial) {
132
132
  });
133
133
  }
134
134
 
135
+ // packages/execution-browser-extension/src/carrier-identity.ts
136
+ function canonicalRoleRef(segment) {
137
+ if (!/^g-[A-Za-z0-9_-]+$/.test(segment)) return null;
138
+ return /^(g-[A-Za-z0-9_-]{32})(?:-.+)?$/.exec(segment)?.[1] ?? segment;
139
+ }
140
+ function parseChatGptCarrierIdentity(raw) {
141
+ let url2;
142
+ try {
143
+ url2 = new URL(raw);
144
+ } catch {
145
+ return null;
146
+ }
147
+ if (url2.protocol !== "https:" || url2.hostname !== "chatgpt.com") return null;
148
+ const segments = url2.pathname.split("/").filter(Boolean);
149
+ if (segments[0] !== "g" || !segments[1]) return null;
150
+ const roleRef = canonicalRoleRef(segments[1]);
151
+ if (!roleRef) return null;
152
+ if (segments.length === 2) return { roleRef, workerRef: null };
153
+ if (segments.length !== 4 || segments[2] !== "c" || !segments[3] || segments[3].length > 512 || !/^[A-Za-z0-9_-]+$/.test(segments[3]))
154
+ return null;
155
+ return { roleRef, workerRef: segments[3] };
156
+ }
157
+ function sameChatGptCarrierTarget(left, right) {
158
+ const leftIdentity = parseChatGptCarrierIdentity(left);
159
+ const rightIdentity = parseChatGptCarrierIdentity(right);
160
+ if (leftIdentity && rightIdentity)
161
+ return leftIdentity.roleRef === rightIdentity.roleRef && leftIdentity.workerRef === rightIdentity.workerRef;
162
+ try {
163
+ return new URL(left).href === new URL(right).href;
164
+ } catch {
165
+ return false;
166
+ }
167
+ }
168
+ function createBrowserOpenObservationGate() {
169
+ let receiptGeneration = 0;
170
+ const receipts = /* @__PURE__ */ new Map();
171
+ return Object.freeze({
172
+ beginOpen(requestedUrl) {
173
+ return Object.freeze({ requestedUrl, receiptGeneration });
174
+ },
175
+ recordReceipt(observation) {
176
+ receiptGeneration += 1;
177
+ receipts.set(
178
+ observation.tabId,
179
+ Object.freeze({
180
+ url: observation.url,
181
+ receiptGeneration
182
+ })
183
+ );
184
+ },
185
+ accepts(tabId, boundary) {
186
+ const receipt = receipts.get(tabId);
187
+ return receipt !== void 0 && receipt.receiptGeneration > boundary.receiptGeneration && sameChatGptCarrierTarget(boundary.requestedUrl, receipt.url);
188
+ }
189
+ });
190
+ }
191
+
135
192
  // packages/execution-browser-extension/src/carrier-permission-attempt.ts
136
193
  function parsedAttempts(value) {
137
194
  const result = /* @__PURE__ */ new Map();
@@ -14118,6 +14175,7 @@ function createTaskObserver(options) {
14118
14175
  // packages/execution-browser-extension/extension/background.ts
14119
14176
  var extensionInstanceId = `extension:${crypto.randomUUID()}`;
14120
14177
  var sessions = /* @__PURE__ */ new Map();
14178
+ var browserOpenObservationGate = createBrowserOpenObservationGate();
14121
14179
  var permissionHandling = /* @__PURE__ */ new Map();
14122
14180
  var permissionAutoAttempts = createCarrierPermissionAttemptRegistry();
14123
14181
  var carrierAttentions = createCarrierAttentionRegistry();
@@ -14595,27 +14653,13 @@ async function contentCommand(tabId, command) {
14595
14653
  }
14596
14654
  return response.value;
14597
14655
  }
14598
- function carrierIdentity(url2) {
14599
- try {
14600
- const parsed = new URL(url2);
14601
- const segments = parsed.pathname.split("/").filter(Boolean);
14602
- if (parsed.protocol !== "https:" || parsed.hostname !== "chatgpt.com" || segments[0] !== "g" || !segments[1]?.startsWith("g-"))
14603
- return null;
14604
- return {
14605
- roleRef: segments[1],
14606
- workerRef: segments[2] === "c" && segments[3] ? segments[3] : null
14607
- };
14608
- } catch {
14609
- return null;
14610
- }
14611
- }
14612
14656
  function permissionAttemptKey(observed) {
14613
14657
  return observed.blockerFacts ? `${observed.url}:${observed.blockerFacts.fingerprint}` : null;
14614
14658
  }
14615
14659
  function setCarrierAttention(observed, reason) {
14616
14660
  const facts = observed.blockerFacts;
14617
14661
  if (!facts) return;
14618
- const identity = carrierIdentity(observed.url);
14662
+ const identity = parseChatGptCarrierIdentity(observed.url);
14619
14663
  carrierAttentions.derive({
14620
14664
  tabId: observed.tabId,
14621
14665
  contentInstanceId: observed.contentInstanceId,
@@ -14737,7 +14781,7 @@ async function handleActionPermission(observed) {
14737
14781
  return;
14738
14782
  permissionHandling.set(observed.tabId, key);
14739
14783
  try {
14740
- const identity = carrierIdentity(observed.url);
14784
+ const identity = parseChatGptCarrierIdentity(observed.url);
14741
14785
  const humanDenied = () => carrierContinuationControl.hasMatchingPermissionDenial({
14742
14786
  tabId: observed.tabId,
14743
14787
  contentInstanceId: observed.contentInstanceId,
@@ -14829,6 +14873,7 @@ function handleCarrierBlocker(observed) {
14829
14873
  function processContentObservation(observed, triggerRecovery) {
14830
14874
  const previous = sessions.get(observed.tabId);
14831
14875
  sessions.set(observed.tabId, observed);
14876
+ browserOpenObservationGate.recordReceipt(observed);
14832
14877
  permissionAutoAttempts.observe(
14833
14878
  observed.tabId,
14834
14879
  permissionAttemptKey(observed)
@@ -14954,6 +14999,10 @@ function text(value, name) {
14954
14999
  return value;
14955
15000
  }
14956
15001
  async function executeCommand(command) {
15002
+ if (command.type === "TASK_OBSERVER_RECOVER") {
15003
+ void runObserverRecovery();
15004
+ return { scheduled: true };
15005
+ }
14957
15006
  if (command.type === "CARRIER_ATTENTION_ACTION") {
14958
15007
  if (command.action !== "allowOnce" && command.action !== "deny")
14959
15008
  throw new Error("ATTENTION_ACTION_INVALID");
@@ -14971,8 +15020,13 @@ async function executeCommand(command) {
14971
15020
  const parsed = new URL(url2);
14972
15021
  if (parsed.protocol !== "https:" || parsed.hostname !== "chatgpt.com")
14973
15022
  throw new Error("URL_SCOPE_DENIED");
15023
+ const boundary = browserOpenObservationGate.beginOpen(url2);
14974
15024
  const tab = await chrome.tabs.create({ url: url2, active: true });
14975
- return waitForObservation(numeric(tab.id, "TAB_ID"));
15025
+ const tabId2 = numeric(tab.id, "TAB_ID");
15026
+ return waitForObservation(
15027
+ tabId2,
15028
+ () => browserOpenObservationGate.accepts(tabId2, boundary)
15029
+ );
14976
15030
  }
14977
15031
  const tabId = numeric(command.tabId, "TAB_ID");
14978
15032
  if (command.type === "OBSERVE")
@@ -1,4 +1,60 @@
1
- import { parseCarrierAttentionViews } from "../src/carrier-attention-view.js";
1
+ // The loopback Tasks surface publishes this entry as one authenticated
2
+ // `/tasks/app.js` module. A runtime relative import would escape `/tasks`, so keep
3
+ // the bounded parser local while sharing only the erased TypeScript type above.
4
+ function nullableString(value) {
5
+ return value === null || typeof value === "string";
6
+ }
7
+ function parseCarrierAttentionView(value) {
8
+ if (typeof value !== "object" || value === null || Array.isArray(value))
9
+ return null;
10
+ const attentionRef = Reflect.get(value, "attentionRef");
11
+ const occurrenceRef = Reflect.get(value, "occurrenceRef");
12
+ const taskId = Reflect.get(value, "taskId");
13
+ const roleRef = Reflect.get(value, "roleRef");
14
+ const workerRef = Reflect.get(value, "workerRef");
15
+ const targetHost = Reflect.get(value, "targetHost");
16
+ const operationId = Reflect.get(value, "operationId");
17
+ const reason = Reflect.get(value, "reason");
18
+ const actions = Reflect.get(value, "actions");
19
+ const observedAt = Reflect.get(value, "observedAt");
20
+ if (typeof attentionRef !== "string" ||
21
+ attentionRef.length === 0 ||
22
+ typeof occurrenceRef !== "string" ||
23
+ occurrenceRef.length === 0 ||
24
+ !nullableString(taskId) ||
25
+ !nullableString(roleRef) ||
26
+ !nullableString(workerRef) ||
27
+ !nullableString(targetHost) ||
28
+ typeof operationId !== "string" ||
29
+ operationId.length === 0 ||
30
+ typeof reason !== "string" ||
31
+ reason.length === 0 ||
32
+ !Array.isArray(actions) ||
33
+ actions.some((action) => action !== "allowOnce" && action !== "deny") ||
34
+ typeof observedAt !== "string" ||
35
+ observedAt.length === 0)
36
+ return null;
37
+ return {
38
+ attentionRef,
39
+ occurrenceRef,
40
+ taskId,
41
+ roleRef,
42
+ workerRef,
43
+ targetHost,
44
+ operationId,
45
+ reason,
46
+ actions: [...actions],
47
+ observedAt,
48
+ };
49
+ }
50
+ function parseCarrierAttentionViews(value) {
51
+ if (!Array.isArray(value))
52
+ return [];
53
+ return value
54
+ .slice(0, 128)
55
+ .map(parseCarrierAttentionView)
56
+ .filter((item) => item !== null);
57
+ }
2
58
  const extensionRuntime = typeof chrome === "undefined" ? null : chrome.runtime;
3
59
  function element(selector) {
4
60
  const value = document.querySelector(selector);
@@ -356,3 +412,4 @@ void run(refreshBrowserStatus);
356
412
  setInterval(() => {
357
413
  void run(refreshBrowserStatus);
358
414
  }, 5_000);
415
+ export {};
@@ -1,6 +1,12 @@
1
1
  import { randomUUID, timingSafeEqual } from "node:crypto";
2
2
  import { createServer, } from "node:http";
3
3
  import { parseCarrierAttentionViews, } from "./carrier-attention-view.js";
4
+ const taskWebObserverRecoveryOperations = new Set([
5
+ "task.create",
6
+ "task.start",
7
+ "task.ensureWorkers",
8
+ "node.reopen",
9
+ ]);
4
10
  export class BrowserRealityBridgeError extends Error {
5
11
  code;
6
12
  constructor(code, message) {
@@ -257,9 +263,18 @@ export async function createBrowserRealityBridgeServer(options) {
257
263
  typeof body.operation !== "string" ||
258
264
  !isRecord(body.input))
259
265
  throw new BrowserRealityBridgeError("BRIDGE_INPUT_INVALID", "task web request is invalid");
260
- const value = url.pathname.endsWith("/task")
266
+ const taskRequest = url.pathname.endsWith("/task");
267
+ const value = taskRequest
261
268
  ? await options.taskWeb.invokeTask(body.operation, body.input)
262
269
  : await options.taskWeb.invokeApproval(body.operation, body.input);
270
+ if (taskRequest &&
271
+ taskWebObserverRecoveryOperations.has(body.operation) &&
272
+ commandConsumerReady()) {
273
+ // The Task mutation is already durable and must not be coupled to
274
+ // transient Browser delivery. Notify the Extension asynchronously so
275
+ // its existing Task Observer can derive the next NODE_READY/REOPEN wake.
276
+ void requestCommand({ type: "TASK_OBSERVER_RECOVER" }).catch(() => undefined);
277
+ }
263
278
  send(response, 200, { ok: true, value });
264
279
  return;
265
280
  }
@@ -0,0 +1,21 @@
1
+ export type ChatGptCarrierIdentity = {
2
+ roleRef: string;
3
+ workerRef: string | null;
4
+ };
5
+ export declare function parseChatGptCarrierIdentity(raw: string): ChatGptCarrierIdentity | null;
6
+ export declare function sameChatGptCarrierTarget(left: string, right: string): boolean;
7
+ type BrowserOpenBoundary = Readonly<{
8
+ requestedUrl: string;
9
+ receiptGeneration: number;
10
+ }>;
11
+ type BrowserObservationReceipt = Readonly<{
12
+ tabId: number;
13
+ url: string;
14
+ observedAt: string;
15
+ }>;
16
+ export declare function createBrowserOpenObservationGate(): Readonly<{
17
+ beginOpen(requestedUrl: string): BrowserOpenBoundary;
18
+ recordReceipt(observation: BrowserObservationReceipt): void;
19
+ accepts(tabId: number, boundary: BrowserOpenBoundary): boolean;
20
+ }>;
21
+ export {};
@@ -0,0 +1,68 @@
1
+ function canonicalRoleRef(segment) {
2
+ if (!/^g-[A-Za-z0-9_-]+$/.test(segment))
3
+ return null;
4
+ // ChatGPT may decorate a real 32-character GPT id with a readable slug.
5
+ // Preserve short synthetic refs used by tests while normalizing real URLs.
6
+ return /^(g-[A-Za-z0-9_-]{32})(?:-.+)?$/.exec(segment)?.[1] ?? segment;
7
+ }
8
+ export function parseChatGptCarrierIdentity(raw) {
9
+ let url;
10
+ try {
11
+ url = new URL(raw);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ if (url.protocol !== "https:" || url.hostname !== "chatgpt.com")
17
+ return null;
18
+ const segments = url.pathname.split("/").filter(Boolean);
19
+ if (segments[0] !== "g" || !segments[1])
20
+ return null;
21
+ const roleRef = canonicalRoleRef(segments[1]);
22
+ if (!roleRef)
23
+ return null;
24
+ if (segments.length === 2)
25
+ return { roleRef, workerRef: null };
26
+ if (segments.length !== 4 ||
27
+ segments[2] !== "c" ||
28
+ !segments[3] ||
29
+ segments[3].length > 512 ||
30
+ !/^[A-Za-z0-9_-]+$/.test(segments[3]))
31
+ return null;
32
+ return { roleRef, workerRef: segments[3] };
33
+ }
34
+ export function sameChatGptCarrierTarget(left, right) {
35
+ const leftIdentity = parseChatGptCarrierIdentity(left);
36
+ const rightIdentity = parseChatGptCarrierIdentity(right);
37
+ if (leftIdentity && rightIdentity)
38
+ return (leftIdentity.roleRef === rightIdentity.roleRef &&
39
+ leftIdentity.workerRef === rightIdentity.workerRef);
40
+ try {
41
+ return new URL(left).href === new URL(right).href;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ export function createBrowserOpenObservationGate() {
48
+ let receiptGeneration = 0;
49
+ const receipts = new Map();
50
+ return Object.freeze({
51
+ beginOpen(requestedUrl) {
52
+ return Object.freeze({ requestedUrl, receiptGeneration });
53
+ },
54
+ recordReceipt(observation) {
55
+ receiptGeneration += 1;
56
+ receipts.set(observation.tabId, Object.freeze({
57
+ url: observation.url,
58
+ receiptGeneration,
59
+ }));
60
+ },
61
+ accepts(tabId, boundary) {
62
+ const receipt = receipts.get(tabId);
63
+ return (receipt !== undefined &&
64
+ receipt.receiptGeneration > boundary.receiptGeneration &&
65
+ sameChatGptCarrierTarget(boundary.requestedUrl, receipt.url));
66
+ },
67
+ });
68
+ }
package/dist/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { browserCapabilityIds, executeCapabilityRequestSchema, } from "@tomflow/proflow-execution-contracts";
3
+ import { parseChatGptCarrierIdentity } from "./carrier-identity.js";
3
4
  import { deferVisionObservation, isVisionObservationVerified, parseCapturedScreenshot, } from "./vision.js";
4
5
  export { BrowserRealityBridgeError, createBrowserRealityBridgeServer, } from "./bridge.js";
5
6
  export { createCollaborationCarrierApplication, } from "./collaboration-carrier.js";
@@ -24,27 +25,10 @@ const workerWakeTriggerTypes = new Set([
24
25
  "RECOVERY_RESUME",
25
26
  ]);
26
27
  function parseCarrierIdentity(raw) {
27
- let url;
28
- try {
29
- url = new URL(raw);
30
- }
31
- catch {
32
- throw new ExecutionBrowserError("PRECONDITION_FAILED", "CARRIER_URL_INVALID");
33
- }
34
- if (url.protocol !== "https:" || url.hostname !== "chatgpt.com")
28
+ const identity = parseChatGptCarrierIdentity(raw);
29
+ if (!identity)
35
30
  throw new ExecutionBrowserError("PRECONDITION_FAILED", "CARRIER_URL_INVALID");
36
- const segments = url.pathname.split("/").filter(Boolean);
37
- if (segments[0] !== "g" || !segments[1]?.startsWith("g-"))
38
- throw new ExecutionBrowserError("PRECONDITION_FAILED", "ROLE_URL_INVALID");
39
- if (segments.length === 2)
40
- return { roleRef: segments[1], workerRef: null };
41
- if (segments[2] !== "c" ||
42
- !segments[3] ||
43
- segments[3].length > 512 ||
44
- !/^[A-Za-z0-9_-]+$/.test(segments[3]) ||
45
- segments.length !== 4)
46
- throw new ExecutionBrowserError("PRECONDITION_FAILED", "WORKER_URL_INVALID");
47
- return { roleRef: segments[1], workerRef: segments[3] };
31
+ return identity;
48
32
  }
49
33
  function browserEvidence(idFactory, observation, verified) {
50
34
  return {
@@ -239,6 +223,10 @@ export function createExecutionBrowserExtension(options) {
239
223
  roleIdentity.workerRef)
240
224
  throw new ExecutionBrowserError("PRECONDITION_FAILED", "ROLE_URL_MISMATCH");
241
225
  const opened = await options.browser.open(request.input.roleUrl);
226
+ const openedIdentity = parseCarrierIdentity(opened.url);
227
+ if (openedIdentity.roleRef !== request.input.roleRef ||
228
+ openedIdentity.workerRef !== null)
229
+ throw new ExecutionBrowserError("PRECONDITION_FAILED", "OPENED_ROLE_IDENTITY_MISMATCH");
242
230
  const precondition = await effectStarted(raw);
243
231
  const submitted = await options.browser.submit(opened.tabId, `WORKER_BIND ${request.input.bootstrapFingerprint}`, request.input.bootstrapFingerprint);
244
232
  if (!(await options.browser.hasMessage(submitted.tabId, request.input.bootstrapFingerprint)))
@@ -3,6 +3,10 @@ import {
3
3
  type CarrierContinuationDenial,
4
4
  createCarrierContinuationControl,
5
5
  } from "../src/carrier-continuation-control.js";
6
+ import {
7
+ createBrowserOpenObservationGate,
8
+ parseChatGptCarrierIdentity,
9
+ } from "../src/carrier-identity.js";
6
10
  import type {
7
11
  ActionPermissionFacts,
8
12
  PermissionSemanticAction,
@@ -72,7 +76,8 @@ type BridgeCommand = {
72
76
  | "VERIFY"
73
77
  | "SCREENSHOT"
74
78
  | "PERFORM"
75
- | "CARRIER_ATTENTION_ACTION";
79
+ | "CARRIER_ATTENTION_ACTION"
80
+ | "TASK_OBSERVER_RECOVER";
76
81
  tabId?: number;
77
82
  url?: string;
78
83
  text?: string;
@@ -176,6 +181,7 @@ declare const chrome: ChromeRuntime;
176
181
 
177
182
  const extensionInstanceId = `extension:${crypto.randomUUID()}`;
178
183
  const sessions = new Map<number, ContentObservation>();
184
+ const browserOpenObservationGate = createBrowserOpenObservationGate();
179
185
  const permissionHandling = new Map<number, string>();
180
186
  const permissionAutoAttempts = createCarrierPermissionAttemptRegistry();
181
187
  const carrierAttentions = createCarrierAttentionRegistry();
@@ -839,28 +845,6 @@ async function contentCommand(
839
845
  return response.value;
840
846
  }
841
847
 
842
- function carrierIdentity(
843
- url: string,
844
- ): { roleRef: string; workerRef: string | null } | null {
845
- try {
846
- const parsed = new URL(url);
847
- const segments = parsed.pathname.split("/").filter(Boolean);
848
- if (
849
- parsed.protocol !== "https:" ||
850
- parsed.hostname !== "chatgpt.com" ||
851
- segments[0] !== "g" ||
852
- !segments[1]?.startsWith("g-")
853
- )
854
- return null;
855
- return {
856
- roleRef: segments[1],
857
- workerRef: segments[2] === "c" && segments[3] ? segments[3] : null,
858
- };
859
- } catch {
860
- return null;
861
- }
862
- }
863
-
864
848
  function permissionAttemptKey(observed: ContentObservation): string | null {
865
849
  return observed.blockerFacts
866
850
  ? `${observed.url}:${observed.blockerFacts.fingerprint}`
@@ -873,7 +857,7 @@ function setCarrierAttention(
873
857
  ): void {
874
858
  const facts = observed.blockerFacts;
875
859
  if (!facts) return;
876
- const identity = carrierIdentity(observed.url);
860
+ const identity = parseChatGptCarrierIdentity(observed.url);
877
861
  carrierAttentions.derive({
878
862
  tabId: observed.tabId,
879
863
  contentInstanceId: observed.contentInstanceId,
@@ -1049,7 +1033,7 @@ async function handleActionPermission(
1049
1033
  return;
1050
1034
  permissionHandling.set(observed.tabId, key);
1051
1035
  try {
1052
- const identity = carrierIdentity(observed.url);
1036
+ const identity = parseChatGptCarrierIdentity(observed.url);
1053
1037
  const humanDenied = () =>
1054
1038
  carrierContinuationControl.hasMatchingPermissionDenial({
1055
1039
  tabId: observed.tabId,
@@ -1166,6 +1150,7 @@ function processContentObservation(
1166
1150
  ): void {
1167
1151
  const previous = sessions.get(observed.tabId);
1168
1152
  sessions.set(observed.tabId, observed);
1153
+ browserOpenObservationGate.recordReceipt(observed);
1169
1154
  permissionAutoAttempts.observe(
1170
1155
  observed.tabId,
1171
1156
  permissionAttemptKey(observed),
@@ -1328,6 +1313,10 @@ function text(value: unknown, name: string): string {
1328
1313
  }
1329
1314
 
1330
1315
  async function executeCommand(command: BridgeCommand): Promise<unknown> {
1316
+ if (command.type === "TASK_OBSERVER_RECOVER") {
1317
+ void runObserverRecovery();
1318
+ return { scheduled: true };
1319
+ }
1331
1320
  if (command.type === "CARRIER_ATTENTION_ACTION") {
1332
1321
  if (command.action !== "allowOnce" && command.action !== "deny")
1333
1322
  throw new Error("ATTENTION_ACTION_INVALID");
@@ -1349,8 +1338,12 @@ async function executeCommand(command: BridgeCommand): Promise<unknown> {
1349
1338
  const parsed = new URL(url);
1350
1339
  if (parsed.protocol !== "https:" || parsed.hostname !== "chatgpt.com")
1351
1340
  throw new Error("URL_SCOPE_DENIED");
1341
+ const boundary = browserOpenObservationGate.beginOpen(url);
1352
1342
  const tab = await chrome.tabs.create({ url, active: true });
1353
- return waitForObservation(numeric(tab.id, "TAB_ID"));
1343
+ const tabId = numeric(tab.id, "TAB_ID");
1344
+ return waitForObservation(tabId, () =>
1345
+ browserOpenObservationGate.accepts(tabId, boundary),
1346
+ );
1354
1347
  }
1355
1348
  const tabId = numeric(command.tabId, "TAB_ID");
1356
1349
  if (command.type === "OBSERVE")
@@ -1,4 +1,67 @@
1
- import { parseCarrierAttentionViews } from "../src/carrier-attention-view.js";
1
+ import type { CarrierAttentionView } from "../src/carrier-attention-view.js";
2
+
3
+ // The loopback Tasks surface publishes this entry as one authenticated
4
+ // `/tasks/app.js` module. A runtime relative import would escape `/tasks`, so keep
5
+ // the bounded parser local while sharing only the erased TypeScript type above.
6
+ function nullableString(value: unknown): value is string | null {
7
+ return value === null || typeof value === "string";
8
+ }
9
+
10
+ function parseCarrierAttentionView(
11
+ value: unknown,
12
+ ): CarrierAttentionView | null {
13
+ if (typeof value !== "object" || value === null || Array.isArray(value))
14
+ return null;
15
+ const attentionRef = Reflect.get(value, "attentionRef");
16
+ const occurrenceRef = Reflect.get(value, "occurrenceRef");
17
+ const taskId = Reflect.get(value, "taskId");
18
+ const roleRef = Reflect.get(value, "roleRef");
19
+ const workerRef = Reflect.get(value, "workerRef");
20
+ const targetHost = Reflect.get(value, "targetHost");
21
+ const operationId = Reflect.get(value, "operationId");
22
+ const reason = Reflect.get(value, "reason");
23
+ const actions = Reflect.get(value, "actions");
24
+ const observedAt = Reflect.get(value, "observedAt");
25
+ if (
26
+ typeof attentionRef !== "string" ||
27
+ attentionRef.length === 0 ||
28
+ typeof occurrenceRef !== "string" ||
29
+ occurrenceRef.length === 0 ||
30
+ !nullableString(taskId) ||
31
+ !nullableString(roleRef) ||
32
+ !nullableString(workerRef) ||
33
+ !nullableString(targetHost) ||
34
+ typeof operationId !== "string" ||
35
+ operationId.length === 0 ||
36
+ typeof reason !== "string" ||
37
+ reason.length === 0 ||
38
+ !Array.isArray(actions) ||
39
+ actions.some((action) => action !== "allowOnce" && action !== "deny") ||
40
+ typeof observedAt !== "string" ||
41
+ observedAt.length === 0
42
+ )
43
+ return null;
44
+ return {
45
+ attentionRef,
46
+ occurrenceRef,
47
+ taskId,
48
+ roleRef,
49
+ workerRef,
50
+ targetHost,
51
+ operationId,
52
+ reason,
53
+ actions: [...actions],
54
+ observedAt,
55
+ };
56
+ }
57
+
58
+ function parseCarrierAttentionViews(value: unknown): CarrierAttentionView[] {
59
+ if (!Array.isArray(value)) return [];
60
+ return value
61
+ .slice(0, 128)
62
+ .map(parseCarrierAttentionView)
63
+ .filter((item): item is CarrierAttentionView => item !== null);
64
+ }
2
65
 
3
66
  type ChromePanel = {
4
67
  runtime: { sendMessage(message: unknown): Promise<unknown> };
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "ProFlow Execution Browser",
4
- "version": "0.1.39",
4
+ "version": "0.1.41",
5
5
  "permissions": [
6
6
  "tabs",
7
7
  "storage"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-execution-browser-extension",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "Execution-owned MV3 Browser executor, evidence provider and browser application surface.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -3,7 +3,7 @@
3
3
  "contractVersion": "1.0.0",
4
4
  "moduleRef": "execution-browser-extension",
5
5
  "packageName": "@tomflow/proflow-execution-browser-extension",
6
- "moduleVersion": "0.1.39",
6
+ "moduleVersion": "0.1.41",
7
7
  "kind": "browser-extension",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",