@tomflow/proflow-execution-browser-extension 0.1.0

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.
Files changed (39) hide show
  1. package/README.md +7 -0
  2. package/conformance.json +1 -0
  3. package/deployment/browser-extension.json +6 -0
  4. package/dist/deployment/adapter.d.ts +61 -0
  5. package/dist/deployment/adapter.js +47 -0
  6. package/dist/deployment/descriptor.d.ts +103 -0
  7. package/dist/deployment/descriptor.js +109 -0
  8. package/dist/extension/background.d.ts +1 -0
  9. package/dist/extension/background.js +752 -0
  10. package/dist/extension/content.d.ts +1 -0
  11. package/dist/extension/content.js +90 -0
  12. package/dist/extension/options.d.ts +1 -0
  13. package/dist/extension/options.js +68 -0
  14. package/dist/extension/side-panel.d.ts +1 -0
  15. package/dist/extension/side-panel.js +262 -0
  16. package/dist/src/bridge.d.ts +26 -0
  17. package/dist/src/bridge.js +288 -0
  18. package/dist/src/collaboration-carrier.d.ts +65 -0
  19. package/dist/src/collaboration-carrier.js +138 -0
  20. package/dist/src/index.d.ts +137 -0
  21. package/dist/src/index.js +779 -0
  22. package/dist/src/runtime-composition.d.ts +97 -0
  23. package/dist/src/runtime-composition.js +124 -0
  24. package/dist/src/system-observer.d.ts +86 -0
  25. package/dist/src/system-observer.js +252 -0
  26. package/dist/src/task-observer.d.ts +118 -0
  27. package/dist/src/task-observer.js +105 -0
  28. package/dist/src/vision.d.ts +73 -0
  29. package/dist/src/vision.js +82 -0
  30. package/extension/background.ts +997 -0
  31. package/extension/content.ts +138 -0
  32. package/extension/options.html +54 -0
  33. package/extension/options.ts +98 -0
  34. package/extension/side-panel.html +77 -0
  35. package/extension/side-panel.ts +349 -0
  36. package/manifest.json +20 -0
  37. package/package.json +58 -0
  38. package/proflow.module.json +127 -0
  39. package/self-install.mjs +27 -0
