@tomflow/proflow-execution-browser-extension 0.1.12 → 0.1.14
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 +12 -0
- package/DOCS.md +12 -4
- package/SETUP.md +12 -16
- package/dist/deployment/adapter.d.ts +34 -85
- package/dist/deployment/adapter.js +119 -138
- package/dist/deployment/descriptor.d.ts +4 -1
- package/dist/deployment/descriptor.js +5 -1
- package/dist/extension/background.js +189 -0
- package/dist/extension/provisioning-content.d.ts +1 -0
- package/dist/extension/provisioning-content.js +936 -0
- package/dist/src/custom-gpt-editor-driver.d.ts +51 -0
- package/dist/src/custom-gpt-editor-driver.js +116 -0
- package/dist/src/custom-gpt-knowledge.d.ts +21 -0
- package/dist/src/custom-gpt-knowledge.js +148 -0
- package/dist/src/custom-gpt-provisioner.d.ts +78 -0
- package/dist/src/custom-gpt-provisioner.js +225 -0
- package/dist/src/custom-gpt-role.d.ts +66 -0
- package/dist/src/custom-gpt-role.js +98 -0
- package/dist/src/install-workflow.d.ts +29 -0
- package/dist/src/install-workflow.js +64 -0
- package/dist/src/pairing.d.ts +20 -0
- package/dist/src/pairing.js +176 -0
- package/dist/src/provisioning-bridge.d.ts +46 -0
- package/dist/src/provisioning-bridge.js +314 -0
- package/extension/background.ts +264 -3
- package/extension/provisioning-content.ts +1033 -0
- package/manifest.json +9 -1
- package/package.json +7 -9
- package/proflow.module.json +5 -1
- package/dist/src/configure.d.ts +0 -4
- package/dist/src/configure.js +0 -141
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type CustomGptCapability = "webSearch" | "imageGeneration" | "codeInterpreter";
|
|
2
|
+
export type CustomGptKnowledgeFile = {
|
|
3
|
+
name: string;
|
|
4
|
+
mime: string;
|
|
5
|
+
sizeBytes: number;
|
|
6
|
+
sha256: string;
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
export type CustomGptProvisioningRequest = {
|
|
10
|
+
packageName: string;
|
|
11
|
+
version: string;
|
|
12
|
+
displayName: string;
|
|
13
|
+
description: string;
|
|
14
|
+
instructions: string;
|
|
15
|
+
conversationStarters: string[];
|
|
16
|
+
recommendedModel: string;
|
|
17
|
+
capabilities: Record<CustomGptCapability, boolean>;
|
|
18
|
+
knowledgeBundle: string;
|
|
19
|
+
actionSchema: string;
|
|
20
|
+
knowledgeFiles: CustomGptKnowledgeFile[];
|
|
21
|
+
bearerCredential?: string;
|
|
22
|
+
};
|
|
23
|
+
export interface CustomGptEditorPort {
|
|
24
|
+
setTextField(field: "displayName" | "description" | "instructions", value: string): Promise<void>;
|
|
25
|
+
replaceConversationStarters(values: readonly string[]): Promise<void>;
|
|
26
|
+
selectRecommendedModel(value: string): Promise<void>;
|
|
27
|
+
setCapability(capability: CustomGptCapability, enabled: boolean): Promise<void>;
|
|
28
|
+
installActionSchema(value: string): Promise<void>;
|
|
29
|
+
configureBearerAuth(value: string): Promise<void>;
|
|
30
|
+
uploadKnowledge(files: readonly CustomGptKnowledgeFile[]): Promise<void>;
|
|
31
|
+
verifyReady(material: CustomGptProvisioningRequest): Promise<void>;
|
|
32
|
+
createPrivate(): Promise<{
|
|
33
|
+
gptId: string;
|
|
34
|
+
carrierUrl: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export declare function parseCustomGptProvisioningRequest(input: unknown): CustomGptProvisioningRequest;
|
|
38
|
+
export declare function createCustomGptEditorDriver(port: CustomGptEditorPort): Readonly<{
|
|
39
|
+
configureDraft: (material: CustomGptProvisioningRequest) => Promise<{
|
|
40
|
+
status: "DRAFT_CONFIGURED";
|
|
41
|
+
packageName: string;
|
|
42
|
+
version: string;
|
|
43
|
+
}>;
|
|
44
|
+
provision(material: CustomGptProvisioningRequest): Promise<{
|
|
45
|
+
gptId: string;
|
|
46
|
+
carrierUrl: string;
|
|
47
|
+
status: "LIVE_CREATED";
|
|
48
|
+
packageName: string;
|
|
49
|
+
version: string;
|
|
50
|
+
}>;
|
|
51
|
+
}>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
function record(value) {
|
|
2
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
3
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
function requiredString(value) {
|
|
7
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
8
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function stringArray(value) {
|
|
12
|
+
if (!Array.isArray(value) ||
|
|
13
|
+
value.length === 0 ||
|
|
14
|
+
!value.every((item) => typeof item === "string" && item.trim().length > 0))
|
|
15
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
16
|
+
return [...value];
|
|
17
|
+
}
|
|
18
|
+
function knowledgeFiles(value) {
|
|
19
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 64)
|
|
20
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
21
|
+
return value.map((candidate) => {
|
|
22
|
+
const file = record(candidate);
|
|
23
|
+
const name = requiredString(file.name);
|
|
24
|
+
const mime = requiredString(file.mime);
|
|
25
|
+
const sha256 = requiredString(file.sha256);
|
|
26
|
+
const url = requiredString(file.url);
|
|
27
|
+
const sizeBytes = file.sizeBytes;
|
|
28
|
+
if (!Number.isInteger(sizeBytes) ||
|
|
29
|
+
Number(sizeBytes) <= 0 ||
|
|
30
|
+
!/^sha256:[0-9a-f]{64}$/.test(sha256))
|
|
31
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = new URL(url);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
38
|
+
}
|
|
39
|
+
if (parsed.protocol !== "http:" ||
|
|
40
|
+
parsed.hostname !== "127.0.0.1" ||
|
|
41
|
+
!parsed.pathname.startsWith("/v1/provisioning/files/") ||
|
|
42
|
+
parsed.username !== "" ||
|
|
43
|
+
parsed.password !== "")
|
|
44
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
45
|
+
return { name, mime, sizeBytes: Number(sizeBytes), sha256, url };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function parseCustomGptProvisioningRequest(input) {
|
|
49
|
+
const source = record(input);
|
|
50
|
+
const capabilities = record(source.capabilities);
|
|
51
|
+
for (const name of [
|
|
52
|
+
"webSearch",
|
|
53
|
+
"imageGeneration",
|
|
54
|
+
"codeInterpreter",
|
|
55
|
+
])
|
|
56
|
+
if (typeof capabilities[name] !== "boolean")
|
|
57
|
+
throw new TypeError("PROVISIONING_REQUEST_INVALID");
|
|
58
|
+
return {
|
|
59
|
+
packageName: requiredString(source.packageName),
|
|
60
|
+
version: requiredString(source.version),
|
|
61
|
+
displayName: requiredString(source.displayName),
|
|
62
|
+
description: requiredString(source.description),
|
|
63
|
+
instructions: requiredString(source.instructions),
|
|
64
|
+
conversationStarters: stringArray(source.conversationStarters),
|
|
65
|
+
recommendedModel: requiredString(source.recommendedModel),
|
|
66
|
+
capabilities: {
|
|
67
|
+
webSearch: capabilities.webSearch,
|
|
68
|
+
imageGeneration: capabilities.imageGeneration,
|
|
69
|
+
codeInterpreter: capabilities.codeInterpreter,
|
|
70
|
+
},
|
|
71
|
+
knowledgeBundle: requiredString(source.knowledgeBundle),
|
|
72
|
+
actionSchema: requiredString(source.actionSchema),
|
|
73
|
+
knowledgeFiles: knowledgeFiles(source.knowledgeFiles),
|
|
74
|
+
...(source.bearerCredential === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { bearerCredential: requiredString(source.bearerCredential) }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function createCustomGptEditorDriver(port) {
|
|
80
|
+
const configureDraft = async (material) => {
|
|
81
|
+
await port.setTextField("displayName", material.displayName);
|
|
82
|
+
await port.setTextField("description", material.description);
|
|
83
|
+
await port.setTextField("instructions", material.instructions);
|
|
84
|
+
await port.replaceConversationStarters(material.conversationStarters);
|
|
85
|
+
await port.selectRecommendedModel(material.recommendedModel);
|
|
86
|
+
for (const capability of [
|
|
87
|
+
"webSearch",
|
|
88
|
+
"imageGeneration",
|
|
89
|
+
"codeInterpreter",
|
|
90
|
+
])
|
|
91
|
+
await port.setCapability(capability, material.capabilities[capability]);
|
|
92
|
+
await port.installActionSchema(material.actionSchema);
|
|
93
|
+
if (material.bearerCredential)
|
|
94
|
+
await port.configureBearerAuth(material.bearerCredential);
|
|
95
|
+
return {
|
|
96
|
+
status: "DRAFT_CONFIGURED",
|
|
97
|
+
packageName: material.packageName,
|
|
98
|
+
version: material.version,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
configureDraft,
|
|
103
|
+
async provision(material) {
|
|
104
|
+
await configureDraft(material);
|
|
105
|
+
await port.uploadKnowledge(material.knowledgeFiles);
|
|
106
|
+
await port.verifyReady(material);
|
|
107
|
+
const live = await port.createPrivate();
|
|
108
|
+
return {
|
|
109
|
+
status: "LIVE_CREATED",
|
|
110
|
+
packageName: material.packageName,
|
|
111
|
+
version: material.version,
|
|
112
|
+
...live,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type MaterializedKnowledgeFile = {
|
|
2
|
+
name: string;
|
|
3
|
+
relativePath: string;
|
|
4
|
+
path: string;
|
|
5
|
+
mime: string;
|
|
6
|
+
sizeBytes: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
};
|
|
9
|
+
export type MaterializedKnowledgeBundle = {
|
|
10
|
+
bundlePath: string;
|
|
11
|
+
bundleSha256: string;
|
|
12
|
+
stagingDirectory: string;
|
|
13
|
+
files: MaterializedKnowledgeFile[];
|
|
14
|
+
};
|
|
15
|
+
export declare function materializeCustomGptKnowledgeBundle(input: {
|
|
16
|
+
bundlePath: string;
|
|
17
|
+
stagingRoot: string;
|
|
18
|
+
maxEntries?: number;
|
|
19
|
+
maxEntryBytes?: number;
|
|
20
|
+
maxTotalBytes?: number;
|
|
21
|
+
}): Promise<MaterializedKnowledgeBundle>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
4
|
+
import { inflateRawSync } from "node:zlib";
|
|
5
|
+
const MIME_BY_EXTENSION = {
|
|
6
|
+
".md": "text/markdown",
|
|
7
|
+
".txt": "text/plain",
|
|
8
|
+
".pdf": "application/pdf",
|
|
9
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
10
|
+
".json": "application/json",
|
|
11
|
+
".csv": "text/csv",
|
|
12
|
+
".yaml": "text/yaml",
|
|
13
|
+
".yml": "text/yaml",
|
|
14
|
+
".js": "text/javascript",
|
|
15
|
+
".ts": "text/plain",
|
|
16
|
+
".py": "text/plain",
|
|
17
|
+
};
|
|
18
|
+
function sha256(bytes) {
|
|
19
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
20
|
+
}
|
|
21
|
+
function unsafeEntry(name) {
|
|
22
|
+
if (name.length === 0 ||
|
|
23
|
+
name.includes("\\") ||
|
|
24
|
+
name.includes("\0") ||
|
|
25
|
+
name.startsWith("/") ||
|
|
26
|
+
/^[a-zA-Z]:/.test(name))
|
|
27
|
+
return true;
|
|
28
|
+
const parts = name.split("/");
|
|
29
|
+
return parts.some((part) => part === "" || part === "." || part === "..");
|
|
30
|
+
}
|
|
31
|
+
function findEndOfCentralDirectory(buffer) {
|
|
32
|
+
const minimum = Math.max(0, buffer.length - 65_557);
|
|
33
|
+
for (let offset = buffer.length - 22; offset >= minimum; offset -= 1)
|
|
34
|
+
if (buffer.readUInt32LE(offset) === 0x06054b50)
|
|
35
|
+
return offset;
|
|
36
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
37
|
+
}
|
|
38
|
+
function parseEntries(buffer, maxEntries) {
|
|
39
|
+
const eocd = findEndOfCentralDirectory(buffer);
|
|
40
|
+
const entries = buffer.readUInt16LE(eocd + 10);
|
|
41
|
+
const centralSize = buffer.readUInt32LE(eocd + 12);
|
|
42
|
+
const centralOffset = buffer.readUInt32LE(eocd + 16);
|
|
43
|
+
if (entries === 0 ||
|
|
44
|
+
entries > maxEntries ||
|
|
45
|
+
centralOffset + centralSize > buffer.length)
|
|
46
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
47
|
+
const result = [];
|
|
48
|
+
let offset = centralOffset;
|
|
49
|
+
for (let index = 0; index < entries; index += 1) {
|
|
50
|
+
if (offset + 46 > buffer.length ||
|
|
51
|
+
buffer.readUInt32LE(offset) !== 0x02014b50)
|
|
52
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
53
|
+
const flags = buffer.readUInt16LE(offset + 8);
|
|
54
|
+
const method = buffer.readUInt16LE(offset + 10);
|
|
55
|
+
const compressedSize = buffer.readUInt32LE(offset + 20);
|
|
56
|
+
const uncompressedSize = buffer.readUInt32LE(offset + 24);
|
|
57
|
+
const nameLength = buffer.readUInt16LE(offset + 28);
|
|
58
|
+
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
59
|
+
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
60
|
+
const localHeaderOffset = buffer.readUInt32LE(offset + 42);
|
|
61
|
+
const end = offset + 46 + nameLength + extraLength + commentLength;
|
|
62
|
+
if (end > buffer.length || (flags & 0x1) !== 0)
|
|
63
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
64
|
+
const name = buffer
|
|
65
|
+
.subarray(offset + 46, offset + 46 + nameLength)
|
|
66
|
+
.toString("utf8");
|
|
67
|
+
result.push({
|
|
68
|
+
name,
|
|
69
|
+
method,
|
|
70
|
+
compressedSize,
|
|
71
|
+
uncompressedSize,
|
|
72
|
+
localHeaderOffset,
|
|
73
|
+
});
|
|
74
|
+
offset = end;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
function extractEntry(buffer, entry) {
|
|
79
|
+
const offset = entry.localHeaderOffset;
|
|
80
|
+
if (offset + 30 > buffer.length || buffer.readUInt32LE(offset) !== 0x04034b50)
|
|
81
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
82
|
+
const nameLength = buffer.readUInt16LE(offset + 26);
|
|
83
|
+
const extraLength = buffer.readUInt16LE(offset + 28);
|
|
84
|
+
const dataOffset = offset + 30 + nameLength + extraLength;
|
|
85
|
+
const dataEnd = dataOffset + entry.compressedSize;
|
|
86
|
+
if (dataEnd > buffer.length)
|
|
87
|
+
throw new Error("KNOWLEDGE_ZIP_INVALID");
|
|
88
|
+
const compressed = buffer.subarray(dataOffset, dataEnd);
|
|
89
|
+
const output = entry.method === 0
|
|
90
|
+
? Buffer.from(compressed)
|
|
91
|
+
: entry.method === 8
|
|
92
|
+
? inflateRawSync(compressed)
|
|
93
|
+
: (() => {
|
|
94
|
+
throw new Error("KNOWLEDGE_ZIP_COMPRESSION_UNSUPPORTED");
|
|
95
|
+
})();
|
|
96
|
+
if (output.length !== entry.uncompressedSize)
|
|
97
|
+
throw new Error("KNOWLEDGE_ZIP_SIZE_MISMATCH");
|
|
98
|
+
return output;
|
|
99
|
+
}
|
|
100
|
+
export async function materializeCustomGptKnowledgeBundle(input) {
|
|
101
|
+
const maxEntries = input.maxEntries ?? 64;
|
|
102
|
+
const maxEntryBytes = input.maxEntryBytes ?? 32 * 1024 * 1024;
|
|
103
|
+
const maxTotalBytes = input.maxTotalBytes ?? 128 * 1024 * 1024;
|
|
104
|
+
const bundlePath = resolve(input.bundlePath);
|
|
105
|
+
const archive = await readFile(bundlePath);
|
|
106
|
+
const bundleSha256 = sha256(archive);
|
|
107
|
+
const entries = parseEntries(archive, maxEntries);
|
|
108
|
+
let totalBytes = 0;
|
|
109
|
+
let knowledgeEntryCount = 0;
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
if (entry.name.endsWith("/"))
|
|
112
|
+
continue;
|
|
113
|
+
if (unsafeEntry(entry.name))
|
|
114
|
+
throw new Error("KNOWLEDGE_ZIP_ENTRY_UNSAFE");
|
|
115
|
+
if (entry.uncompressedSize > maxEntryBytes)
|
|
116
|
+
throw new Error("KNOWLEDGE_ZIP_ENTRY_TOO_LARGE");
|
|
117
|
+
totalBytes += entry.uncompressedSize;
|
|
118
|
+
if (totalBytes > maxTotalBytes)
|
|
119
|
+
throw new Error("KNOWLEDGE_ZIP_TOTAL_TOO_LARGE");
|
|
120
|
+
if (!MIME_BY_EXTENSION[extname(entry.name).toLowerCase()])
|
|
121
|
+
throw new Error("KNOWLEDGE_FILE_TYPE_UNSUPPORTED");
|
|
122
|
+
extractEntry(archive, entry);
|
|
123
|
+
knowledgeEntryCount += 1;
|
|
124
|
+
}
|
|
125
|
+
if (knowledgeEntryCount === 0)
|
|
126
|
+
throw new Error("KNOWLEDGE_ZIP_EMPTY");
|
|
127
|
+
const stagingDirectory = join(resolve(input.stagingRoot), bundleSha256.slice("sha256:".length, "sha256:".length + 24));
|
|
128
|
+
await rm(stagingDirectory, { recursive: true, force: true });
|
|
129
|
+
await mkdir(stagingDirectory, { recursive: true, mode: 0o700 });
|
|
130
|
+
const fileName = basename(bundlePath);
|
|
131
|
+
const stagedBundlePath = join(stagingDirectory, fileName);
|
|
132
|
+
await writeFile(stagedBundlePath, archive, { mode: 0o600 });
|
|
133
|
+
return {
|
|
134
|
+
bundlePath,
|
|
135
|
+
bundleSha256,
|
|
136
|
+
stagingDirectory,
|
|
137
|
+
files: [
|
|
138
|
+
{
|
|
139
|
+
name: fileName,
|
|
140
|
+
relativePath: fileName,
|
|
141
|
+
path: stagedBundlePath,
|
|
142
|
+
mime: "application/zip",
|
|
143
|
+
sizeBytes: archive.length,
|
|
144
|
+
sha256: bundleSha256,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { CustomGptProvisioningRequest } from "./custom-gpt-editor-driver.ts";
|
|
2
|
+
import { type CustomGptProvisioningBridgeOptions } from "./provisioning-bridge.ts";
|
|
3
|
+
export type CustomGptPackageProvisioningMaterial = Omit<CustomGptProvisioningRequest, "actionSchema" | "knowledgeFiles" | "bearerCredential"> & {
|
|
4
|
+
actionSchema: string;
|
|
5
|
+
};
|
|
6
|
+
export type CustomGptProvisioningHostOptions = CustomGptProvisioningBridgeOptions & {
|
|
7
|
+
onlineTimeoutMs?: number;
|
|
8
|
+
};
|
|
9
|
+
export type CustomGptProvisioningResult = {
|
|
10
|
+
status: "LIVE_CREATED";
|
|
11
|
+
packageName: string;
|
|
12
|
+
version: string;
|
|
13
|
+
gptId: string;
|
|
14
|
+
carrierUrl: string;
|
|
15
|
+
knowledgeBundleSha256: string;
|
|
16
|
+
knowledgeFiles: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
mime: string;
|
|
19
|
+
sizeBytes: number;
|
|
20
|
+
sha256: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
export declare function createCustomGptProvisioningHost(options: CustomGptProvisioningHostOptions): Promise<Readonly<{
|
|
24
|
+
endpoint: string;
|
|
25
|
+
status: () => {
|
|
26
|
+
online: boolean;
|
|
27
|
+
extensionInstanceId: string | null;
|
|
28
|
+
queuedCommands: number;
|
|
29
|
+
pendingCommands: number;
|
|
30
|
+
relayFiles: number;
|
|
31
|
+
};
|
|
32
|
+
provisionPackage: (input: {
|
|
33
|
+
packageRoot: string;
|
|
34
|
+
stagingRoot: string;
|
|
35
|
+
gatewayUrl: string;
|
|
36
|
+
material: CustomGptPackageProvisioningMaterial;
|
|
37
|
+
credential?: string;
|
|
38
|
+
}) => Promise<CustomGptProvisioningResult>;
|
|
39
|
+
finalizeRoleAuth: (input: {
|
|
40
|
+
carrierUrl: string;
|
|
41
|
+
credential: string;
|
|
42
|
+
}) => Promise<{
|
|
43
|
+
status: "AUTH_UPDATED";
|
|
44
|
+
gptId: string;
|
|
45
|
+
carrierUrl: string;
|
|
46
|
+
}>;
|
|
47
|
+
close: () => Promise<void>;
|
|
48
|
+
}>>;
|
|
49
|
+
export declare function createWorkspaceCustomGptProvisioningHost(input: {
|
|
50
|
+
workspaceRoot: string;
|
|
51
|
+
commandTimeoutMs?: number;
|
|
52
|
+
onlineTimeoutMs?: number;
|
|
53
|
+
}): Promise<Readonly<{
|
|
54
|
+
endpoint: string;
|
|
55
|
+
status: () => {
|
|
56
|
+
online: boolean;
|
|
57
|
+
extensionInstanceId: string | null;
|
|
58
|
+
queuedCommands: number;
|
|
59
|
+
pendingCommands: number;
|
|
60
|
+
relayFiles: number;
|
|
61
|
+
};
|
|
62
|
+
provisionPackage: (input: {
|
|
63
|
+
packageRoot: string;
|
|
64
|
+
stagingRoot: string;
|
|
65
|
+
gatewayUrl: string;
|
|
66
|
+
material: CustomGptPackageProvisioningMaterial;
|
|
67
|
+
credential?: string;
|
|
68
|
+
}) => Promise<CustomGptProvisioningResult>;
|
|
69
|
+
finalizeRoleAuth: (input: {
|
|
70
|
+
carrierUrl: string;
|
|
71
|
+
credential: string;
|
|
72
|
+
}) => Promise<{
|
|
73
|
+
status: "AUTH_UPDATED";
|
|
74
|
+
gptId: string;
|
|
75
|
+
carrierUrl: string;
|
|
76
|
+
}>;
|
|
77
|
+
close: () => Promise<void>;
|
|
78
|
+
}>>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { readModuleSharedFacts } from "@tomflow/proflow-module-contract";
|
|
4
|
+
import { materializeCustomGptKnowledgeBundle, } from "./custom-gpt-knowledge.js";
|
|
5
|
+
import { createCustomGptProvisioningBridgeServer, } from "./provisioning-bridge.js";
|
|
6
|
+
function packageAsset(packageRoot, asset, name) {
|
|
7
|
+
if (asset.length === 0 || isAbsolute(asset))
|
|
8
|
+
throw new Error(`${name}_INVALID`);
|
|
9
|
+
const root = resolve(packageRoot);
|
|
10
|
+
const path = resolve(root, asset);
|
|
11
|
+
const rel = relative(root, path);
|
|
12
|
+
if (rel === "" ||
|
|
13
|
+
rel === ".." ||
|
|
14
|
+
rel.startsWith(`..${sep}`) ||
|
|
15
|
+
isAbsolute(rel))
|
|
16
|
+
throw new Error(`${name}_OUTSIDE_PACKAGE`);
|
|
17
|
+
return path;
|
|
18
|
+
}
|
|
19
|
+
function publicGatewayUrl(value) {
|
|
20
|
+
let url;
|
|
21
|
+
try {
|
|
22
|
+
url = new URL(value);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error("GATEWAY_URL_INVALID");
|
|
26
|
+
}
|
|
27
|
+
if (url.protocol !== "https:" ||
|
|
28
|
+
url.username !== "" ||
|
|
29
|
+
url.password !== "" ||
|
|
30
|
+
url.search !== "" ||
|
|
31
|
+
url.hash !== "")
|
|
32
|
+
throw new Error("GATEWAY_URL_INVALID");
|
|
33
|
+
return url.toString().replace(/\/$/, "");
|
|
34
|
+
}
|
|
35
|
+
function hydratedSchema(schema, gatewayUrl) {
|
|
36
|
+
const gateway = publicGatewayUrl(gatewayUrl);
|
|
37
|
+
if (schema.includes("https://GATEWAY_PUBLIC_HOST"))
|
|
38
|
+
return schema.replaceAll("https://GATEWAY_PUBLIC_HOST", gateway);
|
|
39
|
+
if (!schema.includes(gateway))
|
|
40
|
+
throw new Error("ACTION_SCHEMA_GATEWAY_MISMATCH");
|
|
41
|
+
return schema;
|
|
42
|
+
}
|
|
43
|
+
function fileEvidence(files) {
|
|
44
|
+
return files.map(({ name, mime, sizeBytes, sha256 }) => ({
|
|
45
|
+
name,
|
|
46
|
+
mime,
|
|
47
|
+
sizeBytes,
|
|
48
|
+
sha256,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
function liveResult(value, material) {
|
|
52
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
53
|
+
throw new Error("PROVISIONING_RESULT_INVALID");
|
|
54
|
+
const result = value;
|
|
55
|
+
if (result.status !== "LIVE_CREATED" ||
|
|
56
|
+
result.packageName !== material.packageName ||
|
|
57
|
+
result.version !== material.version ||
|
|
58
|
+
typeof result.gptId !== "string" ||
|
|
59
|
+
!/^g-[A-Za-z0-9_-]+$/.test(result.gptId) ||
|
|
60
|
+
typeof result.carrierUrl !== "string")
|
|
61
|
+
throw new Error("PROVISIONING_RESULT_INVALID");
|
|
62
|
+
const expected = `https://chatgpt.com/g/${result.gptId}`;
|
|
63
|
+
if (result.carrierUrl !== expected)
|
|
64
|
+
throw new Error("PROVISIONING_RESULT_INVALID");
|
|
65
|
+
return {
|
|
66
|
+
status: "LIVE_CREATED",
|
|
67
|
+
packageName: material.packageName,
|
|
68
|
+
version: material.version,
|
|
69
|
+
gptId: result.gptId,
|
|
70
|
+
carrierUrl: result.carrierUrl,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function carrierGptId(carrierUrl) {
|
|
74
|
+
const url = new URL(carrierUrl);
|
|
75
|
+
const match = /^\/g\/(g-[A-Za-z0-9_-]+)$/.exec(url.pathname);
|
|
76
|
+
if (url.origin !== "https://chatgpt.com" ||
|
|
77
|
+
url.username !== "" ||
|
|
78
|
+
url.password !== "" ||
|
|
79
|
+
url.search !== "" ||
|
|
80
|
+
url.hash !== "" ||
|
|
81
|
+
!match?.[1])
|
|
82
|
+
throw new Error("PROVISIONING_CARRIER_URL_INVALID");
|
|
83
|
+
return match[1];
|
|
84
|
+
}
|
|
85
|
+
function authResult(value, carrierUrl) {
|
|
86
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
87
|
+
throw new Error("PROVISIONING_AUTH_RESULT_INVALID");
|
|
88
|
+
const result = value;
|
|
89
|
+
const expectedGptId = carrierGptId(carrierUrl);
|
|
90
|
+
if (result.status !== "AUTH_UPDATED" || result.gptId !== expectedGptId)
|
|
91
|
+
throw new Error("PROVISIONING_AUTH_RESULT_INVALID");
|
|
92
|
+
return { status: "AUTH_UPDATED", gptId: expectedGptId, carrierUrl };
|
|
93
|
+
}
|
|
94
|
+
const sleep = (milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds));
|
|
95
|
+
export async function createCustomGptProvisioningHost(options) {
|
|
96
|
+
const bridge = await createCustomGptProvisioningBridgeServer(options);
|
|
97
|
+
const onlineTimeoutMs = options.onlineTimeoutMs ?? 30_000;
|
|
98
|
+
async function waitUntilOnline() {
|
|
99
|
+
const deadline = Date.now() + onlineTimeoutMs;
|
|
100
|
+
while (Date.now() < deadline) {
|
|
101
|
+
if (bridge.status().online)
|
|
102
|
+
return;
|
|
103
|
+
await sleep(100);
|
|
104
|
+
}
|
|
105
|
+
throw new Error("PROVISIONING_EXTENSION_OFFLINE");
|
|
106
|
+
}
|
|
107
|
+
async function provisionPackage(input) {
|
|
108
|
+
await waitUntilOnline();
|
|
109
|
+
if (input.credential !== undefined && input.credential.length < 32)
|
|
110
|
+
throw new Error("PROVISIONING_ROLE_CREDENTIAL_INVALID");
|
|
111
|
+
const schemaPath = packageAsset(input.packageRoot, input.material.actionSchema, "ACTION_SCHEMA_PATH");
|
|
112
|
+
const bundlePath = packageAsset(input.packageRoot, input.material.knowledgeBundle, "KNOWLEDGE_BUNDLE_PATH");
|
|
113
|
+
const schema = hydratedSchema(await readFile(schemaPath, "utf8"), input.gatewayUrl);
|
|
114
|
+
const bundle = await materializeCustomGptKnowledgeBundle({
|
|
115
|
+
bundlePath,
|
|
116
|
+
stagingRoot: input.stagingRoot,
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
const relayFiles = await bridge.provisioning.registerFiles(bundle.files.map((file) => ({
|
|
120
|
+
name: file.name,
|
|
121
|
+
path: file.path,
|
|
122
|
+
mime: file.mime,
|
|
123
|
+
})));
|
|
124
|
+
const request = {
|
|
125
|
+
...input.material,
|
|
126
|
+
actionSchema: schema,
|
|
127
|
+
...(input.credential === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { bearerCredential: input.credential }),
|
|
130
|
+
knowledgeFiles: relayFiles.map(({ name, mime, sizeBytes, sha256, url }) => ({
|
|
131
|
+
name,
|
|
132
|
+
mime,
|
|
133
|
+
sizeBytes,
|
|
134
|
+
sha256,
|
|
135
|
+
url,
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
138
|
+
if (JSON.stringify(request).length >= 100_000)
|
|
139
|
+
throw new Error("PROVISIONING_REQUEST_BUDGET_EXCEEDED");
|
|
140
|
+
const value = await bridge.provisioning.request({
|
|
141
|
+
type: "PROVISION_CUSTOM_GPT",
|
|
142
|
+
request,
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
...liveResult(value, input.material),
|
|
146
|
+
knowledgeBundleSha256: bundle.bundleSha256,
|
|
147
|
+
knowledgeFiles: fileEvidence(bundle.files),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await rm(bundle.stagingDirectory, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function finalizeRoleAuth(input) {
|
|
155
|
+
await waitUntilOnline();
|
|
156
|
+
carrierGptId(input.carrierUrl);
|
|
157
|
+
if (input.credential.length < 32)
|
|
158
|
+
throw new Error("PROVISIONING_ROLE_CREDENTIAL_INVALID");
|
|
159
|
+
let credential = input.credential;
|
|
160
|
+
try {
|
|
161
|
+
const value = await bridge.provisioning.request({
|
|
162
|
+
type: "FINALIZE_CUSTOM_GPT_AUTH",
|
|
163
|
+
request: { carrierUrl: input.carrierUrl, credential },
|
|
164
|
+
});
|
|
165
|
+
return authResult(value, input.carrierUrl);
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
credential = "";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return Object.freeze({
|
|
172
|
+
endpoint: bridge.endpoint,
|
|
173
|
+
status: bridge.status,
|
|
174
|
+
provisionPackage,
|
|
175
|
+
finalizeRoleAuth,
|
|
176
|
+
close: bridge.close,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function sharedFactString(facts, name) {
|
|
180
|
+
const value = facts?.[name];
|
|
181
|
+
if (typeof value !== "string" || value.length === 0)
|
|
182
|
+
throw new Error(`PROVISIONING_SHARED_FACT_MISSING:${name}`);
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
export async function createWorkspaceCustomGptProvisioningHost(input) {
|
|
186
|
+
const moduleRef = "execution-browser-extension";
|
|
187
|
+
const facts = await readModuleSharedFacts({ workspaceRoot: input.workspaceRoot }, moduleRef);
|
|
188
|
+
const extensionId = sharedFactString(facts, "extensionId");
|
|
189
|
+
if (!/^[a-z]{32}$/.test(extensionId))
|
|
190
|
+
throw new Error("PROVISIONING_EXTENSION_ID_INVALID");
|
|
191
|
+
const tokenFile = sharedFactString(facts, "provisioningBridgeTokenFile");
|
|
192
|
+
const endpointText = sharedFactString(facts, "provisioningBridgeEndpoint");
|
|
193
|
+
let endpoint;
|
|
194
|
+
try {
|
|
195
|
+
endpoint = new URL(endpointText);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
throw new Error("PROVISIONING_ENDPOINT_INVALID");
|
|
199
|
+
}
|
|
200
|
+
const port = Number(endpoint.port);
|
|
201
|
+
if (endpoint.protocol !== "http:" ||
|
|
202
|
+
endpoint.hostname !== "127.0.0.1" ||
|
|
203
|
+
endpoint.pathname !== "/" ||
|
|
204
|
+
endpoint.search !== "" ||
|
|
205
|
+
endpoint.hash !== "" ||
|
|
206
|
+
!Number.isInteger(port) ||
|
|
207
|
+
port <= 0 ||
|
|
208
|
+
port > 65_535)
|
|
209
|
+
throw new Error("PROVISIONING_ENDPOINT_INVALID");
|
|
210
|
+
const token = (await readFile(tokenFile, "utf8")).trim();
|
|
211
|
+
if (token.length < 32)
|
|
212
|
+
throw new Error("PROVISIONING_TOKEN_INVALID");
|
|
213
|
+
return createCustomGptProvisioningHost({
|
|
214
|
+
token,
|
|
215
|
+
extensionId,
|
|
216
|
+
host: "127.0.0.1",
|
|
217
|
+
port,
|
|
218
|
+
...(input.commandTimeoutMs === undefined
|
|
219
|
+
? {}
|
|
220
|
+
: { commandTimeoutMs: input.commandTimeoutMs }),
|
|
221
|
+
...(input.onlineTimeoutMs === undefined
|
|
222
|
+
? {}
|
|
223
|
+
: { onlineTimeoutMs: input.onlineTimeoutMs }),
|
|
224
|
+
});
|
|
225
|
+
}
|