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

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,7 @@
1
+ # @tomflow/proflow-execution-browser-extension
2
+
3
+ ## 0.1.40
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.40";
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.40";
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.40";
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.40";
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.40";
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.40";
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.40";
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.40";
184
184
  } | {
185
185
  contract: "deployment.result.v1";
186
186
  moduleRef: "execution-browser-extension";
187
- moduleVersion: "0.1.39";
187
+ moduleVersion: "0.1.40";
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.40";
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.40";
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.40",
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)
@@ -14971,8 +15016,13 @@ async function executeCommand(command) {
14971
15016
  const parsed = new URL(url2);
14972
15017
  if (parsed.protocol !== "https:" || parsed.hostname !== "chatgpt.com")
14973
15018
  throw new Error("URL_SCOPE_DENIED");
15019
+ const boundary = browserOpenObservationGate.beginOpen(url2);
14974
15020
  const tab = await chrome.tabs.create({ url: url2, active: true });
14975
- return waitForObservation(numeric(tab.id, "TAB_ID"));
15021
+ const tabId2 = numeric(tab.id, "TAB_ID");
15022
+ return waitForObservation(
15023
+ tabId2,
15024
+ () => browserOpenObservationGate.accepts(tabId2, boundary)
15025
+ );
14976
15026
  }
14977
15027
  const tabId = numeric(command.tabId, "TAB_ID");
14978
15028
  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 {};
@@ -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,
@@ -176,6 +180,7 @@ declare const chrome: ChromeRuntime;
176
180
 
177
181
  const extensionInstanceId = `extension:${crypto.randomUUID()}`;
178
182
  const sessions = new Map<number, ContentObservation>();
183
+ const browserOpenObservationGate = createBrowserOpenObservationGate();
179
184
  const permissionHandling = new Map<number, string>();
180
185
  const permissionAutoAttempts = createCarrierPermissionAttemptRegistry();
181
186
  const carrierAttentions = createCarrierAttentionRegistry();
@@ -839,28 +844,6 @@ async function contentCommand(
839
844
  return response.value;
840
845
  }
841
846
 
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
847
  function permissionAttemptKey(observed: ContentObservation): string | null {
865
848
  return observed.blockerFacts
866
849
  ? `${observed.url}:${observed.blockerFacts.fingerprint}`
@@ -873,7 +856,7 @@ function setCarrierAttention(
873
856
  ): void {
874
857
  const facts = observed.blockerFacts;
875
858
  if (!facts) return;
876
- const identity = carrierIdentity(observed.url);
859
+ const identity = parseChatGptCarrierIdentity(observed.url);
877
860
  carrierAttentions.derive({
878
861
  tabId: observed.tabId,
879
862
  contentInstanceId: observed.contentInstanceId,
@@ -1049,7 +1032,7 @@ async function handleActionPermission(
1049
1032
  return;
1050
1033
  permissionHandling.set(observed.tabId, key);
1051
1034
  try {
1052
- const identity = carrierIdentity(observed.url);
1035
+ const identity = parseChatGptCarrierIdentity(observed.url);
1053
1036
  const humanDenied = () =>
1054
1037
  carrierContinuationControl.hasMatchingPermissionDenial({
1055
1038
  tabId: observed.tabId,
@@ -1166,6 +1149,7 @@ function processContentObservation(
1166
1149
  ): void {
1167
1150
  const previous = sessions.get(observed.tabId);
1168
1151
  sessions.set(observed.tabId, observed);
1152
+ browserOpenObservationGate.recordReceipt(observed);
1169
1153
  permissionAutoAttempts.observe(
1170
1154
  observed.tabId,
1171
1155
  permissionAttemptKey(observed),
@@ -1349,8 +1333,12 @@ async function executeCommand(command: BridgeCommand): Promise<unknown> {
1349
1333
  const parsed = new URL(url);
1350
1334
  if (parsed.protocol !== "https:" || parsed.hostname !== "chatgpt.com")
1351
1335
  throw new Error("URL_SCOPE_DENIED");
1336
+ const boundary = browserOpenObservationGate.beginOpen(url);
1352
1337
  const tab = await chrome.tabs.create({ url, active: true });
1353
- return waitForObservation(numeric(tab.id, "TAB_ID"));
1338
+ const tabId = numeric(tab.id, "TAB_ID");
1339
+ return waitForObservation(tabId, () =>
1340
+ browserOpenObservationGate.accepts(tabId, boundary),
1341
+ );
1354
1342
  }
1355
1343
  const tabId = numeric(command.tabId, "TAB_ID");
1356
1344
  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.40",
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.40",
4
4
  "description": "Execution-owned MV3 Browser executor, evidence provider and browser application surface.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -31,8 +31,8 @@
31
31
  "@tomflow/proflow-module-contract": "^0.1.13"
32
32
  },
33
33
  "devDependencies": {
34
- "@tomflow/proflow-execution-runtime": "^0.1.17",
35
- "@tomflow/proflow-deployment-conformance": "^0.1.13"
34
+ "@tomflow/proflow-deployment-conformance": "^0.1.13",
35
+ "@tomflow/proflow-execution-runtime": "^0.1.17"
36
36
  },
37
37
  "keywords": [
38
38
  "proflow",
@@ -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.40",
7
7
  "kind": "browser-extension",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",