@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 +13 -0
- package/dist/deployment/adapter.d.ts +10 -10
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/extension/background.js +71 -17
- package/dist/extension/tasks.js +58 -1
- package/dist/src/bridge.js +16 -1
- package/dist/src/carrier-identity.d.ts +21 -0
- package/dist/src/carrier-identity.js +68 -0
- package/dist/src/index.js +8 -20
- package/extension/background.ts +19 -26
- package/extension/tasks.ts +64 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/proflow.module.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
183
|
+
readonly moduleVersion: "0.1.41";
|
|
184
184
|
} | {
|
|
185
185
|
contract: "deployment.result.v1";
|
|
186
186
|
moduleRef: "execution-browser-extension";
|
|
187
|
-
moduleVersion: "0.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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")
|
package/dist/extension/tasks.js
CHANGED
|
@@ -1,4 +1,60 @@
|
|
|
1
|
-
|
|
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 {};
|
package/dist/src/bridge.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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)))
|
package/extension/background.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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")
|
package/extension/tasks.ts
CHANGED
|
@@ -1,4 +1,67 @@
|
|
|
1
|
-
import {
|
|
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
package/package.json
CHANGED
package/proflow.module.json
CHANGED
|
@@ -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.
|
|
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",
|