@@ -0,0 +1,752 @@
1
+ import { createCollaborationCarrierApplication, createSystemObserver, createTaskObserver, } from "../src/index.js";
2
+ const extensionInstanceId = `extension:${crypto.randomUUID()}`;
3
+ const sessions = new Map();
4
+ const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
5
+ async function persistSnapshot() {
6
+ await chrome.storage.session.set({
7
+ proflowBrowserSnapshot: {
8
+ extensionInstanceId,
9
+ observedAt: new Date().toISOString(),
10
+ sessions: [...sessions.values()],
11
+ recoveryScan: "BOUNDED_ON_START",
12
+ },
13
+ });
14
+ }
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ function parseConfig(value) {
19
+ if (!isRecord(value))
20
+ return null;
21
+ const endpoint = value.endpoint;
22
+ const token = value.token;
23
+ if (typeof endpoint !== "string" || typeof token !== "string")
24
+ return null;
25
+ let url;
26
+ try {
27
+ url = new URL(endpoint);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ if (url.protocol !== "http:" ||
33
+ url.hostname !== "127.0.0.1" ||
34
+ url.pathname !== "/" ||
35
+ url.search !== "" ||
36
+ url.hash !== "" ||
37
+ token.length < 32)
38
+ return null;
39
+ return { endpoint: endpoint.replace(/\/$/, ""), token };
40
+ }
41
+ async function bridgeConfig() {
42
+ const stored = await chrome.storage.local.get("proflowRuntimeBridge");
43
+ return parseConfig(stored.proflowRuntimeBridge);
44
+ }
45
+ async function taskApplicationConfig() {
46
+ const stored = await chrome.storage.local.get("proflowTaskApplication");
47
+ return parseConfig(stored.proflowTaskApplication);
48
+ }
49
+ async function approvalApplicationConfig() {
50
+ const stored = await chrome.storage.local.get("proflowApprovalApplication");
51
+ return parseConfig(stored.proflowApprovalApplication);
52
+ }
53
+ async function invokeApprovalApplication(operation, input) {
54
+ const config = await approvalApplicationConfig();
55
+ if (!config)
56
+ throw new Error("APPROVAL_APPLICATION_NOT_CONFIGURED");
57
+ const response = await fetch(`${config.endpoint}/application/approval`, {
58
+ method: "POST",
59
+ headers: {
60
+ authorization: `Bearer ${config.token}`,
61
+ "content-type": "application/json",
62
+ },
63
+ body: JSON.stringify({ operation, input }),
64
+ });
65
+ const body = (await response.json());
66
+ if (!response.ok) {
67
+ const detail = isRecord(body) && typeof body.error === "string"
68
+ ? body.error
69
+ : "APPROVAL_APPLICATION_REQUEST_FAILED";
70
+ throw new Error(detail);
71
+ }
72
+ return body;
73
+ }
74
+ async function invokeTaskApplication(operation, input) {
75
+ const config = await taskApplicationConfig();
76
+ if (!config)
77
+ throw new Error("TASK_APPLICATION_NOT_CONFIGURED");
78
+ const response = await fetch(`${config.endpoint}/application/task`, {
79
+ method: "POST",
80
+ headers: {
81
+ authorization: `Bearer ${config.token}`,
82
+ "content-type": "application/json",
83
+ },
84
+ body: JSON.stringify({ operation, input }),
85
+ });
86
+ const body = (await response.json());
87
+ if (!response.ok) {
88
+ const detail = isRecord(body) && typeof body.error === "string"
89
+ ? body.error
90
+ : "TASK_APPLICATION_REQUEST_FAILED";
91
+ throw new Error(detail);
92
+ }
93
+ return body;
94
+ }
95
+ function sanitizeConversationLocator(value) {
96
+ try {
97
+ const locator = new URL(value);
98
+ locator.username = "";
99
+ locator.password = "";
100
+ locator.search = "";
101
+ locator.hash = "";
102
+ return locator.toString();
103
+ }
104
+ catch {
105
+ return "[REDACTED_INVALID_LOCATOR]";
106
+ }
107
+ }
108
+ function normalizeLogErrorCode(error, fallback) {
109
+ const value = error instanceof Error ? error.message : error;
110
+ return typeof value === "string" && /^[A-Z][A-Z0-9_.:-]{0,159}$/.test(value)
111
+ ? value
112
+ : fallback;
113
+ }
114
+ function structuredAxes(input) {
115
+ const result = {};
116
+ for (const key of [
117
+ "correlationId",
118
+ "taskId",
119
+ "nodeId",
120
+ "agentPackageRef",
121
+ "roleRef",
122
+ "workerRef",
123
+ "executionRef",
124
+ "messageRef",
125
+ "artifactRef",
126
+ "evidenceRef",
127
+ ]) {
128
+ const value = input[key];
129
+ if (typeof value === "string" && value.length > 0)
130
+ result[key] = value;
131
+ }
132
+ if (typeof input.conversationLocator === "string" &&
133
+ input.conversationLocator.length > 0)
134
+ result.conversationLocator = sanitizeConversationLocator(input.conversationLocator);
135
+ for (const key of ["runNo", "attemptNo", "tabId"]) {
136
+ const value = input[key];
137
+ if (Number.isInteger(value) && Number(value) >= 0)
138
+ result[key] = Number(value);
139
+ }
140
+ return result;
141
+ }
142
+ async function emitStructuredLog(entry) {
143
+ const config = await taskApplicationConfig();
144
+ if (!config)
145
+ return;
146
+ await fetch(`${config.endpoint}/application/log`, {
147
+ method: "POST",
148
+ headers: {
149
+ authorization: `Bearer ${config.token}`,
150
+ "content-type": "application/json",
151
+ },
152
+ body: JSON.stringify({ timestamp: new Date().toISOString(), ...entry }),
153
+ }).catch(() => undefined);
154
+ }
155
+ async function invokeObserverApplication(operation, input) {
156
+ const config = await taskApplicationConfig();
157
+ if (!config)
158
+ throw new Error("OBSERVER_APPLICATION_NOT_CONFIGURED");
159
+ const component = operation.startsWith("collaboration.")
160
+ ? "browser-collaboration-carrier"
161
+ : "browser-observer";
162
+ try {
163
+ const response = await fetch(`${config.endpoint}/application/observer`, {
164
+ method: "POST",
165
+ headers: {
166
+ authorization: `Bearer ${config.token}`,
167
+ "content-type": "application/json",
168
+ },
169
+ body: JSON.stringify({ operation, input }),
170
+ });
171
+ const body = (await response.json());
172
+ if (!response.ok) {
173
+ const detail = isRecord(body) && typeof body.error === "string"
174
+ ? body.error
175
+ : "OBSERVER_APPLICATION_REQUEST_FAILED";
176
+ throw new Error(detail);
177
+ }
178
+ void emitStructuredLog({
179
+ level: "INFO",
180
+ component,
181
+ operation,
182
+ status: "SUCCEEDED",
183
+ ...structuredAxes(input),
184
+ });
185
+ return body;
186
+ }
187
+ catch (error) {
188
+ void emitStructuredLog({
189
+ level: "WARN",
190
+ component,
191
+ operation,
192
+ status: "FAILED",
193
+ errorCode: normalizeLogErrorCode(error, "OBSERVER_APPLICATION_REQUEST_FAILED"),
194
+ ...structuredAxes(input),
195
+ });
196
+ throw error;
197
+ }
198
+ }
199
+ const taskObserver = createTaskObserver({
200
+ owner: {
201
+ async getTaskDriveProjection(taskId) {
202
+ return (await invokeObserverApplication("task.projection", {
203
+ taskId,
204
+ }));
205
+ },
206
+ },
207
+ diagnostic: {
208
+ async assess(input) {
209
+ return (await invokeObserverApplication("task.diagnostic", {
210
+ taskId: input.taskId,
211
+ nodeId: input.nodeId,
212
+ correlationId: input.anomaly.ref,
213
+ payload: input,
214
+ }));
215
+ },
216
+ },
217
+ carrier: {
218
+ async requestWake(input) {
219
+ return invokeObserverApplication("task.wake", input);
220
+ },
221
+ },
222
+ });
223
+ const collaborationCarrier = createCollaborationCarrierApplication({
224
+ task: {
225
+ async getWorkerBinding(taskId, roleRef) {
226
+ return (await invokeObserverApplication("collaboration.binding", {
227
+ taskId,
228
+ roleRef,
229
+ }));
230
+ },
231
+ },
232
+ agent: {
233
+ async listPendingMessages(limit) {
234
+ return (await invokeObserverApplication("collaboration.listPending", {
235
+ limit,
236
+ }));
237
+ },
238
+ async getPendingMessage(messageRef) {
239
+ return (await invokeObserverApplication("collaboration.getPending", {
240
+ messageRef,
241
+ }));
242
+ },
243
+ async reportDeliveryOutcome(input) {
244
+ await invokeObserverApplication("collaboration.reportDelivery", input);
245
+ },
246
+ },
247
+ execution: {
248
+ async execute(request) {
249
+ return invokeObserverApplication("collaboration.execute", { request });
250
+ },
251
+ },
252
+ callerRef: "extension:collaboration-carrier",
253
+ });
254
+ const systemObserver = createSystemObserver({
255
+ snapshots: {
256
+ async readView(view) {
257
+ return invokeObserverApplication("system.view", { view });
258
+ },
259
+ async readDrilldown({ topic }) {
260
+ return invokeObserverApplication("system.drilldown", { topic });
261
+ },
262
+ },
263
+ async reason(request) {
264
+ return (await invokeObserverApplication("system.reason", {
265
+ assessmentRef: request.assessmentRef,
266
+ payload: request,
267
+ }));
268
+ },
269
+ });
270
+ const SYSTEM_OBSERVER_STATE_KEY = "proflowSystemObserverState";
271
+ async function loadSystemObserverState() {
272
+ const stored = await chrome.storage.local.get(SYSTEM_OBSERVER_STATE_KEY);
273
+ const value = stored[SYSTEM_OBSERVER_STATE_KEY];
274
+ if (!isRecord(value))
275
+ return null;
276
+ if (typeof value.assessmentRef !== "string" ||
277
+ typeof value.observedAt !== "string") {
278
+ return null;
279
+ }
280
+ const unresolved = Array.isArray(value.unresolved)
281
+ ? value.unresolved
282
+ .filter((item) => typeof item === "string")
283
+ .slice(0, 50)
284
+ : [];
285
+ const carryForward = Array.isArray(value.carryForward)
286
+ ? value.carryForward.slice(0, 50).flatMap((item) => {
287
+ if (!isRecord(item) ||
288
+ typeof item.hypothesis !== "string" ||
289
+ typeof item.confidence !== "number") {
290
+ return [];
291
+ }
292
+ return [
293
+ {
294
+ hypothesis: item.hypothesis,
295
+ ...(typeof item.risk === "string" ? { risk: item.risk } : {}),
296
+ ...(typeof item.evidenceRef === "string"
297
+ ? { evidenceRef: item.evidenceRef }
298
+ : {}),
299
+ confidence: item.confidence,
300
+ },
301
+ ];
302
+ })
303
+ : [];
304
+ return {
305
+ assessmentRef: value.assessmentRef,
306
+ observedAt: value.observedAt,
307
+ unresolved,
308
+ carryForward,
309
+ };
310
+ }
311
+ async function persistSystemObserverState(result) {
312
+ if (result.status !== "ASSESSED" || !result.global)
313
+ return;
314
+ await chrome.storage.local.set({
315
+ [SYSTEM_OBSERVER_STATE_KEY]: {
316
+ assessmentRef: result.assessmentRef,
317
+ observedAt: result.observedAt,
318
+ unresolved: [...result.global.unresolved],
319
+ carryForward: result.global.carryForward.map((item) => ({ ...item })),
320
+ },
321
+ });
322
+ }
323
+ let observerRecoveryInFlight = null;
324
+ function runObserverRecovery() {
325
+ if (observerRecoveryInFlight)
326
+ return observerRecoveryInFlight;
327
+ observerRecoveryInFlight = (async () => {
328
+ await collaborationCarrier.recoverPending(50).catch(() => undefined);
329
+ const signalBatch = await invokeObserverApplication("execution.listSignals", { limit: 50 }).catch(() => null);
330
+ if (isRecord(signalBatch) && Array.isArray(signalBatch.signals)) {
331
+ for (const candidate of signalBatch.signals) {
332
+ if (!isRecord(candidate) ||
333
+ typeof candidate.signalRef !== "string" ||
334
+ typeof candidate.executionRef !== "string" ||
335
+ typeof candidate.taskId !== "string" ||
336
+ typeof candidate.workerRef !== "string")
337
+ continue;
338
+ try {
339
+ const decision = candidate.kind === "RECOVERY_RESUME"
340
+ ? await taskObserver.drive(candidate.taskId, {
341
+ trigger: "RECOVERY_RESUME",
342
+ ref: candidate.executionRef,
343
+ targetWorkerRef: candidate.workerRef,
344
+ })
345
+ : candidate.kind === "UNKNOWN_REALITY"
346
+ ? await taskObserver.drive(candidate.taskId, undefined, {
347
+ kind: "UNKNOWN_REALITY",
348
+ ref: candidate.executionRef,
349
+ facts: {
350
+ executionRef: candidate.executionRef,
351
+ summary: `Execution ${candidate.executionRef} recovery remains UNKNOWN`,
352
+ },
353
+ })
354
+ : null;
355
+ if (!decision)
356
+ continue;
357
+ if (decision.kind === "NOOP" &&
358
+ (decision.reason === "BINDING_NOT_READY" ||
359
+ decision.reason === "RESUME_TARGET_NOT_CURRENT_WORKER" ||
360
+ decision.reason === "DIAGNOSTIC_UNAVAILABLE" ||
361
+ decision.reason.startsWith("DIAGNOSTIC_DEFERRED:")))
362
+ continue;
363
+ await invokeObserverApplication("execution.ackSignal", {
364
+ signalRef: candidate.signalRef,
365
+ });
366
+ }
367
+ catch {
368
+ // Leave the durable signal unacknowledged for the next bounded recovery pass.
369
+ }
370
+ }
371
+ }
372
+ const listed = await invokeTaskApplication("task.list", {});
373
+ if (isRecord(listed) && Array.isArray(listed.tasks)) {
374
+ for (const candidate of listed.tasks.slice(0, 100)) {
375
+ if (!isRecord(candidate) || typeof candidate.taskId !== "string")
376
+ continue;
377
+ if (candidate.status === "SUCCEEDED" ||
378
+ candidate.status === "TERMINATED")
379
+ continue;
380
+ // J1 Worker teaming recovery is driven from durable Task binding facts.
381
+ // This bounded startup/event recovery pass re-runs the idempotent
382
+ // ensureWorkers application before Task progression, so Dev/Test
383
+ // completion never depends on an in-memory Promise surviving a Host
384
+ // or Extension restart. Successful bindings are preserved and only
385
+ // missing roles are re-provisioned by the Host/Execution path.
386
+ await invokeTaskApplication("task.ensureWorkers", {
387
+ taskId: candidate.taskId,
388
+ }).catch(() => undefined);
389
+ await taskObserver.drive(candidate.taskId).catch(() => undefined);
390
+ }
391
+ }
392
+ const previousSystemState = await loadSystemObserverState().catch(() => null);
393
+ const systemAssessment = await systemObserver
394
+ .synthesize({
395
+ previousUnresolved: previousSystemState?.unresolved ?? [],
396
+ previousCarryForward: previousSystemState?.carryForward ?? [],
397
+ })
398
+ .catch(() => null);
399
+ if (systemAssessment) {
400
+ await persistSystemObserverState(systemAssessment).catch(() => undefined);
401
+ }
402
+ })().finally(() => {
403
+ observerRecoveryInFlight = null;
404
+ });
405
+ return observerRecoveryInFlight;
406
+ }
407
+ function observationFor(tabId) {
408
+ const observed = sessions.get(tabId);
409
+ if (!observed)
410
+ throw new Error("CONTENT_SESSION_NOT_READY");
411
+ return observed;
412
+ }
413
+ async function waitForObservation(tabId, predicate = () => true) {
414
+ for (let attempt = 0; attempt < 60; attempt += 1) {
415
+ const observed = sessions.get(tabId);
416
+ if (observed && predicate(observed))
417
+ return observed;
418
+ await sleep(250);
419
+ }
420
+ throw new Error("CONTENT_SESSION_TIMEOUT");
421
+ }
422
+ async function contentCommand(tabId, command) {
423
+ const observed = observationFor(tabId);
424
+ const response = await chrome.tabs.sendMessage(tabId, {
425
+ type: "PROFLOW_PAGE_COMMAND",
426
+ contentInstanceId: observed.contentInstanceId,
427
+ expectedUrl: observed.url,
428
+ ...command,
429
+ });
430
+ if (!isRecord(response) || response.ok !== true) {
431
+ const detail = isRecord(response) && typeof response.error === "string"
432
+ ? response.error
433
+ : "PAGE_COMMAND_FAILED";
434
+ throw new Error(detail);
435
+ }
436
+ return response.value;
437
+ }
438
+ function numeric(value, name) {
439
+ if (!Number.isInteger(value))
440
+ throw new Error(`${name}_INVALID`);
441
+ return value;
442
+ }
443
+ function text(value, name) {
444
+ if (typeof value !== "string" || value.length === 0)
445
+ throw new Error(`${name}_INVALID`);
446
+ return value;
447
+ }
448
+ async function executeCommand(command) {
449
+ if (command.type === "LIST_TABS") {
450
+ const tabs = await chrome.tabs.query({ url: "https://chatgpt.com/g/*" });
451
+ return tabs
452
+ .map((tab) => tab.id)
453
+ .filter((tabId) => tabId !== undefined)
454
+ .map((tabId) => sessions.get(tabId))
455
+ .filter((value) => value !== undefined);
456
+ }
457
+ if (command.type === "OPEN") {
458
+ const url = text(command.url, "URL");
459
+ const parsed = new URL(url);
460
+ if (parsed.protocol !== "https:" || parsed.hostname !== "chatgpt.com")
461
+ throw new Error("URL_SCOPE_DENIED");
462
+ const tab = await chrome.tabs.create({ url, active: true });
463
+ return waitForObservation(numeric(tab.id, "TAB_ID"));
464
+ }
465
+ const tabId = numeric(command.tabId, "TAB_ID");
466
+ if (command.type === "OBSERVE")
467
+ return contentCommand(tabId, { operation: "observe" });
468
+ if (command.type === "SUBMIT") {
469
+ const before = observationFor(tabId);
470
+ try {
471
+ await contentCommand(tabId, {
472
+ operation: "submit",
473
+ value: text(command.text, "TEXT"),
474
+ fingerprint: text(command.fingerprint, "FINGERPRINT"),
475
+ });
476
+ }
477
+ catch (error) {
478
+ const replacement = await waitForObservation(tabId, (value) => value.contentInstanceId !== before.contentInstanceId).catch(() => null);
479
+ if (!replacement)
480
+ throw error;
481
+ }
482
+ return waitForObservation(tabId);
483
+ }
484
+ if (command.type === "VERIFY")
485
+ return contentCommand(tabId, {
486
+ operation: "verify",
487
+ fingerprint: text(command.fingerprint, "FINGERPRINT"),
488
+ });
489
+ if (command.type === "SCREENSHOT") {
490
+ const observed = observationFor(tabId);
491
+ await chrome.tabs.update(tabId, { active: true });
492
+ const dataUrl = await chrome.tabs.captureVisibleTab(observed.windowId, {
493
+ format: "png",
494
+ });
495
+ const bytes = new TextEncoder().encode(dataUrl);
496
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
497
+ const hex = [...new Uint8Array(digest)]
498
+ .map((value) => value.toString(16).padStart(2, "0"))
499
+ .join("");
500
+ return {
501
+ evidenceRef: `screenshot:sha256:${hex}`,
502
+ dataUrl,
503
+ mimeType: "image/png",
504
+ sizeBytes: bytes.byteLength,
505
+ hash: `sha256:${hex}`,
506
+ };
507
+ }
508
+ const request = command.request;
509
+ if (!isRecord(request) || !isRecord(request.input))
510
+ throw new Error("EXECUTION_REQUEST_INVALID");
511
+ const capability = text(request.capability, "CAPABILITY");
512
+ if (capability === "browser.navigate") {
513
+ const url = text(request.input.url, "URL");
514
+ const parsed = new URL(url);
515
+ if (parsed.protocol !== "https:" || parsed.hostname !== "chatgpt.com")
516
+ throw new Error("URL_SCOPE_DENIED");
517
+ const before = sessions.get(tabId)?.contentInstanceId;
518
+ await chrome.tabs.update(tabId, { url });
519
+ return waitForObservation(tabId, (value) => before === undefined || value.contentInstanceId !== before);
520
+ }
521
+ if (capability === "browser.input" || capability === "browser.click") {
522
+ await contentCommand(tabId, {
523
+ operation: capability === "browser.input" ? "input" : "click",
524
+ selector: text(request.input.selector, "SELECTOR"),
525
+ ...(capability === "browser.input"
526
+ ? { value: text(request.input.value, "VALUE") }
527
+ : {}),
528
+ });
529
+ return waitForObservation(tabId);
530
+ }
531
+ if (capability === "browser.submit") {
532
+ await contentCommand(tabId, {
533
+ operation: "submit",
534
+ ...(typeof request.input.selector === "string"
535
+ ? { selector: request.input.selector }
536
+ : {}),
537
+ value: text(request.input.fingerprint, "FINGERPRINT"),
538
+ fingerprint: text(request.input.fingerprint, "FINGERPRINT"),
539
+ });
540
+ return waitForObservation(tabId);
541
+ }
542
+ if (capability === "browser.wait") {
543
+ const timeoutMs = numeric(request.input.timeoutMs, "TIMEOUT");
544
+ const end = Date.now() + timeoutMs;
545
+ while (Date.now() < end) {
546
+ const observed = await contentCommand(tabId, { operation: "observe" });
547
+ if (isRecord(observed) && observed.pageState === "IDLE")
548
+ return observed;
549
+ await sleep(250);
550
+ }
551
+ throw new Error("WAIT_TIMEOUT");
552
+ }
553
+ throw new Error("BROWSER_PRIMITIVE_UNAVAILABLE");
554
+ }
555
+ async function bridgeFetch(config, path, init = {}) {
556
+ return fetch(`${config.endpoint}${path}`, {
557
+ ...init,
558
+ headers: {
559
+ authorization: `Bearer ${config.token}`,
560
+ "content-type": "application/json",
561
+ ...(init.headers ?? {}),
562
+ },
563
+ });
564
+ }
565
+ let bridgeLoopStarted = false;
566
+ async function runBridgeLoop() {
567
+ if (bridgeLoopStarted)
568
+ return;
569
+ bridgeLoopStarted = true;
570
+ while (true) {
571
+ const config = await bridgeConfig();
572
+ if (!config) {
573
+ await sleep(1_000);
574
+ continue;
575
+ }
576
+ const query = `?extensionInstanceId=${encodeURIComponent(extensionInstanceId)}`;
577
+ try {
578
+ const hello = await bridgeFetch(config, "/v1/session/hello", {
579
+ method: "POST",
580
+ body: JSON.stringify({
581
+ extensionId: chrome.runtime.id,
582
+ extensionInstanceId,
583
+ }),
584
+ });
585
+ if (!hello.ok)
586
+ throw new Error("BRIDGE_HELLO_REJECTED");
587
+ let lastHeartbeatAt = 0;
588
+ while (true) {
589
+ if (Date.now() - lastHeartbeatAt >= 5_000) {
590
+ const heartbeat = await bridgeFetch(config, `/v1/session/heartbeat${query}`, { method: "POST", body: "{}" });
591
+ if (!heartbeat.ok)
592
+ throw new Error("BRIDGE_HEARTBEAT_REJECTED");
593
+ lastHeartbeatAt = Date.now();
594
+ }
595
+ const response = await bridgeFetch(config, `/v1/commands/next${query}`);
596
+ if (response.status === 204) {
597
+ await sleep(250);
598
+ continue;
599
+ }
600
+ if (!response.ok)
601
+ throw new Error("BRIDGE_POLL_REJECTED");
602
+ const command = (await response.json());
603
+ let result;
604
+ try {
605
+ result = {
606
+ commandId: command.commandId,
607
+ ok: true,
608
+ value: await executeCommand(command),
609
+ };
610
+ }
611
+ catch (error) {
612
+ result = {
613
+ commandId: command.commandId,
614
+ ok: false,
615
+ error: error instanceof Error
616
+ ? error.message
617
+ : "EXTENSION_COMMAND_FAILED",
618
+ };
619
+ }
620
+ void emitStructuredLog({
621
+ level: result.ok === true ? "INFO" : "WARN",
622
+ component: "browser-carrier",
623
+ operation: command.type,
624
+ operationRef: command.commandId,
625
+ status: result.ok === true ? "SUCCEEDED" : "FAILED",
626
+ ...(typeof result.error === "string"
627
+ ? {
628
+ errorCode: normalizeLogErrorCode(result.error, "EXTENSION_COMMAND_FAILED"),
629
+ }
630
+ : {}),
631
+ ...(command.tabId === undefined ? {} : { tabId: command.tabId }),
632
+ ...(isRecord(command.request)
633
+ ? {
634
+ ...(typeof command.request.capability === "string"
635
+ ? { capability: command.request.capability }
636
+ : {}),
637
+ ...structuredAxes(command.request),
638
+ }
639
+ : {}),
640
+ });
641
+ const reported = await bridgeFetch(config, `/v1/commands/result${query}`, { method: "POST", body: JSON.stringify(result) });
642
+ if (!reported.ok)
643
+ throw new Error("BRIDGE_RESULT_REJECTED");
644
+ }
645
+ }
646
+ catch {
647
+ await sleep(1_000);
648
+ }
649
+ }
650
+ }
651
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
652
+ if (message.type === "PROFLOW_CONTENT_OBSERVATION" &&
653
+ message.observation &&
654
+ sender.tab?.id !== undefined &&
655
+ sender.tab.windowId !== undefined) {
656
+ const observed = {
657
+ ...message.observation,
658
+ tabId: sender.tab.id,
659
+ windowId: sender.tab.windowId,
660
+ };
661
+ sessions.set(sender.tab.id, observed);
662
+ void persistSnapshot();
663
+ if (observed.pageState === "IDLE")
664
+ void runObserverRecovery();
665
+ sendResponse({ accepted: true, extensionInstanceId });
666
+ return;
667
+ }
668
+ if (message.type === "PROFLOW_SIDE_PANEL_SNAPSHOT") {
669
+ void Promise.all([
670
+ taskApplicationConfig(),
671
+ approvalApplicationConfig(),
672
+ loadSystemObserverState().catch(() => null),
673
+ ]).then(([application, approval, observerState]) => sendResponse({
674
+ extensionInstanceId,
675
+ observedAt: new Date().toISOString(),
676
+ sessions: [...sessions.values()],
677
+ taskApplicationConfigured: application !== null,
678
+ approvalApplicationConfigured: approval !== null,
679
+ systemObserver: observerState
680
+ ? {
681
+ assessmentRef: observerState.assessmentRef,
682
+ observedAt: observerState.observedAt,
683
+ unresolved: observerState.unresolved,
684
+ carryForward: observerState.carryForward,
685
+ needsHumanAttention: observerState.unresolved.length > 0 ||
686
+ observerState.carryForward.length > 0,
687
+ }
688
+ : null,
689
+ }));
690
+ return true;
691
+ }
692
+ if (message.type === "PROFLOW_APPROVAL_APPLICATION") {
693
+ if (typeof message.operation !== "string" || !message.input) {
694
+ sendResponse({
695
+ ok: false,
696
+ error: "APPROVAL_APPLICATION_MESSAGE_INVALID",
697
+ });
698
+ return;
699
+ }
700
+ void invokeApprovalApplication(message.operation, message.input).then((value) => {
701
+ if ((message.operation === "approval.allow" ||
702
+ message.operation === "approval.deny" ||
703
+ message.operation === "approval.revoke") &&
704
+ isRecord(value) &&
705
+ typeof value.approvalRef === "string" &&
706
+ typeof value.taskId === "string" &&
707
+ typeof value.workerRef === "string")
708
+ void taskObserver
709
+ .drive(value.taskId, {
710
+ trigger: "RECOVERY_RESUME",
711
+ ref: value.approvalRef,
712
+ targetWorkerRef: value.workerRef,
713
+ })
714
+ .catch(() => undefined);
715
+ sendResponse({ ok: true, value });
716
+ }, (error) => sendResponse({
717
+ ok: false,
718
+ error: error instanceof Error
719
+ ? error.message
720
+ : "APPROVAL_APPLICATION_FAILED",
721
+ }));
722
+ return true;
723
+ }
724
+ if (message.type === "PROFLOW_TASK_APPLICATION") {
725
+ if (typeof message.operation !== "string" || !message.input) {
726
+ sendResponse({ ok: false, error: "TASK_APPLICATION_MESSAGE_INVALID" });
727
+ return;
728
+ }
729
+ void invokeTaskApplication(message.operation, message.input).then((value) => {
730
+ void runObserverRecovery();
731
+ sendResponse({ ok: true, value });
732
+ }, (error) => sendResponse({
733
+ ok: false,
734
+ error: error instanceof Error ? error.message : "TASK_APPLICATION_FAILED",
735
+ }));
736
+ return true;
737
+ }
738
+ });
739
+ chrome.runtime.onInstalled.addListener(() => {
740
+ void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
741
+ void runBridgeLoop();
742
+ void runObserverRecovery();
743
+ });
744
+ chrome.runtime.onStartup.addListener(() => {
745
+ sessions.clear();
746
+ void persistSnapshot();
747
+ void runBridgeLoop();
748
+ void runObserverRecovery();
749
+ });
750
+ void persistSnapshot();
751
+ void runBridgeLoop();
752
+ void runObserverRecovery();