@hraness/ghostget 0.17.5 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +16 -9
- package/dist/apple-photos-client.js +1 -1
- package/dist/beeper-client.js +1 -1
- package/dist/{index-pf74yjs2.js → index-9wca02er.js} +1 -1
- package/docs/control-panel.md +147 -0
- package/package.json +47 -7
- package/skills/ghostget/SKILL.md +3 -1
- package/skills/ghostget/references/control-panel.md +43 -0
- package/skills/ghostget/references/install.md +5 -5
- package/skills/ghostget/references/linkedin-adapter.md +17 -3
- package/skills/ghostget/references/platform-patterns.md +1 -1
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +1 -1
- package/src/auth.ts +35 -1
- package/src/beeper-client-types.ts +1 -1
- package/src/cli.ts +18 -0
- package/src/confirmed-write-platform.ts +16 -1
- package/src/control/account-revision.ts +16 -0
- package/src/control/activity.ts +104 -0
- package/src/control/approval-broker.ts +59 -0
- package/src/control/approval-client.ts +49 -0
- package/src/control/bundled-interfaces.ts +20 -0
- package/src/control/cli.ts +20 -0
- package/src/control/connections.ts +87 -0
- package/src/control/credential-helper.ts +152 -0
- package/src/control/helper.ts +79 -0
- package/src/control/interface-cli.ts +22 -0
- package/src/control/interface-json.ts +94 -0
- package/src/control/interface-schema.ts +120 -0
- package/src/control/interfaces.ts +438 -0
- package/src/control/protocol.ts +182 -0
- package/src/control/service.ts +104 -0
- package/src/control/validation.ts +103 -0
- package/src/control/vault.ts +105 -0
- package/src/control/web-gateway.ts +62 -0
- package/src/control/web-policy.ts +56 -0
- package/src/ghostget.ts +2 -0
- package/src/messaging-runtime.ts +3 -0
- package/src/oauth-google.ts +11 -5
- package/src/omni-runtime.ts +18 -3
- package/src/operation-permission-store.ts +92 -0
- package/src/operation-permission.ts +308 -0
- package/src/pinned-https.ts +5 -0
- package/src/provider-http.ts +11 -3
- package/src/provider-plugin-contract-identity.ts +2 -2
- package/src/provider-plugin-import-analysis.ts +52 -0
- package/src/provider-plugin-module-analysis.ts +21 -1
- package/src/provider-plugin-registry.ts +4 -8
- package/src/provider-plugin.ts +4 -8
- package/src/providers/linkedin-web-contact.ts +237 -21
- package/src/read-client.ts +12 -2
- package/src/runtime.ts +81 -9
- package/src/state-helper.ts +2 -0
- package/src/storage.ts +71 -1
- package/src/usage.ts +6 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { canonicalJson, manifestHash, parseRuntimeManifest, sha256, type GhostgetManifest } from "../model";
|
|
4
|
+
import type { ProviderPluginRegistry } from "../provider-plugin-registry";
|
|
5
|
+
import {
|
|
6
|
+
createPrivateJsonIfAbsent,
|
|
7
|
+
ensurePrivateStateDirectory,
|
|
8
|
+
ghostgetStateHome,
|
|
9
|
+
installManifest,
|
|
10
|
+
listInstalledManifests,
|
|
11
|
+
loadInstalledManifestSnapshot,
|
|
12
|
+
readPrivateStateFileIfPresent,
|
|
13
|
+
snapshotPrivateStateDirectory,
|
|
14
|
+
writePrivateJsonIfUnchanged,
|
|
15
|
+
} from "../storage";
|
|
16
|
+
import { interfaceId, interfaceKeys, interfaceRecord, interfaceText, readInterfaceJson } from "./interface-json";
|
|
17
|
+
import { inputFromInterfaceSchema, interfaceInputSchema, validateInertInterfaceSchema } from "./interface-schema";
|
|
18
|
+
import type { InterfaceView } from "./protocol";
|
|
19
|
+
|
|
20
|
+
export const MAX_INTERFACE_DOCUMENT_BYTES = 512 * 1024;
|
|
21
|
+
const MAX_INTERFACE_RECORD_BYTES = 1024 * 1024;
|
|
22
|
+
const MAX_INTERFACE_ADAPTERS = 64;
|
|
23
|
+
const MAX_INTERFACE_OPERATIONS = 512;
|
|
24
|
+
const MAX_INTERFACE_DRAFTS = 128;
|
|
25
|
+
const methods = ["get", "head", "post", "put", "patch", "delete", "options"] as const;
|
|
26
|
+
const digestPattern = /^[0-9a-f]{64}$/u;
|
|
27
|
+
type ObjectValue = Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
export interface InterfaceContext {
|
|
30
|
+
readonly environment: Readonly<Record<string, string | undefined>>;
|
|
31
|
+
readonly registry: ProviderPluginRegistry;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AdapterProjection {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly mode: "editable" | "reference";
|
|
37
|
+
readonly manifest: GhostgetManifest | null;
|
|
38
|
+
readonly issues: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ParsedInterfaceDocument {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly title: string;
|
|
44
|
+
readonly digest: string;
|
|
45
|
+
readonly text: string;
|
|
46
|
+
readonly operationCount: number;
|
|
47
|
+
readonly adapters: readonly AdapterProjection[];
|
|
48
|
+
readonly issues: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ActiveProjection {
|
|
52
|
+
readonly adapterId: string;
|
|
53
|
+
readonly documentDigest: string;
|
|
54
|
+
readonly manifestDigest: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface InterfaceRecord {
|
|
58
|
+
readonly schemaVersion: 1;
|
|
59
|
+
readonly source: "user" | "imported";
|
|
60
|
+
readonly document: string;
|
|
61
|
+
readonly active: readonly ActiveProjection[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface StoredInterface {
|
|
65
|
+
readonly record: InterfaceRecord;
|
|
66
|
+
readonly parsed: ParsedInterfaceDocument;
|
|
67
|
+
readonly contentDigest: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function digest(value: unknown, label: string): string {
|
|
71
|
+
if (typeof value !== "string" || !digestPattern.test(value)) throw new Error(`${label} is invalid`);
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function operationId(value: unknown): string {
|
|
76
|
+
const id = interfaceText(value, "operation identifier", 128);
|
|
77
|
+
if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u.test(id)) throw new Error("operation identifier is invalid");
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function semanticPath(adapterId: string, operation: string): string {
|
|
82
|
+
return `/adapters/${adapterId}/operations/${operation}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function responseEnvelope(): ObjectValue {
|
|
86
|
+
return {
|
|
87
|
+
type: "object",
|
|
88
|
+
description: "Bounded Ghostget result envelope. Provider output has no narrower portable schema and remains untrusted data.",
|
|
89
|
+
properties: { output: {}, status: { type: "string" } },
|
|
90
|
+
additionalProperties: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function jsonContent(schema: ObjectValue): ObjectValue {
|
|
95
|
+
return { "application/json": { schema } };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readContent(value: unknown): unknown {
|
|
99
|
+
const content = interfaceRecord(value, "OpenAPI content");
|
|
100
|
+
interfaceKeys(content, ["application/json"], [], "OpenAPI content");
|
|
101
|
+
const media = interfaceRecord(content["application/json"], "OpenAPI media type");
|
|
102
|
+
interfaceKeys(media, ["schema"], [], "OpenAPI media type");
|
|
103
|
+
validateInertInterfaceSchema(media.schema);
|
|
104
|
+
return media.schema;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateResponses(value: unknown): void {
|
|
108
|
+
const responses = interfaceRecord(value, "OpenAPI responses");
|
|
109
|
+
if (Object.keys(responses).length < 1 || Object.keys(responses).length > 16) throw new Error("OpenAPI responses exceed their bound");
|
|
110
|
+
for (const [status, raw] of Object.entries(responses)) {
|
|
111
|
+
if (!/^(?:[1-5][0-9]{2}|default)$/u.test(status)) throw new Error("OpenAPI response status is invalid");
|
|
112
|
+
const response = interfaceRecord(raw, "OpenAPI response");
|
|
113
|
+
interfaceKeys(response, ["description"], ["content"], "OpenAPI response");
|
|
114
|
+
interfaceText(response.description, "response description", 4_096);
|
|
115
|
+
if (response.content !== undefined) readContent(response.content);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateParameters(value: unknown): void {
|
|
120
|
+
if (!Array.isArray(value) || value.length > 64) throw new Error("OpenAPI parameters exceed their bound");
|
|
121
|
+
const names = new Set<string>();
|
|
122
|
+
for (const raw of value) {
|
|
123
|
+
const parameter = interfaceRecord(raw, "OpenAPI parameter");
|
|
124
|
+
interfaceKeys(parameter, ["name", "in", "schema"], ["description", "required"], "OpenAPI parameter");
|
|
125
|
+
const name = interfaceText(parameter.name, "parameter name", 128);
|
|
126
|
+
if (parameter.in !== "path" && parameter.in !== "query") throw new Error("only inert path/query parameters are supported");
|
|
127
|
+
const coordinate = `${parameter.in}:${name}`;
|
|
128
|
+
if (names.has(coordinate)) throw new Error("OpenAPI parameters repeat ownership");
|
|
129
|
+
names.add(coordinate);
|
|
130
|
+
if (parameter.required !== undefined && typeof parameter.required !== "boolean") throw new Error("parameter required must be boolean");
|
|
131
|
+
if (parameter.in === "path" && parameter.required !== true) throw new Error("path parameters must be required");
|
|
132
|
+
if (parameter.description !== undefined) interfaceText(parameter.description, "parameter description", 4_096);
|
|
133
|
+
validateInertInterfaceSchema(parameter.schema);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateServers(value: unknown): void {
|
|
138
|
+
if (!Array.isArray(value) || value.length > 8) throw new Error("OpenAPI servers exceed their bound");
|
|
139
|
+
for (const raw of value) {
|
|
140
|
+
const server = interfaceRecord(raw, "OpenAPI server");
|
|
141
|
+
interfaceKeys(server, ["url"], ["description"], "OpenAPI server");
|
|
142
|
+
const address = interfaceText(server.url, "server URL", 2_048);
|
|
143
|
+
let url: URL;
|
|
144
|
+
try { url = new URL(address); } catch { throw new Error("OpenAPI server URL is invalid"); }
|
|
145
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || /[{}]/u.test(address)) throw new Error("OpenAPI servers must be fixed credential-free HTTPS URLs");
|
|
146
|
+
if (server.description !== undefined) interfaceText(server.description, "server description", 4_096);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extensionMetadata(value: unknown): { readonly id: string; readonly metadata: readonly { mode: "editable" | "reference"; manifest: ObjectValue }[] } {
|
|
151
|
+
const extension = interfaceRecord(value, "Ghostget interface extension");
|
|
152
|
+
interfaceKeys(extension, ["schemaVersion", "id", "adapters"], [], "Ghostget interface extension");
|
|
153
|
+
if (extension.schemaVersion !== 1) throw new Error("Ghostget interface extension version is unsupported");
|
|
154
|
+
if (!Array.isArray(extension.adapters) || extension.adapters.length > MAX_INTERFACE_ADAPTERS) throw new Error("interface adapters exceed their bound");
|
|
155
|
+
const ids = new Set<string>();
|
|
156
|
+
const metadata = extension.adapters.map((raw) => {
|
|
157
|
+
const adapter = interfaceRecord(raw, "interface adapter");
|
|
158
|
+
interfaceKeys(adapter, ["mode", "manifest"], [], "interface adapter");
|
|
159
|
+
if (adapter.mode !== "editable" && adapter.mode !== "reference") throw new Error("interface adapter mode is unsupported");
|
|
160
|
+
const manifest = interfaceRecord(adapter.manifest, "interface adapter manifest");
|
|
161
|
+
interfaceKeys(manifest, ["schemaVersion", "id", "version", "displayName", "origins", "browserDomains"], ["surfaceId"], "interface adapter manifest");
|
|
162
|
+
const id = interfaceId(manifest.id);
|
|
163
|
+
if (ids.has(id)) throw new Error("interface repeats adapter ownership");
|
|
164
|
+
ids.add(id);
|
|
165
|
+
return { mode: adapter.mode as "editable" | "reference", manifest };
|
|
166
|
+
});
|
|
167
|
+
return { id: interfaceId(extension.id), metadata };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** This compiles declarations only; it never loads an executor or follows a URL. */
|
|
171
|
+
export function parseInterfaceDocument(text: string, registry: ProviderPluginRegistry): ParsedInterfaceDocument {
|
|
172
|
+
const document = interfaceRecord(readInterfaceJson(text, MAX_INTERFACE_DOCUMENT_BYTES), "OpenAPI document");
|
|
173
|
+
interfaceKeys(document, ["openapi", "info", "paths"], ["servers", "x-ghostget"], "OpenAPI document");
|
|
174
|
+
if (document.openapi !== "3.1.0" && document.openapi !== "3.1.1") throw new Error("interface requires OpenAPI 3.1.0 or 3.1.1");
|
|
175
|
+
const info = interfaceRecord(document.info, "OpenAPI info");
|
|
176
|
+
interfaceKeys(info, ["title", "version"], ["description"], "OpenAPI info");
|
|
177
|
+
const title = interfaceText(info.title, "interface title", 100);
|
|
178
|
+
interfaceText(info.version, "interface version", 64);
|
|
179
|
+
if (info.description !== undefined) interfaceText(info.description, "interface description", 4_096);
|
|
180
|
+
if (document.servers !== undefined) validateServers(document.servers);
|
|
181
|
+
const extension = document["x-ghostget"] === undefined ? null : extensionMetadata(document["x-ghostget"]);
|
|
182
|
+
const id = extension?.id ?? interfaceId(`import-${sha256(title).slice(0, 24)}`);
|
|
183
|
+
const paths = interfaceRecord(document.paths, "OpenAPI paths");
|
|
184
|
+
const definitions = new Map<string, Record<string, unknown>>();
|
|
185
|
+
const metadata = new Map((extension?.metadata ?? []).map((entry) => [interfaceId(entry.manifest.id), entry]));
|
|
186
|
+
const ownership = new Set<string>();
|
|
187
|
+
const pathOwnership = new Set<string>();
|
|
188
|
+
const issues: string[] = [];
|
|
189
|
+
let operationCount = 0;
|
|
190
|
+
for (const [path, rawPath] of Object.entries(paths)) {
|
|
191
|
+
if (!path.startsWith("/") || path.length > 2_048 || /[\s?#\\]/u.test(path)) throw new Error("OpenAPI path is invalid");
|
|
192
|
+
const normalized = path.replace(/\{[^{}]+\}/gu, "{}");
|
|
193
|
+
if (pathOwnership.has(normalized)) throw new Error("OpenAPI paths have ambiguous ownership");
|
|
194
|
+
pathOwnership.add(normalized);
|
|
195
|
+
const item = interfaceRecord(rawPath, "OpenAPI path item");
|
|
196
|
+
interfaceKeys(item, [], methods, "OpenAPI path item");
|
|
197
|
+
for (const [method, rawOperation] of Object.entries(item)) {
|
|
198
|
+
if (++operationCount > MAX_INTERFACE_OPERATIONS) throw new Error("interface operations exceed their bound");
|
|
199
|
+
const operation = interfaceRecord(rawOperation, "OpenAPI operation");
|
|
200
|
+
interfaceKeys(operation, ["operationId", "responses"], ["summary", "description", "requestBody", "parameters", "x-ghostget"], "OpenAPI operation");
|
|
201
|
+
const publicId = interfaceText(operation.operationId, "OpenAPI operationId", 192);
|
|
202
|
+
if (ownership.has(publicId)) throw new Error("interface repeats operation ownership");
|
|
203
|
+
ownership.add(publicId);
|
|
204
|
+
if (operation.summary !== undefined) interfaceText(operation.summary, "operation summary", 512);
|
|
205
|
+
if (operation.description !== undefined) interfaceText(operation.description, "operation description", 4_096);
|
|
206
|
+
if (operation.parameters !== undefined) validateParameters(operation.parameters);
|
|
207
|
+
validateResponses(operation.responses);
|
|
208
|
+
let inputSchema: unknown;
|
|
209
|
+
if (operation.requestBody !== undefined) {
|
|
210
|
+
const body = interfaceRecord(operation.requestBody, "OpenAPI request body");
|
|
211
|
+
interfaceKeys(body, ["required", "content"], [], "OpenAPI request body");
|
|
212
|
+
if (typeof body.required !== "boolean") throw new Error("request body required must be boolean");
|
|
213
|
+
inputSchema = readContent(body.content);
|
|
214
|
+
}
|
|
215
|
+
if (operation["x-ghostget"] === undefined) {
|
|
216
|
+
issues.push(`Operation ${publicId} needs a trusted Ghostget executor; its HTTP description is inert.`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const binding = interfaceRecord(operation["x-ghostget"], "operation Ghostget binding");
|
|
220
|
+
interfaceKeys(binding, ["adapterId", "operationId", "definition"], [], "operation Ghostget binding");
|
|
221
|
+
const adapterId = interfaceId(binding.adapterId);
|
|
222
|
+
const boundOperationId = operationId(binding.operationId);
|
|
223
|
+
if (!metadata.has(adapterId)) throw new Error("operation binding references an undeclared adapter");
|
|
224
|
+
if (method !== "post" || path !== semanticPath(adapterId, boundOperationId) || publicId !== `${adapterId}:${boundOperationId}` || operation.parameters !== undefined || operation.summary !== undefined) throw new Error("bound operation changed its exact semantic route");
|
|
225
|
+
const definition = interfaceRecord(binding.definition, "bound operation definition");
|
|
226
|
+
interfaceKeys(definition, ["risk", "sideEffect", "idempotency", "dedupeWindowMs"], ["provider", "webSession", "localCli", "reviewedTemplate"], "bound operation definition");
|
|
227
|
+
if (Object.keys(definition).filter((key) => ["provider", "webSession", "localCli", "reviewedTemplate"].includes(key)).length !== 1) throw new Error("bound operation must select exactly one semantic executor");
|
|
228
|
+
const description = interfaceText(operation.description, "bound operation description", 4_096);
|
|
229
|
+
const body = interfaceRecord(operation.requestBody, "bound request body");
|
|
230
|
+
if (body.required !== true) throw new Error("bound operation requires its input object");
|
|
231
|
+
const expectedResponses = { "200": { description: "Ghostget operation result", content: jsonContent(responseEnvelope()) } };
|
|
232
|
+
if (canonicalJson(operation.responses) !== canonicalJson(expectedResponses)) throw new Error("bound operation changed its declared result envelope");
|
|
233
|
+
const operations = definitions.get(adapterId) ?? {};
|
|
234
|
+
if (Object.hasOwn(operations, boundOperationId)) throw new Error("interface repeats adapter operation ownership");
|
|
235
|
+
operations[boundOperationId] = { ...definition, description, input: inputFromInterfaceSchema(inputSchema) };
|
|
236
|
+
definitions.set(adapterId, operations);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const adapters: AdapterProjection[] = [];
|
|
240
|
+
for (const [adapterId, entry] of metadata) {
|
|
241
|
+
const candidate = { ...entry.manifest, operations: definitions.get(adapterId) ?? {} };
|
|
242
|
+
const parsed = parseRuntimeManifest(candidate, registry);
|
|
243
|
+
const adapterIssues = parsed.ok ? [] : parsed.issues.map((issue) => `Adapter ${adapterId}: ${issue}`);
|
|
244
|
+
if (parsed.ok && canonicalJson(parsed.value) !== canonicalJson(candidate)) throw new Error("interface binding contains non-canonical manifest state");
|
|
245
|
+
adapters.push({ id: adapterId, mode: entry.mode, manifest: parsed.ok ? parsed.value : null, issues: adapterIssues });
|
|
246
|
+
issues.push(...adapterIssues);
|
|
247
|
+
}
|
|
248
|
+
if (operationCount === 0) issues.push("This interface has no operations.");
|
|
249
|
+
const canonical = canonicalJson(document);
|
|
250
|
+
return Object.freeze({ id, title, digest: sha256(canonical), text: `${canonical}\n`, operationCount, adapters: Object.freeze(adapters), issues: Object.freeze(issues.slice(0, 64)) });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function interfaceDocumentForManifests(options: { readonly id: string; readonly title: string; readonly manifests: readonly GhostgetManifest[]; readonly registry: ProviderPluginRegistry }): string {
|
|
254
|
+
const paths: Record<string, unknown> = {};
|
|
255
|
+
const ids = new Set<string>();
|
|
256
|
+
const adapters = [...options.manifests].sort((left, right) => left.id.localeCompare(right.id)).map((candidate) => {
|
|
257
|
+
const parsed = parseRuntimeManifest(candidate, options.registry);
|
|
258
|
+
if (!parsed.ok) throw new Error("cannot export an invalid executable adapter");
|
|
259
|
+
const manifest = parsed.value;
|
|
260
|
+
if (ids.has(manifest.id)) throw new Error("cannot compose duplicate adapter ownership");
|
|
261
|
+
ids.add(manifest.id);
|
|
262
|
+
for (const [id, operation] of Object.entries(manifest.operations).sort(([left], [right]) => left.localeCompare(right))) {
|
|
263
|
+
const { input, description, ...definition } = operation;
|
|
264
|
+
paths[semanticPath(manifest.id, id)] = { post: {
|
|
265
|
+
operationId: `${manifest.id}:${id}`, description,
|
|
266
|
+
requestBody: { required: true, content: jsonContent(interfaceInputSchema(input)) },
|
|
267
|
+
responses: { "200": { description: "Ghostget operation result", content: jsonContent(responseEnvelope()) } },
|
|
268
|
+
"x-ghostget": { adapterId: manifest.id, operationId: id, definition },
|
|
269
|
+
} };
|
|
270
|
+
}
|
|
271
|
+
const { operations: _operations, ...metadata } = manifest;
|
|
272
|
+
return { mode: options.registry.resolveOwnedManifest(manifest.id) === undefined ? "editable" : "reference", manifest: metadata };
|
|
273
|
+
});
|
|
274
|
+
return parseInterfaceDocument(canonicalJson({
|
|
275
|
+
openapi: "3.1.1", info: { title: options.title, version: "1.0.0" }, paths,
|
|
276
|
+
"x-ghostget": { schemaVersion: 1, id: interfaceId(options.id), adapters },
|
|
277
|
+
}), options.registry).text;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function directory(context: InterfaceContext): string {
|
|
281
|
+
return join(ghostgetStateHome(context.environment), "control", "interfaces");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function recordPath(id: string, context: InterfaceContext): string {
|
|
285
|
+
return join(directory(context), `${interfaceId(id)}.json`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function readStored(id: string, context: InterfaceContext): StoredInterface | null {
|
|
289
|
+
const bytes = readPrivateStateFileIfPresent(recordPath(id, context), MAX_INTERFACE_RECORD_BYTES, "interface draft", context.environment);
|
|
290
|
+
if (bytes === null) return null;
|
|
291
|
+
const raw = interfaceRecord(readInterfaceJson(bytes, MAX_INTERFACE_RECORD_BYTES), "interface record");
|
|
292
|
+
interfaceKeys(raw, ["schemaVersion", "source", "document", "active"], [], "interface record");
|
|
293
|
+
if (raw.schemaVersion !== 1 || (raw.source !== "user" && raw.source !== "imported") || typeof raw.document !== "string" || !Array.isArray(raw.active) || raw.active.length > MAX_INTERFACE_ADAPTERS) throw new Error("interface record is invalid");
|
|
294
|
+
const parsed = parseInterfaceDocument(raw.document, context.registry);
|
|
295
|
+
if (parsed.id !== id || raw.document !== parsed.text) throw new Error("interface record identity or canonical bytes changed");
|
|
296
|
+
const active = raw.active.map((value) => {
|
|
297
|
+
const item = interfaceRecord(value, "active interface projection");
|
|
298
|
+
interfaceKeys(item, ["adapterId", "documentDigest", "manifestDigest"], [], "active interface projection");
|
|
299
|
+
return { adapterId: interfaceId(item.adapterId), documentDigest: digest(item.documentDigest, "active document digest"), manifestDigest: digest(item.manifestDigest, "active manifest digest") };
|
|
300
|
+
});
|
|
301
|
+
if (new Set(active.map((entry) => entry.adapterId)).size !== active.length) throw new Error("interface record repeats active ownership");
|
|
302
|
+
return { record: { schemaVersion: 1, source: raw.source, document: raw.document, active }, parsed, contentDigest: sha256(bytes) };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function view(stored: Pick<StoredInterface, "record" | "parsed">, context: InterfaceContext): InterfaceView {
|
|
306
|
+
const { record, parsed } = stored;
|
|
307
|
+
const issues = [...parsed.issues];
|
|
308
|
+
const activationTargets: { adapterId: string; installedDigest: string | null }[] = [];
|
|
309
|
+
let allActive = parsed.adapters.length > 0 && parsed.issues.length === 0;
|
|
310
|
+
const knownActiveDigests = new Set<string>();
|
|
311
|
+
for (const adapter of parsed.adapters) {
|
|
312
|
+
const owned = context.registry.resolveOwnedManifest(adapter.id);
|
|
313
|
+
const snapshot = owned === undefined ? loadInstalledManifestSnapshot(adapter.id, context.environment, context.registry) : null;
|
|
314
|
+
if (snapshot?.availability === "unsafe" || (snapshot?.availability === "present" && !snapshot.result.ok)) throw new Error("installed interface adapter is unsafe; repair it before activation");
|
|
315
|
+
const installed = owned ?? (snapshot?.result.ok ? snapshot.result.value : null);
|
|
316
|
+
if (adapter.mode === "reference" || owned !== undefined) {
|
|
317
|
+
issues.push(`Adapter ${adapter.id} belongs to its provider package; update it through that package lifecycle.`);
|
|
318
|
+
allActive = false;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (adapter.manifest !== null) activationTargets.push({ adapterId: adapter.id, installedDigest: snapshot?.contentSha256 ?? null });
|
|
322
|
+
const active = record.active.find((entry) => entry.adapterId === adapter.id);
|
|
323
|
+
const matches = installed !== null && active !== undefined && active.manifestDigest === manifestHash(installed);
|
|
324
|
+
if (matches) knownActiveDigests.add(active.documentDigest);
|
|
325
|
+
if (!matches || active?.documentDigest !== parsed.digest || adapter.manifest === null || manifestHash(adapter.manifest) !== active.manifestDigest) allActive = false;
|
|
326
|
+
}
|
|
327
|
+
return Object.freeze({
|
|
328
|
+
id: parsed.id, title: parsed.title, source: record.source, digest: parsed.digest,
|
|
329
|
+
activeDigest: allActive ? parsed.digest : knownActiveDigests.size === 1 ? [...knownActiveDigests][0] ?? null : null,
|
|
330
|
+
state: parsed.issues.length > 0 ? "needs-executor" : allActive ? "active" : "draft",
|
|
331
|
+
operationCount: parsed.operationCount, adapterIds: parsed.adapters.map((entry) => entry.id), activationTargets, issues: issues.slice(0, 64),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function listInterfaces(context: InterfaceContext): readonly InterfaceView[] {
|
|
336
|
+
const snapshot = snapshotPrivateStateDirectory(directory(context), context.environment);
|
|
337
|
+
const files = snapshot.entries.filter((entry) => entry.name.endsWith(".json"));
|
|
338
|
+
if (files.length > MAX_INTERFACE_DRAFTS) throw new Error("interface draft collection exceeds its bound");
|
|
339
|
+
return files.sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
340
|
+
if (entry.kind !== "file") throw new Error("interface draft entry is not a private file");
|
|
341
|
+
const stored = readStored(interfaceId(entry.name.slice(0, -5)), context);
|
|
342
|
+
if (stored === null) throw new Error("interface draft changed during listing; refresh");
|
|
343
|
+
return view(stored, context);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function saveInterface(options: InterfaceContext & { readonly document: string; readonly source: "user" | "imported"; readonly expectedDigest: string | null }): InterfaceView {
|
|
348
|
+
const parsed = parseInterfaceDocument(options.document, options.registry);
|
|
349
|
+
if (options.source !== "user" && options.source !== "imported") throw new Error("interface source is unsupported");
|
|
350
|
+
const previous = readStored(parsed.id, options);
|
|
351
|
+
if (options.expectedDigest !== null) digest(options.expectedDigest, "expected interface digest");
|
|
352
|
+
if ((previous?.parsed.digest ?? null) !== options.expectedDigest) throw new Error("interface draft changed; refresh before saving");
|
|
353
|
+
if (previous !== null && previous.record.source !== options.source) throw new Error("interface provenance cannot be relabeled");
|
|
354
|
+
const record: InterfaceRecord = { schemaVersion: 1, source: options.source, document: parsed.text, active: previous?.record.active ?? [] };
|
|
355
|
+
if (Buffer.byteLength(canonicalJson(record)) > MAX_INTERFACE_RECORD_BYTES) throw new Error("interface record exceeds its storage limit");
|
|
356
|
+
ensurePrivateStateDirectory(directory(options), options.environment);
|
|
357
|
+
if (previous === null && snapshotPrivateStateDirectory(directory(options), options.environment).entries.filter((entry) => entry.name.endsWith(".json")).length >= MAX_INTERFACE_DRAFTS) throw new Error("interface draft collection is full");
|
|
358
|
+
const changed = previous === null
|
|
359
|
+
? createPrivateJsonIfAbsent(recordPath(parsed.id, options), record, { privateParent: true, environment: options.environment }).created
|
|
360
|
+
: writePrivateJsonIfUnchanged(recordPath(parsed.id, options), record, { expectedCurrentContentSha256: previous.contentDigest, privateParent: true });
|
|
361
|
+
if (!changed) throw new Error("interface draft changed concurrently; refresh before saving");
|
|
362
|
+
return view({ record, parsed }, options);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function activateInterface(options: InterfaceContext & { readonly id: string; readonly digest: string; readonly adapterId: string; readonly expectedInstalledDigest: string | null }): InterfaceView {
|
|
366
|
+
digest(options.digest, "interface digest");
|
|
367
|
+
if (options.expectedInstalledDigest !== null) digest(options.expectedInstalledDigest, "installed adapter digest");
|
|
368
|
+
const stored = readStored(interfaceId(options.id), options);
|
|
369
|
+
if (stored === null || stored.parsed.digest !== options.digest) throw new Error("interface draft changed; refresh before activation");
|
|
370
|
+
const adapter = stored.parsed.adapters.find((entry) => entry.id === options.adapterId);
|
|
371
|
+
if (adapter?.manifest === null || adapter === undefined) throw new Error("selected adapter needs a matching trusted executor");
|
|
372
|
+
if (adapter.mode === "reference" || options.registry.resolveOwnedManifest(adapter.id) !== undefined) throw new Error("portable-owned adapters must use their provider package lifecycle");
|
|
373
|
+
const current = loadInstalledManifestSnapshot(adapter.id, options.environment, options.registry);
|
|
374
|
+
if (current.availability === "unsafe" || (current.availability === "present" && !current.result.ok)) throw new Error("installed adapter is unsafe; activation refused");
|
|
375
|
+
if (current.contentSha256 !== options.expectedInstalledDigest) throw new Error("installed adapter changed; refresh before activation");
|
|
376
|
+
installManifest(adapter.manifest, {
|
|
377
|
+
force: options.expectedInstalledDigest !== null,
|
|
378
|
+
environment: options.environment,
|
|
379
|
+
registry: options.registry,
|
|
380
|
+
...(options.expectedInstalledDigest === null ? {} : { expectedCurrentContentSha256: options.expectedInstalledDigest }),
|
|
381
|
+
});
|
|
382
|
+
// The installed manifest is the authority. This receipt records UI provenance only;
|
|
383
|
+
// losing it cannot preserve a permission for an old manifest or roll back a commit.
|
|
384
|
+
const active: ActiveProjection = { adapterId: adapter.id, documentDigest: stored.parsed.digest, manifestDigest: manifestHash(adapter.manifest) };
|
|
385
|
+
const record: InterfaceRecord = { ...stored.record, active: [...stored.record.active.filter((entry) => entry.adapterId !== adapter.id), active].sort((left, right) => left.adapterId.localeCompare(right.adapterId)) };
|
|
386
|
+
if (!writePrivateJsonIfUnchanged(recordPath(options.id, options), record, { expectedCurrentContentSha256: stored.contentDigest, privateParent: true })) throw new Error("adapter activated, but its draft changed concurrently; refresh to reconcile");
|
|
387
|
+
return view({ record, parsed: stored.parsed }, options);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function exportInterfaces(options: InterfaceContext & { readonly adapterId: string | null }): { readonly text: string; readonly filename: string } {
|
|
391
|
+
const manifests = new Map<string, GhostgetManifest>();
|
|
392
|
+
for (const { id, result } of listInstalledManifests(options.environment, options.registry)) {
|
|
393
|
+
if (options.adapterId !== null && options.adapterId !== id) continue;
|
|
394
|
+
if (!result.ok) throw new Error("cannot export an invalid installed adapter");
|
|
395
|
+
manifests.set(id, result.value);
|
|
396
|
+
}
|
|
397
|
+
for (const owned of options.registry.listOwnedManifests()) {
|
|
398
|
+
if (options.adapterId !== null && options.adapterId !== owned.id) continue;
|
|
399
|
+
const existing = manifests.get(owned.id);
|
|
400
|
+
if (existing !== undefined && manifestHash(existing) !== manifestHash(owned)) throw new Error("installed and portable-owned adapters conflict");
|
|
401
|
+
manifests.set(owned.id, owned);
|
|
402
|
+
}
|
|
403
|
+
if (options.adapterId !== null && !manifests.has(options.adapterId)) throw new Error("adapter is not installed");
|
|
404
|
+
const id = options.adapterId === null ? "ghostget-interfaces" : interfaceId(options.adapterId);
|
|
405
|
+
return {
|
|
406
|
+
text: interfaceDocumentForManifests({ id, title: options.adapterId === null ? "Ghostget interfaces" : manifests.get(id)?.displayName ?? id, manifests: [...manifests.values()], registry: options.registry }),
|
|
407
|
+
filename: `${id}.openapi.json`,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Projection used by catalog views; inactive edits never relabel an installed capability. */
|
|
412
|
+
export function interfaceSources(context: InterfaceContext): ReadonlyMap<string, { readonly manifestDigest: string; readonly source: "user" | "imported" }> {
|
|
413
|
+
const snapshot = snapshotPrivateStateDirectory(directory(context), context.environment);
|
|
414
|
+
const files = snapshot.entries.filter((entry) => entry.name.endsWith(".json"));
|
|
415
|
+
if (files.length > MAX_INTERFACE_DRAFTS) throw new Error("interface draft collection exceeds its bound");
|
|
416
|
+
const sources = new Map<string, { readonly manifestDigest: string; readonly source: "user" | "imported" }>();
|
|
417
|
+
const conflicts = new Set<string>();
|
|
418
|
+
for (const file of files.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
419
|
+
if (file.kind !== "file") throw new Error("interface draft entry is not a private file");
|
|
420
|
+
const stored = readStored(interfaceId(file.name.slice(0, -5)), context);
|
|
421
|
+
if (stored === null) throw new Error("interface draft changed during listing; refresh");
|
|
422
|
+
for (const active of stored.record.active) {
|
|
423
|
+
if (conflicts.has(active.adapterId)) continue;
|
|
424
|
+
const previous = sources.get(active.adapterId);
|
|
425
|
+
if (previous !== undefined && (previous.manifestDigest !== active.manifestDigest || previous.source !== stored.record.source)) {
|
|
426
|
+
// Multiple historic owners cannot manufacture a current provenance label.
|
|
427
|
+
sources.delete(active.adapterId);
|
|
428
|
+
conflicts.add(active.adapterId);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
sources.set(active.adapterId, { manifestDigest: active.manifestDigest, source: stored.record.source });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return sources;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Middleware never executes while parsing, composing or activating an interface.
|
|
438
|
+
// Future hooks may narrow authority; changing an invocation must reauthorize it.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/** Production-safe data contract. No runtime, filesystem, provider or UI imports. */
|
|
2
|
+
export const CONTROL_PROTOCOL = "ghostget.control/1" as const;
|
|
3
|
+
export type JsonValue = null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue };
|
|
4
|
+
export type PermissionDecision = "allow" | "deny" | "ask";
|
|
5
|
+
export type ControlSection = "accounts" | "capabilities" | "integrations" | "web" | "approvals" | "activity" | "setup";
|
|
6
|
+
|
|
7
|
+
export interface AccountView {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly provider: string | null;
|
|
10
|
+
readonly kind: string;
|
|
11
|
+
readonly subject: string | null;
|
|
12
|
+
readonly revision: string;
|
|
13
|
+
readonly status: "configured" | "verified" | "reconnect-required";
|
|
14
|
+
readonly source: string | null;
|
|
15
|
+
readonly tokenStorage: "external" | "ghostget-import" | "managed-oauth" | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CapabilityView {
|
|
19
|
+
readonly digest: string;
|
|
20
|
+
readonly adapterId: string;
|
|
21
|
+
readonly operationId: string;
|
|
22
|
+
readonly pluginId: string | null;
|
|
23
|
+
readonly surface: string;
|
|
24
|
+
readonly transport: string;
|
|
25
|
+
readonly risk: string;
|
|
26
|
+
readonly effect: string;
|
|
27
|
+
readonly state: "available" | "capture-required" | "unsupported";
|
|
28
|
+
readonly executorSource: "built-in" | "source" | "portable" | "unknown";
|
|
29
|
+
readonly interfaceSource: "bundled" | "user" | "imported";
|
|
30
|
+
readonly permission: PermissionDecision | "unmanaged" | "unavailable";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface InterfaceView {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly title: string;
|
|
36
|
+
readonly source: "user" | "imported";
|
|
37
|
+
readonly digest: string;
|
|
38
|
+
readonly activeDigest: string | null;
|
|
39
|
+
readonly state: "draft" | "active" | "needs-executor";
|
|
40
|
+
readonly operationCount: number;
|
|
41
|
+
readonly adapterIds: readonly string[];
|
|
42
|
+
readonly activationTargets: readonly { readonly adapterId: string; readonly installedDigest: string | null }[];
|
|
43
|
+
readonly issues: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface WebRule {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly origin: string;
|
|
49
|
+
readonly path: { readonly kind: "exact" | "prefix"; readonly value: string };
|
|
50
|
+
readonly methods: readonly ("GET" | "HEAD")[];
|
|
51
|
+
readonly queryKeys: readonly string[];
|
|
52
|
+
readonly decision: PermissionDecision;
|
|
53
|
+
/** The human reviewed the endpoint as a retrieval interface; HTTP method is not proof. */
|
|
54
|
+
readonly effect: "retrieval";
|
|
55
|
+
readonly maxResponseBytes: number;
|
|
56
|
+
readonly timeoutMs: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ApprovalView {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly digest: string;
|
|
62
|
+
readonly kind: "provider" | "web";
|
|
63
|
+
readonly title: string;
|
|
64
|
+
readonly account: string | null;
|
|
65
|
+
readonly effect: string;
|
|
66
|
+
readonly preview: string;
|
|
67
|
+
readonly expiresAt: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ActivityOutcome = "started" | "succeeded" | "denied" | "failed" | "cancelled" | "interrupted";
|
|
71
|
+
export interface ActivityRow {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly sequence: number;
|
|
74
|
+
readonly startedAt: string;
|
|
75
|
+
readonly finishedAt: string | null;
|
|
76
|
+
readonly durationMs: number | null;
|
|
77
|
+
readonly method: "GET" | "HEAD";
|
|
78
|
+
readonly origin: string | null;
|
|
79
|
+
readonly ruleId: string | null;
|
|
80
|
+
readonly endpoint: string | null;
|
|
81
|
+
readonly decision: PermissionDecision;
|
|
82
|
+
readonly outcome: ActivityOutcome;
|
|
83
|
+
readonly httpStatus: number | null;
|
|
84
|
+
readonly responseBytes: number;
|
|
85
|
+
readonly errorCode: string | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ActivityQuery {
|
|
89
|
+
readonly search: string;
|
|
90
|
+
readonly method: "all" | "GET" | "HEAD";
|
|
91
|
+
readonly outcome: "all" | ActivityOutcome;
|
|
92
|
+
readonly origin: string | null;
|
|
93
|
+
readonly since: string | null;
|
|
94
|
+
readonly order: "newest" | "oldest";
|
|
95
|
+
readonly cursor: string | null;
|
|
96
|
+
readonly limit: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ActivityPage {
|
|
100
|
+
readonly rows: readonly ActivityRow[];
|
|
101
|
+
readonly nextCursor: string | null;
|
|
102
|
+
readonly snapshotSequence: number;
|
|
103
|
+
readonly matchingCount: number;
|
|
104
|
+
readonly newerCount: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ControlSnapshot {
|
|
108
|
+
readonly version: string;
|
|
109
|
+
readonly accountId: string | null;
|
|
110
|
+
readonly accounts: readonly AccountView[];
|
|
111
|
+
readonly capabilities: readonly CapabilityView[];
|
|
112
|
+
readonly interfaces: readonly InterfaceView[];
|
|
113
|
+
readonly policy: { readonly managed: boolean; readonly revision: number };
|
|
114
|
+
readonly web: { readonly revision: number; readonly gatewayOnly: boolean; readonly rules: readonly WebRule[] };
|
|
115
|
+
readonly approvals: readonly ApprovalView[];
|
|
116
|
+
readonly connectionProviders: readonly { readonly id: string; readonly title: string }[];
|
|
117
|
+
readonly vault: { readonly provider: "1password"; readonly available: boolean; readonly purpose: "x-user-token-import" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type ControlRequest =
|
|
121
|
+
| { readonly action: "snapshot"; readonly accountId: string | null }
|
|
122
|
+
| { readonly action: "approval.list" }
|
|
123
|
+
| { readonly action: "permission.enable"; readonly expectedRevision: number }
|
|
124
|
+
| { readonly action: "permission.set"; readonly adapterId: string; readonly operationId: string; readonly accountId: string | null; readonly decision: PermissionDecision; readonly expectedRevision: number; readonly expectedCapabilityDigest: string }
|
|
125
|
+
| { readonly action: "approval.decide"; readonly id: string; readonly digest: string; readonly decision: "allow-once" | "deny" }
|
|
126
|
+
| { readonly action: "web.save"; readonly rules: readonly WebRule[]; readonly gatewayOnly: boolean; readonly expectedRevision: number }
|
|
127
|
+
| { readonly action: "activity.query"; readonly query: ActivityQuery }
|
|
128
|
+
| { readonly action: "interface.save"; readonly document: string; readonly source: "user" | "imported"; readonly expectedDigest: string | null }
|
|
129
|
+
| { readonly action: "interface.activate"; readonly id: string; readonly digest: string; readonly adapterId: string; readonly expectedInstalledDigest: string | null }
|
|
130
|
+
| { readonly action: "interface.export"; readonly adapterId: string | null }
|
|
131
|
+
| { readonly action: "connection.begin"; readonly id: string; readonly provider: string; readonly browser: "chrome" | "safari"; readonly profile: string | null; readonly expectedRevision: string | null }
|
|
132
|
+
| { readonly action: "connection.verify"; readonly attemptId: string }
|
|
133
|
+
| { readonly action: "connection.commit"; readonly attemptId: string; readonly expectedSubject: string }
|
|
134
|
+
| { readonly action: "connection.cancel"; readonly attemptId: string }
|
|
135
|
+
| { readonly action: "connection.disconnect"; readonly id: string; readonly expectedRevision: string }
|
|
136
|
+
| { readonly action: "vault.import"; readonly id: string; readonly account: string; readonly reference: string; readonly expectedSubject: string; readonly scopes: readonly string[]; readonly expiresAt: string | null; readonly expectedRevision: string | null }
|
|
137
|
+
| { readonly action: "prompt"; readonly kind: "install" | "use" | "extend" | "gateway"; readonly adapterId: string | null };
|
|
138
|
+
|
|
139
|
+
export type ControlData =
|
|
140
|
+
| { readonly kind: "snapshot"; readonly snapshot: ControlSnapshot }
|
|
141
|
+
| { readonly kind: "approvals"; readonly approvals: readonly ApprovalView[] }
|
|
142
|
+
| { readonly kind: "activity"; readonly page: ActivityPage }
|
|
143
|
+
| { readonly kind: "connection"; readonly attemptId: string; readonly status: "awaiting-sign-in" | "verified"; readonly subject: string | null }
|
|
144
|
+
| { readonly kind: "document"; readonly text: string; readonly filename: string }
|
|
145
|
+
| { readonly kind: "prompt"; readonly text: string }
|
|
146
|
+
| { readonly kind: "success"; readonly message: string };
|
|
147
|
+
|
|
148
|
+
export type ControlResponse =
|
|
149
|
+
| { readonly ok: true; readonly data: ControlData }
|
|
150
|
+
| { readonly ok: false; readonly code: string; readonly message: string };
|
|
151
|
+
|
|
152
|
+
/** Native and Direct supply this port to the same product screens. */
|
|
153
|
+
export interface ControlPanelPort {
|
|
154
|
+
request(request: ControlRequest, signal?: AbortSignal): Promise<ControlResponse>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type ApprovalTarget =
|
|
158
|
+
| { readonly kind: "provider"; readonly adapterId: string; readonly operationId: string; readonly authId: string | null; readonly input: JsonValue; readonly planDigest: string | null }
|
|
159
|
+
| { readonly kind: "web"; readonly method: "GET" | "HEAD"; readonly url: string };
|
|
160
|
+
|
|
161
|
+
export interface CheckedApproval {
|
|
162
|
+
readonly digest: string;
|
|
163
|
+
readonly revision: number;
|
|
164
|
+
readonly decision: PermissionDecision;
|
|
165
|
+
readonly kind: "provider" | "web";
|
|
166
|
+
readonly title: string;
|
|
167
|
+
readonly account: string | null;
|
|
168
|
+
readonly effect: string;
|
|
169
|
+
readonly preview: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type AgentApprovalRequest =
|
|
173
|
+
| { readonly protocol: "ghostget.approval/1"; readonly action: "request"; readonly id: string; readonly target: ApprovalTarget; readonly expectedDigest: string }
|
|
174
|
+
| { readonly protocol: "ghostget.approval/1"; readonly action: "check"; readonly id: string; readonly digest: string }
|
|
175
|
+
| { readonly protocol: "ghostget.approval/1"; readonly action: "cancel"; readonly id: string; readonly digest: string };
|
|
176
|
+
|
|
177
|
+
export type AgentApprovalResponse = {
|
|
178
|
+
readonly protocol: "ghostget.approval/1";
|
|
179
|
+
readonly status: "pending" | "allowed" | "denied" | "expired" | "cancelled" | "invalid";
|
|
180
|
+
readonly id: string;
|
|
181
|
+
readonly digest: string;
|
|
182
|
+
};
|