@tomflow/proflow-execution-browser-extension 0.1.36 → 0.1.38
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 +7 -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 +687 -19
- package/dist/extension/content.js +340 -80
- package/dist/extension/tasks.js +74 -3
- package/dist/src/bridge.js +59 -6
- package/dist/src/carrier-attention-view.d.ts +13 -0
- package/dist/src/carrier-attention-view.js +54 -0
- package/dist/src/carrier-attention.d.ts +26 -0
- package/dist/src/carrier-attention.js +44 -0
- package/dist/src/carrier-continuation-control.d.ts +44 -0
- package/dist/src/carrier-continuation-control.js +114 -0
- package/dist/src/carrier-permission-attempt.d.ts +18 -0
- package/dist/src/carrier-permission-attempt.js +68 -0
- package/dist/src/carrier-permission-lifecycle.d.ts +34 -0
- package/dist/src/carrier-permission-lifecycle.js +73 -0
- package/dist/src/carrier-permission.d.ts +16 -0
- package/dist/src/carrier-permission.js +65 -0
- package/dist/src/chatgpt-runtime-adapter.d.ts +16 -0
- package/dist/src/chatgpt-runtime-adapter.js +173 -0
- package/dist/src/composer-submit.d.ts +12 -0
- package/dist/src/composer-submit.js +22 -0
- package/extension/background.ts +622 -17
- package/extension/content.ts +40 -39
- package/extension/tasks.html +6 -0
- package/extension/tasks.ts +93 -5
- package/manifest.json +1 -1
- package/package.json +3 -3
- package/proflow.module.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type CarrierAttentionView = {
|
|
2
|
+
attentionRef: string;
|
|
3
|
+
occurrenceRef: string;
|
|
4
|
+
taskId: string | null;
|
|
5
|
+
roleRef: string | null;
|
|
6
|
+
workerRef: string | null;
|
|
7
|
+
targetHost: string | null;
|
|
8
|
+
operationId: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
actions: Array<"allowOnce" | "deny">;
|
|
11
|
+
observedAt: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function parseCarrierAttentionViews(value: unknown): CarrierAttentionView[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function nullableString(value) {
|
|
2
|
+
return value === null || typeof value === "string";
|
|
3
|
+
}
|
|
4
|
+
function parseCarrierAttentionView(value) {
|
|
5
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
6
|
+
return null;
|
|
7
|
+
const attentionRef = Reflect.get(value, "attentionRef");
|
|
8
|
+
const occurrenceRef = Reflect.get(value, "occurrenceRef");
|
|
9
|
+
const taskId = Reflect.get(value, "taskId");
|
|
10
|
+
const roleRef = Reflect.get(value, "roleRef");
|
|
11
|
+
const workerRef = Reflect.get(value, "workerRef");
|
|
12
|
+
const targetHost = Reflect.get(value, "targetHost");
|
|
13
|
+
const operationId = Reflect.get(value, "operationId");
|
|
14
|
+
const reason = Reflect.get(value, "reason");
|
|
15
|
+
const actions = Reflect.get(value, "actions");
|
|
16
|
+
const observedAt = Reflect.get(value, "observedAt");
|
|
17
|
+
if (typeof attentionRef !== "string" ||
|
|
18
|
+
attentionRef.length === 0 ||
|
|
19
|
+
typeof occurrenceRef !== "string" ||
|
|
20
|
+
occurrenceRef.length === 0 ||
|
|
21
|
+
!nullableString(taskId) ||
|
|
22
|
+
!nullableString(roleRef) ||
|
|
23
|
+
!nullableString(workerRef) ||
|
|
24
|
+
!nullableString(targetHost) ||
|
|
25
|
+
typeof operationId !== "string" ||
|
|
26
|
+
operationId.length === 0 ||
|
|
27
|
+
typeof reason !== "string" ||
|
|
28
|
+
reason.length === 0 ||
|
|
29
|
+
!Array.isArray(actions) ||
|
|
30
|
+
actions.some((action) => action !== "allowOnce" && action !== "deny") ||
|
|
31
|
+
typeof observedAt !== "string" ||
|
|
32
|
+
observedAt.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
return {
|
|
35
|
+
attentionRef,
|
|
36
|
+
occurrenceRef,
|
|
37
|
+
taskId,
|
|
38
|
+
roleRef,
|
|
39
|
+
workerRef,
|
|
40
|
+
targetHost,
|
|
41
|
+
operationId,
|
|
42
|
+
reason,
|
|
43
|
+
actions: [...actions],
|
|
44
|
+
observedAt,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function parseCarrierAttentionViews(value) {
|
|
48
|
+
if (!Array.isArray(value))
|
|
49
|
+
return [];
|
|
50
|
+
return value
|
|
51
|
+
.slice(0, 128)
|
|
52
|
+
.map(parseCarrierAttentionView)
|
|
53
|
+
.filter((item) => item !== null);
|
|
54
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type CarrierAttentionAction = "allowOnce" | "deny";
|
|
2
|
+
export type CarrierAttention = {
|
|
3
|
+
attentionRef: string;
|
|
4
|
+
occurrenceRef: string;
|
|
5
|
+
tabId: number;
|
|
6
|
+
contentInstanceId: string;
|
|
7
|
+
url: string;
|
|
8
|
+
permissionFingerprint: string;
|
|
9
|
+
taskId: string | null;
|
|
10
|
+
roleRef: string | null;
|
|
11
|
+
workerRef: string | null;
|
|
12
|
+
targetHost: string | null;
|
|
13
|
+
operationId: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
actions: CarrierAttentionAction[];
|
|
16
|
+
observedAt: string;
|
|
17
|
+
};
|
|
18
|
+
export type CarrierAttentionInput = Omit<CarrierAttention, "attentionRef" | "occurrenceRef">;
|
|
19
|
+
export declare function createCarrierAttentionRegistry(idFactory?: () => string): Readonly<{
|
|
20
|
+
derive(input: CarrierAttentionInput): CarrierAttention;
|
|
21
|
+
find(attentionRef: string): CarrierAttention | null;
|
|
22
|
+
current(tabId: number): CarrierAttention | null;
|
|
23
|
+
values(): CarrierAttention[];
|
|
24
|
+
delete(attentionRef: string): boolean;
|
|
25
|
+
removeTab(tabId: number): boolean;
|
|
26
|
+
}>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function createCarrierAttentionRegistry(idFactory = () => crypto.randomUUID()) {
|
|
2
|
+
const byTab = new Map();
|
|
3
|
+
return Object.freeze({
|
|
4
|
+
derive(input) {
|
|
5
|
+
const current = byTab.get(input.tabId);
|
|
6
|
+
if (current?.contentInstanceId === input.contentInstanceId &&
|
|
7
|
+
current.url === input.url &&
|
|
8
|
+
current.permissionFingerprint === input.permissionFingerprint) {
|
|
9
|
+
const refreshed = { ...current, ...input };
|
|
10
|
+
byTab.set(input.tabId, refreshed);
|
|
11
|
+
return refreshed;
|
|
12
|
+
}
|
|
13
|
+
const occurrenceRef = idFactory();
|
|
14
|
+
const attention = {
|
|
15
|
+
...input,
|
|
16
|
+
occurrenceRef,
|
|
17
|
+
attentionRef: `carrier-attention:${input.tabId}:${occurrenceRef}`,
|
|
18
|
+
};
|
|
19
|
+
byTab.set(input.tabId, attention);
|
|
20
|
+
return attention;
|
|
21
|
+
},
|
|
22
|
+
find(attentionRef) {
|
|
23
|
+
for (const attention of byTab.values())
|
|
24
|
+
if (attention.attentionRef === attentionRef)
|
|
25
|
+
return attention;
|
|
26
|
+
return null;
|
|
27
|
+
},
|
|
28
|
+
current(tabId) {
|
|
29
|
+
return byTab.get(tabId) ?? null;
|
|
30
|
+
},
|
|
31
|
+
values() {
|
|
32
|
+
return [...byTab.values()];
|
|
33
|
+
},
|
|
34
|
+
delete(attentionRef) {
|
|
35
|
+
for (const [tabId, attention] of byTab)
|
|
36
|
+
if (attention.attentionRef === attentionRef)
|
|
37
|
+
return byTab.delete(tabId);
|
|
38
|
+
return false;
|
|
39
|
+
},
|
|
40
|
+
removeTab(tabId) {
|
|
41
|
+
return byTab.delete(tabId);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type CarrierContinuationDenial = {
|
|
2
|
+
attentionRef: string;
|
|
3
|
+
tabId: number;
|
|
4
|
+
taskId: string | null;
|
|
5
|
+
roleRef: string | null;
|
|
6
|
+
workerRef: string | null;
|
|
7
|
+
url: string;
|
|
8
|
+
contentInstanceId: string;
|
|
9
|
+
permissionFingerprint: string;
|
|
10
|
+
};
|
|
11
|
+
export type CarrierRecoveryObservation = {
|
|
12
|
+
tabId: number;
|
|
13
|
+
url: string;
|
|
14
|
+
contentInstanceId?: string;
|
|
15
|
+
pageState: "IDLE" | "BUSY" | "BLOCKED" | "UNKNOWN";
|
|
16
|
+
blockerFacts?: {
|
|
17
|
+
fingerprint: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type CarrierPermissionDenialContext = {
|
|
21
|
+
tabId: number;
|
|
22
|
+
contentInstanceId: string;
|
|
23
|
+
url: string;
|
|
24
|
+
permissionFingerprint: string;
|
|
25
|
+
taskId: string | null;
|
|
26
|
+
roleRef: string | null;
|
|
27
|
+
workerRef: string | null;
|
|
28
|
+
};
|
|
29
|
+
export type CarrierDispatchDenialContext = {
|
|
30
|
+
taskId: string;
|
|
31
|
+
roleRef: string;
|
|
32
|
+
workerRef: string;
|
|
33
|
+
conversationLocator: string | null;
|
|
34
|
+
};
|
|
35
|
+
export declare function createCarrierContinuationControl(initial?: unknown): Readonly<{
|
|
36
|
+
beginDenied(denial: CarrierContinuationDenial): void;
|
|
37
|
+
cancelDenied(attentionRef: string): boolean;
|
|
38
|
+
hasMatchingPermissionDenial(context: CarrierPermissionDenialContext): boolean;
|
|
39
|
+
hasMatchingDispatchDenial(context: CarrierDispatchDenialContext): boolean;
|
|
40
|
+
consumeRecovery(previous: CarrierRecoveryObservation | undefined, current: CarrierRecoveryObservation): CarrierContinuationDenial | null;
|
|
41
|
+
suppressRecovery(previous: CarrierRecoveryObservation | undefined, current: CarrierRecoveryObservation): boolean;
|
|
42
|
+
snapshot(): CarrierContinuationDenial[];
|
|
43
|
+
load: (value: unknown) => boolean;
|
|
44
|
+
}>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
function parseDenials(value) {
|
|
2
|
+
if (value === undefined)
|
|
3
|
+
return [];
|
|
4
|
+
if (!Array.isArray(value) || value.length > 128)
|
|
5
|
+
return null;
|
|
6
|
+
const parsed = [];
|
|
7
|
+
for (const item of value) {
|
|
8
|
+
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
9
|
+
return null;
|
|
10
|
+
const attentionRef = Reflect.get(item, "attentionRef");
|
|
11
|
+
const tabId = Reflect.get(item, "tabId");
|
|
12
|
+
const taskId = Reflect.get(item, "taskId");
|
|
13
|
+
const roleRef = Reflect.get(item, "roleRef");
|
|
14
|
+
const workerRef = Reflect.get(item, "workerRef");
|
|
15
|
+
const url = Reflect.get(item, "url");
|
|
16
|
+
const contentInstanceId = Reflect.get(item, "contentInstanceId");
|
|
17
|
+
const permissionFingerprint = Reflect.get(item, "permissionFingerprint");
|
|
18
|
+
if (typeof attentionRef !== "string" ||
|
|
19
|
+
!Number.isInteger(tabId) ||
|
|
20
|
+
(taskId !== null && typeof taskId !== "string") ||
|
|
21
|
+
(roleRef !== null && typeof roleRef !== "string") ||
|
|
22
|
+
(workerRef !== null && typeof workerRef !== "string") ||
|
|
23
|
+
typeof url !== "string" ||
|
|
24
|
+
typeof contentInstanceId !== "string" ||
|
|
25
|
+
typeof permissionFingerprint !== "string")
|
|
26
|
+
return null;
|
|
27
|
+
parsed.push({
|
|
28
|
+
attentionRef,
|
|
29
|
+
tabId: tabId,
|
|
30
|
+
taskId,
|
|
31
|
+
roleRef,
|
|
32
|
+
workerRef,
|
|
33
|
+
url,
|
|
34
|
+
contentInstanceId,
|
|
35
|
+
permissionFingerprint,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
export function createCarrierContinuationControl(initial) {
|
|
41
|
+
const byTab = new Map();
|
|
42
|
+
const load = (value) => {
|
|
43
|
+
const denials = parseDenials(value);
|
|
44
|
+
byTab.clear();
|
|
45
|
+
if (!denials)
|
|
46
|
+
return false;
|
|
47
|
+
for (const denial of denials)
|
|
48
|
+
byTab.set(denial.tabId, denial);
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
51
|
+
if (initial !== undefined)
|
|
52
|
+
load(initial);
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
beginDenied(denial) {
|
|
55
|
+
byTab.set(denial.tabId, { ...denial });
|
|
56
|
+
},
|
|
57
|
+
cancelDenied(attentionRef) {
|
|
58
|
+
for (const [tabId, denial] of byTab)
|
|
59
|
+
if (denial.attentionRef === attentionRef)
|
|
60
|
+
return byTab.delete(tabId);
|
|
61
|
+
return false;
|
|
62
|
+
},
|
|
63
|
+
hasMatchingPermissionDenial(context) {
|
|
64
|
+
const denial = byTab.get(context.tabId);
|
|
65
|
+
return (denial?.contentInstanceId === context.contentInstanceId &&
|
|
66
|
+
denial.url === context.url &&
|
|
67
|
+
denial.permissionFingerprint === context.permissionFingerprint &&
|
|
68
|
+
denial.taskId === context.taskId &&
|
|
69
|
+
denial.roleRef === context.roleRef &&
|
|
70
|
+
denial.workerRef === context.workerRef);
|
|
71
|
+
},
|
|
72
|
+
hasMatchingDispatchDenial(context) {
|
|
73
|
+
if (context.conversationLocator === null)
|
|
74
|
+
return false;
|
|
75
|
+
for (const denial of byTab.values())
|
|
76
|
+
if (denial.taskId === context.taskId &&
|
|
77
|
+
denial.roleRef === context.roleRef &&
|
|
78
|
+
denial.workerRef === context.workerRef &&
|
|
79
|
+
denial.url === context.conversationLocator)
|
|
80
|
+
return true;
|
|
81
|
+
return false;
|
|
82
|
+
},
|
|
83
|
+
consumeRecovery(previous, current) {
|
|
84
|
+
const denial = byTab.get(current.tabId);
|
|
85
|
+
if (!denial)
|
|
86
|
+
return null;
|
|
87
|
+
if (denial.url !== current.url ||
|
|
88
|
+
denial.contentInstanceId !== current.contentInstanceId) {
|
|
89
|
+
byTab.delete(current.tabId);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (current.pageState !== "IDLE")
|
|
93
|
+
return null;
|
|
94
|
+
if (previous &&
|
|
95
|
+
(previous.tabId !== denial.tabId ||
|
|
96
|
+
previous.url !== denial.url ||
|
|
97
|
+
previous.contentInstanceId !== denial.contentInstanceId ||
|
|
98
|
+
previous.pageState !== "BLOCKED" ||
|
|
99
|
+
previous.blockerFacts?.fingerprint !== denial.permissionFingerprint)) {
|
|
100
|
+
byTab.delete(current.tabId);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
byTab.delete(current.tabId);
|
|
104
|
+
return { ...denial };
|
|
105
|
+
},
|
|
106
|
+
suppressRecovery(previous, current) {
|
|
107
|
+
return this.consumeRecovery(previous, current) !== null;
|
|
108
|
+
},
|
|
109
|
+
snapshot() {
|
|
110
|
+
return [...byTab.values()].map((denial) => ({ ...denial }));
|
|
111
|
+
},
|
|
112
|
+
load,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type CarrierPermissionAttemptSnapshot = ReadonlyArray<{
|
|
2
|
+
tabId: number;
|
|
3
|
+
key: string;
|
|
4
|
+
}>;
|
|
5
|
+
/**
|
|
6
|
+
* Tracks only automatic actions whose post-click reality is not yet known.
|
|
7
|
+
* A confirmed release removes the entry, so a later identical permission is a
|
|
8
|
+
* new occurrence; an MV3 service-worker restart reloads the bounded snapshot
|
|
9
|
+
* and therefore cannot blindly repeat an uncertain click.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createCarrierPermissionAttemptRegistry(initial?: unknown): Readonly<{
|
|
12
|
+
has(tabId: number, key: string): boolean;
|
|
13
|
+
begin(tabId: number, key: string): void;
|
|
14
|
+
release(tabId: number, key: string): boolean;
|
|
15
|
+
observe(tabId: number, currentKey: string | null): boolean;
|
|
16
|
+
load(value: unknown): boolean;
|
|
17
|
+
snapshot(): CarrierPermissionAttemptSnapshot;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function parsedAttempts(value) {
|
|
2
|
+
const result = new Map();
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
return { attempts: result, valid: true };
|
|
5
|
+
if (!Array.isArray(value))
|
|
6
|
+
return { attempts: result, valid: false };
|
|
7
|
+
let valid = value.length <= 256;
|
|
8
|
+
for (const candidate of value.slice(0, 256)) {
|
|
9
|
+
if (typeof candidate !== "object" ||
|
|
10
|
+
candidate === null ||
|
|
11
|
+
Array.isArray(candidate)) {
|
|
12
|
+
valid = false;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const tabId = Reflect.get(candidate, "tabId");
|
|
16
|
+
const key = Reflect.get(candidate, "key");
|
|
17
|
+
if (!Number.isInteger(tabId) ||
|
|
18
|
+
tabId < 0 ||
|
|
19
|
+
typeof key !== "string" ||
|
|
20
|
+
key.length === 0 ||
|
|
21
|
+
key.length > 1_000) {
|
|
22
|
+
valid = false;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (result.has(tabId))
|
|
26
|
+
valid = false;
|
|
27
|
+
result.set(tabId, key);
|
|
28
|
+
}
|
|
29
|
+
return { attempts: result, valid };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Tracks only automatic actions whose post-click reality is not yet known.
|
|
33
|
+
* A confirmed release removes the entry, so a later identical permission is a
|
|
34
|
+
* new occurrence; an MV3 service-worker restart reloads the bounded snapshot
|
|
35
|
+
* and therefore cannot blindly repeat an uncertain click.
|
|
36
|
+
*/
|
|
37
|
+
export function createCarrierPermissionAttemptRegistry(initial) {
|
|
38
|
+
const attempts = parsedAttempts(initial).attempts;
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
has(tabId, key) {
|
|
41
|
+
return attempts.get(tabId) === key;
|
|
42
|
+
},
|
|
43
|
+
begin(tabId, key) {
|
|
44
|
+
attempts.set(tabId, key);
|
|
45
|
+
},
|
|
46
|
+
release(tabId, key) {
|
|
47
|
+
if (attempts.get(tabId) !== key)
|
|
48
|
+
return false;
|
|
49
|
+
return attempts.delete(tabId);
|
|
50
|
+
},
|
|
51
|
+
observe(tabId, currentKey) {
|
|
52
|
+
const attempted = attempts.get(tabId);
|
|
53
|
+
if (attempted === undefined || attempted === currentKey)
|
|
54
|
+
return false;
|
|
55
|
+
return attempts.delete(tabId);
|
|
56
|
+
},
|
|
57
|
+
load(value) {
|
|
58
|
+
const parsed = parsedAttempts(value);
|
|
59
|
+
attempts.clear();
|
|
60
|
+
for (const [tabId, key] of parsed.attempts)
|
|
61
|
+
attempts.set(tabId, key);
|
|
62
|
+
return parsed.valid;
|
|
63
|
+
},
|
|
64
|
+
snapshot() {
|
|
65
|
+
return [...attempts].map(([tabId, key]) => ({ tabId, key }));
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ActionPermissionFacts, PermissionSemanticAction } from "./carrier-permission.ts";
|
|
2
|
+
export type CarrierPermissionDecision = {
|
|
3
|
+
decision: "AUTO_ALLOW" | "DEFER" | "HUMAN_REQUIRED";
|
|
4
|
+
reason: string;
|
|
5
|
+
};
|
|
6
|
+
export type CarrierPermissionLifecycleResult = {
|
|
7
|
+
status: "RELEASED";
|
|
8
|
+
action: "allowAlways";
|
|
9
|
+
} | {
|
|
10
|
+
status: "HUMAN_REQUIRED";
|
|
11
|
+
reason: string;
|
|
12
|
+
} | {
|
|
13
|
+
status: "STALE";
|
|
14
|
+
};
|
|
15
|
+
export type CarrierPermissionLifecyclePort = {
|
|
16
|
+
classify(): Promise<CarrierPermissionDecision>;
|
|
17
|
+
revalidate(): boolean | Promise<boolean>;
|
|
18
|
+
waitBeforeReclassify?(): Promise<void>;
|
|
19
|
+
act(action: PermissionSemanticAction): Promise<void>;
|
|
20
|
+
released(): Promise<boolean>;
|
|
21
|
+
};
|
|
22
|
+
export declare function resolveRoutineCarrierPermission(input: {
|
|
23
|
+
facts: ActionPermissionFacts;
|
|
24
|
+
autoAlreadyAttempted: boolean;
|
|
25
|
+
maxClassifications?: number;
|
|
26
|
+
humanDenied?: () => boolean;
|
|
27
|
+
port: CarrierPermissionLifecyclePort;
|
|
28
|
+
}): Promise<CarrierPermissionLifecycleResult>;
|
|
29
|
+
export declare function resolveHumanCarrierPermission(input: {
|
|
30
|
+
action: "allowOnce" | "deny";
|
|
31
|
+
revalidate(): boolean | Promise<boolean>;
|
|
32
|
+
act(action: "allowOnce" | "deny"): Promise<void>;
|
|
33
|
+
released(): Promise<boolean>;
|
|
34
|
+
}): Promise<"RELEASED">;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export async function resolveRoutineCarrierPermission(input) {
|
|
2
|
+
const denied = () => input.humanDenied?.()
|
|
3
|
+
? { status: "HUMAN_REQUIRED", reason: "HUMAN_DENIED" }
|
|
4
|
+
: null;
|
|
5
|
+
const maxClassifications = Math.max(1, input.maxClassifications ?? 40);
|
|
6
|
+
let decision = null;
|
|
7
|
+
for (let attempt = 0; attempt < maxClassifications; attempt += 1) {
|
|
8
|
+
const beforeClassification = denied();
|
|
9
|
+
if (beforeClassification)
|
|
10
|
+
return beforeClassification;
|
|
11
|
+
try {
|
|
12
|
+
decision = await input.port.classify();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {
|
|
16
|
+
status: "HUMAN_REQUIRED",
|
|
17
|
+
reason: "PERMISSION_CLASSIFICATION_FAILED",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const afterClassification = denied();
|
|
21
|
+
if (afterClassification)
|
|
22
|
+
return afterClassification;
|
|
23
|
+
if (decision.decision !== "DEFER")
|
|
24
|
+
break;
|
|
25
|
+
if (!(await input.port.revalidate()))
|
|
26
|
+
return { status: "STALE" };
|
|
27
|
+
if (attempt === maxClassifications - 1)
|
|
28
|
+
return {
|
|
29
|
+
status: "HUMAN_REQUIRED",
|
|
30
|
+
reason: "PERMISSION_CONTEXT_DEFER_TIMEOUT",
|
|
31
|
+
};
|
|
32
|
+
await input.port.waitBeforeReclassify?.();
|
|
33
|
+
}
|
|
34
|
+
if (!decision)
|
|
35
|
+
return {
|
|
36
|
+
status: "HUMAN_REQUIRED",
|
|
37
|
+
reason: "PERMISSION_CLASSIFICATION_FAILED",
|
|
38
|
+
};
|
|
39
|
+
if (decision.decision !== "AUTO_ALLOW")
|
|
40
|
+
return { status: "HUMAN_REQUIRED", reason: decision.reason };
|
|
41
|
+
if (!input.facts.actions.includes("allowAlways"))
|
|
42
|
+
return {
|
|
43
|
+
status: "HUMAN_REQUIRED",
|
|
44
|
+
reason: "AUTO_ALLOW_ACTION_UNAVAILABLE",
|
|
45
|
+
};
|
|
46
|
+
if (input.autoAlreadyAttempted)
|
|
47
|
+
return {
|
|
48
|
+
status: "HUMAN_REQUIRED",
|
|
49
|
+
reason: "AUTO_ALLOW_REALITY_UNCONFIRMED",
|
|
50
|
+
};
|
|
51
|
+
if (!(await input.port.revalidate()))
|
|
52
|
+
return { status: "STALE" };
|
|
53
|
+
const beforeAction = denied();
|
|
54
|
+
if (beforeAction)
|
|
55
|
+
return beforeAction;
|
|
56
|
+
try {
|
|
57
|
+
await input.port.act("allowAlways");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { status: "HUMAN_REQUIRED", reason: "AUTO_ALLOW_FAILED" };
|
|
61
|
+
}
|
|
62
|
+
return (await input.port.released())
|
|
63
|
+
? { status: "RELEASED", action: "allowAlways" }
|
|
64
|
+
: { status: "HUMAN_REQUIRED", reason: "AUTO_ALLOW_REALITY_UNCONFIRMED" };
|
|
65
|
+
}
|
|
66
|
+
export async function resolveHumanCarrierPermission(input) {
|
|
67
|
+
if (!(await input.revalidate()))
|
|
68
|
+
throw new Error("STALE_PERMISSION");
|
|
69
|
+
await input.act(input.action);
|
|
70
|
+
if (!(await input.released()))
|
|
71
|
+
throw new Error("PERMISSION_ACTION_REALITY_UNCONFIRMED");
|
|
72
|
+
return "RELEASED";
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type PermissionSemanticAction = "allowAlways" | "allowOnce" | "deny";
|
|
2
|
+
export type ActionPermissionCandidate = {
|
|
3
|
+
text: string;
|
|
4
|
+
buttonLabels: readonly string[];
|
|
5
|
+
};
|
|
6
|
+
export type ActionPermissionFacts = {
|
|
7
|
+
kind: "ACTION_PERMISSION";
|
|
8
|
+
targetHost: string | null;
|
|
9
|
+
operationId: string;
|
|
10
|
+
taskId: string | null;
|
|
11
|
+
actions: PermissionSemanticAction[];
|
|
12
|
+
fingerprint: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function permissionSemanticAction(label: string): PermissionSemanticAction | null;
|
|
15
|
+
export declare function detectActionPermission(candidates: readonly ActionPermissionCandidate[]): ActionPermissionFacts | null;
|
|
16
|
+
export declare function permissionActionAllowed(facts: ActionPermissionFacts, expectedFingerprint: string, action: string): action is PermissionSemanticAction;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const actionLabels = [
|
|
2
|
+
[/^(始终允许|always allow)$/i, "allowAlways"],
|
|
3
|
+
[/^(允许一次|allow once)$/i, "allowOnce"],
|
|
4
|
+
[/^(拒绝|deny)$/i, "deny"],
|
|
5
|
+
];
|
|
6
|
+
export function permissionSemanticAction(label) {
|
|
7
|
+
const normalized = label.trim();
|
|
8
|
+
for (const [pattern, action] of actionLabels)
|
|
9
|
+
if (pattern.test(normalized))
|
|
10
|
+
return action;
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function normalizedText(value) {
|
|
14
|
+
return value.replace(/\s+/g, " ").trim().slice(0, 4_096);
|
|
15
|
+
}
|
|
16
|
+
function hashFingerprint(value) {
|
|
17
|
+
let hash = 0x811c9dc5;
|
|
18
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
19
|
+
hash ^= value.charCodeAt(index);
|
|
20
|
+
hash = Math.imul(hash, 0x01000193);
|
|
21
|
+
}
|
|
22
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
23
|
+
}
|
|
24
|
+
function targetHost(text) {
|
|
25
|
+
const url = text.match(/https?:\/\/([a-z0-9.-]+)(?=[/:\s"'”]|$)/i)?.[1];
|
|
26
|
+
if (url)
|
|
27
|
+
return url.toLowerCase();
|
|
28
|
+
const hosts = text.match(/[a-z0-9][a-z0-9-]*(?:\.[a-z0-9-]+){2,}/gi) ?? [];
|
|
29
|
+
return (hosts.find((value) => value.includes("devtunnels.ms"))?.toLowerCase() ??
|
|
30
|
+
hosts[0]?.toLowerCase() ??
|
|
31
|
+
null);
|
|
32
|
+
}
|
|
33
|
+
function operationId(text) {
|
|
34
|
+
return (text.match(/(?:工具调用|tool call)\s*[::]\s*[^\s.]+(?:\.[^\s.]+)*\.([A-Za-z][A-Za-z0-9_]*)/i)?.[1] ?? null);
|
|
35
|
+
}
|
|
36
|
+
function taskId(text) {
|
|
37
|
+
return text.match(/\btask-[A-Za-z0-9-]+\b/)?.[0] ?? null;
|
|
38
|
+
}
|
|
39
|
+
export function detectActionPermission(candidates) {
|
|
40
|
+
for (const candidate of candidates) {
|
|
41
|
+
const actions = candidate.buttonLabels
|
|
42
|
+
.map(permissionSemanticAction)
|
|
43
|
+
.filter((value) => value !== null);
|
|
44
|
+
if (!actions.includes("deny") ||
|
|
45
|
+
(!actions.includes("allowAlways") && !actions.includes("allowOnce")))
|
|
46
|
+
continue;
|
|
47
|
+
const text = normalizedText(candidate.text);
|
|
48
|
+
const operation = operationId(text);
|
|
49
|
+
if (!operation)
|
|
50
|
+
continue;
|
|
51
|
+
return {
|
|
52
|
+
kind: "ACTION_PERMISSION",
|
|
53
|
+
targetHost: targetHost(text),
|
|
54
|
+
operationId: operation,
|
|
55
|
+
taskId: taskId(text),
|
|
56
|
+
actions,
|
|
57
|
+
fingerprint: `permission:v1:${hashFingerprint(`${text}|${actions.join(",")}`)}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
export function permissionActionAllowed(facts, expectedFingerprint, action) {
|
|
63
|
+
return (facts.fingerprint === expectedFingerprint &&
|
|
64
|
+
facts.actions.includes(action));
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ActionPermissionFacts, type PermissionSemanticAction } from "./carrier-permission.ts";
|
|
2
|
+
export type ChatGptPageReality = {
|
|
3
|
+
pageState: "IDLE" | "BUSY" | "BLOCKED" | "UNKNOWN";
|
|
4
|
+
activityKind: "GENERATING" | "ACTION_PERMISSION" | "ACTION_RUNNING" | "WAITING_HUMAN" | null;
|
|
5
|
+
blockerFacts?: ActionPermissionFacts;
|
|
6
|
+
};
|
|
7
|
+
export declare function classifyChatGptPageSignals(input: {
|
|
8
|
+
permission: ActionPermissionFacts | null;
|
|
9
|
+
hasDialog: boolean;
|
|
10
|
+
isGenerating: boolean;
|
|
11
|
+
hasComposer: boolean;
|
|
12
|
+
}): ChatGptPageReality;
|
|
13
|
+
export declare function observeChatGptPage(document: Document): ChatGptPageReality;
|
|
14
|
+
export declare function performChatGptPermissionAction(document: Document, expectedFingerprint: string, action: PermissionSemanticAction): ActionPermissionFacts;
|
|
15
|
+
export declare function submitChatGptComposer(document: Document, value: string): Promise<void>;
|
|
16
|
+
export declare function writeChatGptInput(document: Document, selector: string, value: string): void;
|