@tomflow/proflow-execution-browser-extension 0.1.38 → 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 +2 -2
- 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 +67 -17
- package/dist/extension/tasks.js +58 -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/dist/src/pairing.js +13 -0
- package/extension/background.ts +13 -25
- 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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @tomflow/proflow-execution-browser-extension
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.40
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
183
|
+
readonly moduleVersion: "0.1.40";
|
|
184
184
|
} | {
|
|
185
185
|
contract: "deployment.result.v1";
|
|
186
186
|
moduleRef: "execution-browser-extension";
|
|
187
|
-
moduleVersion: "0.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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)
|
|
@@ -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
|
-
|
|
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")
|
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 {};
|
|
@@ -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/dist/src/pairing.js
CHANGED
|
@@ -110,6 +110,19 @@ export async function createBrowserExtensionPairingServer(options) {
|
|
|
110
110
|
identity.extensionInstanceId) {
|
|
111
111
|
throw new Error("PAIRING_AUTH_INVALID");
|
|
112
112
|
}
|
|
113
|
+
if (request.method === "POST" &&
|
|
114
|
+
url.pathname === "/v1/carrier/attentions") {
|
|
115
|
+
// The production bridge consumes Carrier Attention state immediately after
|
|
116
|
+
// hello. Pairing only needs to absorb that authenticated snapshot so the
|
|
117
|
+
// extension can reach its first poll; only heartbeat proves paired=true.
|
|
118
|
+
if (!originId)
|
|
119
|
+
throw new Error("PAIRING_AUTH_INVALID");
|
|
120
|
+
const body = await readJson(request);
|
|
121
|
+
if (!Array.isArray(body.carrierAttentions))
|
|
122
|
+
throw new Error("PAIRING_INPUT_INVALID");
|
|
123
|
+
send(response, 200, { accepted: true });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
113
126
|
if (request.method === "POST" &&
|
|
114
127
|
url.pathname === "/v1/session/heartbeat") {
|
|
115
128
|
paired = { ...identity };
|
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,
|
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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")
|
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.40",
|
|
7
7
|
"kind": "browser-extension",
|
|
8
8
|
"templateVersion": "1.0.0",
|
|
9
9
|
"platformCompatibility": ">=1.0.0 <2.0.0",
|