openworkgraph 0.2.12
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/README.md +64 -0
- package/dist/api-image-agent.d.ts +32 -0
- package/dist/api-image-agent.js +132 -0
- package/dist/api-image-output.d.ts +8 -0
- package/dist/api-image-output.js +41 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.js +153 -0
- package/dist/auth.d.ts +58 -0
- package/dist/auth.js +273 -0
- package/dist/backend/adapter.d.ts +35 -0
- package/dist/backend/adapter.js +439 -0
- package/dist/backend/capabilities.d.ts +23 -0
- package/dist/backend/capabilities.js +75 -0
- package/dist/backend/diagnostics.d.ts +9 -0
- package/dist/backend/diagnostics.js +80 -0
- package/dist/backend/index.d.ts +6 -0
- package/dist/backend/index.js +7 -0
- package/dist/backend/models.d.ts +15 -0
- package/dist/backend/models.js +43 -0
- package/dist/backend/paths.d.ts +6 -0
- package/dist/backend/paths.js +69 -0
- package/dist/backend/stdio.d.ts +66 -0
- package/dist/backend/stdio.js +172 -0
- package/dist/backend/types.d.ts +90 -0
- package/dist/backend/types.js +11 -0
- package/dist/backup-api.d.ts +10 -0
- package/dist/backup-api.js +40 -0
- package/dist/blob-store.d.ts +40 -0
- package/dist/blob-store.js +218 -0
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.js +63 -0
- package/dist/cli-output.d.ts +4 -0
- package/dist/cli-output.js +130 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +284 -0
- package/dist/client-pairing.d.ts +8 -0
- package/dist/client-pairing.js +30 -0
- package/dist/directories.d.ts +14 -0
- package/dist/directories.js +34 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +7 -0
- package/dist/events.d.ts +22 -0
- package/dist/events.js +155 -0
- package/dist/execution-chains.d.ts +24 -0
- package/dist/execution-chains.js +231 -0
- package/dist/execution-settings.d.ts +10 -0
- package/dist/execution-settings.js +38 -0
- package/dist/graph-document-history.d.ts +43 -0
- package/dist/graph-document-history.js +275 -0
- package/dist/graph-files.d.ts +14 -0
- package/dist/graph-files.js +59 -0
- package/dist/graph-transfer.d.ts +53 -0
- package/dist/graph-transfer.js +501 -0
- package/dist/graphs.d.ts +68 -0
- package/dist/graphs.js +771 -0
- package/dist/history.d.ts +20 -0
- package/dist/history.js +61 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +42 -0
- package/dist/image-credentials.d.ts +11 -0
- package/dist/image-credentials.js +130 -0
- package/dist/image-providers.d.ts +51 -0
- package/dist/image-providers.js +271 -0
- package/dist/input-materialization.d.ts +17 -0
- package/dist/input-materialization.js +95 -0
- package/dist/inputs.d.ts +74 -0
- package/dist/inputs.js +301 -0
- package/dist/local-control.d.ts +13 -0
- package/dist/local-control.js +37 -0
- package/dist/network.d.ts +11 -0
- package/dist/network.js +48 -0
- package/dist/openai-image-driver.d.ts +42 -0
- package/dist/openai-image-driver.js +368 -0
- package/dist/operations/backups.d.ts +40 -0
- package/dist/operations/backups.js +391 -0
- package/dist/operations/capacity.d.ts +7 -0
- package/dist/operations/capacity.js +35 -0
- package/dist/operations/cli-context.d.ts +7 -0
- package/dist/operations/cli-context.js +58 -0
- package/dist/operations/daemon.d.ts +36 -0
- package/dist/operations/daemon.js +159 -0
- package/dist/operations/files.d.ts +12 -0
- package/dist/operations/files.js +84 -0
- package/dist/operations/lifecycle.d.ts +28 -0
- package/dist/operations/lifecycle.js +155 -0
- package/dist/operations/local-admin.d.ts +6 -0
- package/dist/operations/local-admin.js +38 -0
- package/dist/operations/logs.d.ts +16 -0
- package/dist/operations/logs.js +37 -0
- package/dist/operations/plugins.d.ts +10 -0
- package/dist/operations/plugins.js +11 -0
- package/dist/operations/snapshot-worker.d.ts +1 -0
- package/dist/operations/snapshot-worker.js +18 -0
- package/dist/operations/snapshot.d.ts +1 -0
- package/dist/operations/snapshot.js +14 -0
- package/dist/operations/update.d.ts +26 -0
- package/dist/operations/update.js +216 -0
- package/dist/persistence/asset-business-schema.d.ts +2 -0
- package/dist/persistence/asset-business-schema.js +36 -0
- package/dist/persistence/database.d.ts +7 -0
- package/dist/persistence/database.js +91 -0
- package/dist/persistence/plugin-schema.d.ts +9 -0
- package/dist/persistence/plugin-schema.js +38 -0
- package/dist/persistence/repositories.d.ts +20 -0
- package/dist/persistence/repositories.js +106 -0
- package/dist/persistence/resource-schema.d.ts +2 -0
- package/dist/persistence/resource-schema.js +44 -0
- package/dist/persistence/schema.d.ts +6 -0
- package/dist/persistence/schema.js +236 -0
- package/dist/persistence/workflow-schema.d.ts +2 -0
- package/dist/persistence/workflow-schema.js +19 -0
- package/dist/plugin-api.d.ts +10 -0
- package/dist/plugin-api.js +40 -0
- package/dist/plugin-management.d.ts +36 -0
- package/dist/plugin-management.js +112 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.js +239 -0
- package/dist/project-discovery.d.ts +3 -0
- package/dist/project-discovery.js +57 -0
- package/dist/project-files-api.d.ts +16 -0
- package/dist/project-files-api.js +243 -0
- package/dist/project-files.d.ts +97 -0
- package/dist/project-files.js +557 -0
- package/dist/project-lock.d.ts +4 -0
- package/dist/project-lock.js +35 -0
- package/dist/project-probe-worker.d.ts +1 -0
- package/dist/project-probe-worker.js +17 -0
- package/dist/project-probe.d.ts +22 -0
- package/dist/project-probe.js +67 -0
- package/dist/projects.d.ts +29 -0
- package/dist/projects.js +219 -0
- package/dist/publication.d.ts +31 -0
- package/dist/publication.js +642 -0
- package/dist/resource-api.d.ts +15 -0
- package/dist/resource-api.js +256 -0
- package/dist/resources-orphans.d.ts +41 -0
- package/dist/resources-orphans.js +235 -0
- package/dist/resources.d.ts +246 -0
- package/dist/resources.js +714 -0
- package/dist/run-api.d.ts +10 -0
- package/dist/run-api.js +308 -0
- package/dist/run-context.d.ts +54 -0
- package/dist/run-context.js +137 -0
- package/dist/runs.d.ts +82 -0
- package/dist/runs.js +506 -0
- package/dist/runtime-backend.d.ts +24 -0
- package/dist/runtime-backend.js +214 -0
- package/dist/runtime.d.ts +97 -0
- package/dist/runtime.js +553 -0
- package/dist/scheduler.d.ts +85 -0
- package/dist/scheduler.js +344 -0
- package/dist/service.d.ts +29 -0
- package/dist/service.js +515 -0
- package/dist/skills-api.d.ts +18 -0
- package/dist/skills-api.js +120 -0
- package/dist/storage-api.d.ts +21 -0
- package/dist/storage-api.js +92 -0
- package/dist/transfer-api.d.ts +10 -0
- package/dist/transfer-api.js +60 -0
- package/dist/vendor/protocol/contracts.typecheck.d.ts +1 -0
- package/dist/vendor/protocol/contracts.typecheck.js +35 -0
- package/dist/vendor/protocol/execution-chain.d.ts +6 -0
- package/dist/vendor/protocol/execution-chain.js +26 -0
- package/dist/vendor/protocol/graph-binary.d.ts +15 -0
- package/dist/vendor/protocol/graph-binary.js +121 -0
- package/dist/vendor/protocol/graph-title.d.ts +2 -0
- package/dist/vendor/protocol/graph-title.js +23 -0
- package/dist/vendor/protocol/index.d.ts +617 -0
- package/dist/vendor/protocol/index.js +20 -0
- package/dist/vendor/protocol/pairing.d.ts +29 -0
- package/dist/vendor/protocol/pairing.js +71 -0
- package/dist/vendor/protocol/preview.d.ts +2 -0
- package/dist/vendor/protocol/preview.js +9 -0
- package/dist/vendor/protocol/resource-mime.d.ts +2 -0
- package/dist/vendor/protocol/resource-mime.js +35 -0
- package/dist/video-thumbnail.d.ts +2 -0
- package/dist/video-thumbnail.js +34 -0
- package/package.json +9 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, realpath, mkdtemp, open, rm } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve, sep, isAbsolute } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { ServiceError } from './errors.js';
|
|
6
|
+
async function realDirectory(path) {
|
|
7
|
+
const stat = await lstat(path);
|
|
8
|
+
const canonical = await realpath(path);
|
|
9
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || canonical !== resolve(path))
|
|
10
|
+
throw new ServiceError('INPUT_BLOCKED', 'Input/blob directory must be canonical and cannot be a symlink');
|
|
11
|
+
return canonical;
|
|
12
|
+
}
|
|
13
|
+
/** Async preparation outside SQLite transactions; only the newly created private directory is written. */
|
|
14
|
+
export async function materializeInputs(snapshot, options) {
|
|
15
|
+
// Copy before the first await so callers cannot alter the accepted resource set during preparation.
|
|
16
|
+
const resources = structuredClone(snapshot.resources);
|
|
17
|
+
const inputDirectory = options.inputDirectory, blobDirectory = options.blobDirectory;
|
|
18
|
+
const paths = new Map();
|
|
19
|
+
for (const envelope of resources) {
|
|
20
|
+
if (!envelope.resource)
|
|
21
|
+
continue;
|
|
22
|
+
const sha = envelope.resource.sha256;
|
|
23
|
+
if (!/^[a-f0-9]{64}$/.test(sha))
|
|
24
|
+
throw new ServiceError('INPUT_BLOCKED', 'Invalid frozen resource hash');
|
|
25
|
+
if (!paths.has(sha))
|
|
26
|
+
paths.set(sha, options.resolveBlobPath ? options.resolveBlobPath(sha) : join(blobDirectory, sha));
|
|
27
|
+
}
|
|
28
|
+
const input = await realDirectory(inputDirectory);
|
|
29
|
+
const blobs = await realDirectory(blobDirectory);
|
|
30
|
+
if (input === blobs || input.startsWith(blobs + sep) || blobs.startsWith(input + sep))
|
|
31
|
+
throw new ServiceError('INPUT_BLOCKED', 'Blob and input directories must be isolated');
|
|
32
|
+
const directory = await mkdtemp(join(input, 'prepared-'));
|
|
33
|
+
const result = { directory, files: [] };
|
|
34
|
+
const seen = new Map();
|
|
35
|
+
try {
|
|
36
|
+
for (const envelope of resources) {
|
|
37
|
+
const resource = envelope.resource;
|
|
38
|
+
if (!resource)
|
|
39
|
+
continue;
|
|
40
|
+
if (!/^[a-f0-9]{64}$/.test(resource.sha256) || !Number.isSafeInteger(resource.bytes) || resource.bytes < 0)
|
|
41
|
+
throw new ServiceError('INPUT_BLOCKED', 'Invalid frozen resource metadata');
|
|
42
|
+
const key = JSON.stringify([resource.resourceId, resource.version]);
|
|
43
|
+
const identity = JSON.stringify(resource);
|
|
44
|
+
if (seen.has(key)) {
|
|
45
|
+
if (seen.get(key) !== identity)
|
|
46
|
+
throw new ServiceError('INPUT_BLOCKED', 'Conflicting frozen resource identities');
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
seen.set(key, identity);
|
|
50
|
+
const selected = paths.get(resource.sha256);
|
|
51
|
+
if (typeof selected !== 'string' || !isAbsolute(selected) || !resolve(selected).startsWith(blobs + sep))
|
|
52
|
+
throw new ServiceError('INPUT_BLOCKED', 'Selected Blob path escapes Blob directory');
|
|
53
|
+
const canonical = await realpath(selected);
|
|
54
|
+
if (canonical !== resolve(selected) || !canonical.startsWith(blobs + sep))
|
|
55
|
+
throw new ServiceError('INPUT_BLOCKED', 'Selected Blob path contains a symlink or escapes Blob directory');
|
|
56
|
+
const source = await open(canonical, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
57
|
+
try {
|
|
58
|
+
const stat = await source.stat();
|
|
59
|
+
if (!stat.isFile() || stat.size !== resource.bytes)
|
|
60
|
+
throw new ServiceError('INPUT_BLOCKED', 'Frozen resource size/type mismatch');
|
|
61
|
+
const path = join(directory, `${result.files.length}-${resource.sha256}`);
|
|
62
|
+
const target = await open(path, 'wx', 0o600);
|
|
63
|
+
try {
|
|
64
|
+
const hash = createHash('sha256');
|
|
65
|
+
let bytes = 0;
|
|
66
|
+
for await (const chunk of source.createReadStream({ autoClose: false })) {
|
|
67
|
+
const buffer = chunk;
|
|
68
|
+
bytes += buffer.length;
|
|
69
|
+
if (bytes > resource.bytes)
|
|
70
|
+
throw new ServiceError('INPUT_BLOCKED', 'Resource grew during preparation');
|
|
71
|
+
hash.update(buffer);
|
|
72
|
+
await target.writeFile(buffer);
|
|
73
|
+
}
|
|
74
|
+
if (bytes !== resource.bytes || hash.digest('hex') !== resource.sha256)
|
|
75
|
+
throw new ServiceError('INPUT_BLOCKED', 'Frozen resource hash mismatch');
|
|
76
|
+
await target.sync();
|
|
77
|
+
await target.chmod(0o400);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await target.close();
|
|
81
|
+
}
|
|
82
|
+
result.files.push({ resourceId: resource.resourceId, version: resource.version, path, sha256: resource.sha256 });
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await source.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
await rm(directory, { recursive: true, force: true });
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=input-materialization.js.map
|
package/dist/inputs.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { CanvasResourceVersion, FrozenApiImageRoute, ImageRoute, InputSnapshot, ModelSelection, ProjectFileInput, ResourceEnvelope } from "./vendor/protocol/index.js";
|
|
3
|
+
import { PluginRegistry } from './plugins.js';
|
|
4
|
+
export interface InputBudget {
|
|
5
|
+
maxItemBytes: number;
|
|
6
|
+
maxTotalBytes: number;
|
|
7
|
+
maxResourceItemBytes: number;
|
|
8
|
+
maxResourceTotalBytes: number;
|
|
9
|
+
maxResources: number;
|
|
10
|
+
}
|
|
11
|
+
export interface PreparedRepresentation {
|
|
12
|
+
state: 'processing' | 'ready' | 'failed';
|
|
13
|
+
resourceSha256: string;
|
|
14
|
+
representationVersion: number | null;
|
|
15
|
+
text: string | null;
|
|
16
|
+
contentHash: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface InputOptions {
|
|
19
|
+
plugins?: PluginRegistry;
|
|
20
|
+
budget?: Partial<InputBudget>;
|
|
21
|
+
readRepresentation?: (resource: CanvasResourceVersion) => PreparedRepresentation | null;
|
|
22
|
+
}
|
|
23
|
+
export interface InputIssue {
|
|
24
|
+
code: 'INPUT_BLOCKED' | 'INPUT_BUDGET_EXCEEDED' | 'PLUGIN_UNAVAILABLE';
|
|
25
|
+
nodeId: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
export interface InputSource {
|
|
29
|
+
edgeId: string;
|
|
30
|
+
sourceNodeId: string;
|
|
31
|
+
contentVersion: number;
|
|
32
|
+
resourceIndexes: number[];
|
|
33
|
+
projectFileIndexes: number[];
|
|
34
|
+
preview?: {
|
|
35
|
+
nodeId: string;
|
|
36
|
+
contentVersion: number;
|
|
37
|
+
edgeId: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface InputPreview {
|
|
41
|
+
graphId: string;
|
|
42
|
+
nodeId: string;
|
|
43
|
+
executionRevision: number;
|
|
44
|
+
prompt: string;
|
|
45
|
+
resources: ResourceEnvelope[];
|
|
46
|
+
projectFiles: ProjectFileInput[];
|
|
47
|
+
sources: InputSource[];
|
|
48
|
+
issues: InputIssue[];
|
|
49
|
+
canSubmit: boolean;
|
|
50
|
+
predecessorCount: number;
|
|
51
|
+
totalBytes: number;
|
|
52
|
+
}
|
|
53
|
+
export type BuildInputOptions = {
|
|
54
|
+
expectedExecutionRevision?: number;
|
|
55
|
+
} & ({
|
|
56
|
+
model: ModelSelection;
|
|
57
|
+
imageRoute?: Extract<ImageRoute, {
|
|
58
|
+
type: 'codex';
|
|
59
|
+
}>;
|
|
60
|
+
} | {
|
|
61
|
+
imageRoute: FrozenApiImageRoute;
|
|
62
|
+
model?: never;
|
|
63
|
+
});
|
|
64
|
+
/** A DB-only, synchronous builder. No extraction, filesystem or network work belongs here. */
|
|
65
|
+
export declare class InputPreparation {
|
|
66
|
+
private readonly db;
|
|
67
|
+
private readonly options;
|
|
68
|
+
private readonly plugins;
|
|
69
|
+
private readonly budget;
|
|
70
|
+
constructor(db: DatabaseSync, options?: InputOptions);
|
|
71
|
+
inputPreview(graphId: string, nodeId: string, excludeFiles?: boolean): InputPreview;
|
|
72
|
+
build(graphId: string, nodeId: string, options: BuildInputOptions): InputSnapshot;
|
|
73
|
+
private collect;
|
|
74
|
+
}
|
package/dist/inputs.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { ServiceError } from './errors.js';
|
|
3
|
+
import { PluginRegistry, readField } from './plugins.js';
|
|
4
|
+
import { normalizeProjectPath } from './project-files.js';
|
|
5
|
+
const DEFAULT_BUDGET = { maxItemBytes: 8_388_608, maxTotalBytes: 33_554_432, maxResourceItemBytes: 52_428_800, maxResourceTotalBytes: 134_217_728, maxResources: 64 };
|
|
6
|
+
function freeze(value) {
|
|
7
|
+
if (value && typeof value === 'object') {
|
|
8
|
+
for (const child of Object.values(value))
|
|
9
|
+
freeze(child);
|
|
10
|
+
Object.freeze(value);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
/** A DB-only, synchronous builder. No extraction, filesystem or network work belongs here. */
|
|
15
|
+
export class InputPreparation {
|
|
16
|
+
db;
|
|
17
|
+
options;
|
|
18
|
+
plugins;
|
|
19
|
+
budget;
|
|
20
|
+
constructor(db, options = {}) {
|
|
21
|
+
this.db = db;
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.plugins = options.plugins ?? new PluginRegistry(db);
|
|
24
|
+
this.budget = { ...DEFAULT_BUDGET, ...options.budget };
|
|
25
|
+
for (const n of Object.values(this.budget))
|
|
26
|
+
if (!Number.isSafeInteger(n) || n < 1)
|
|
27
|
+
throw new ServiceError('INVALID_REQUEST', 'Input budgets must be positive safe integers');
|
|
28
|
+
}
|
|
29
|
+
inputPreview(graphId, nodeId, excludeFiles = false) {
|
|
30
|
+
// A savepoint gives standalone previews one read snapshot and nests in the submission transaction.
|
|
31
|
+
this.db.exec('SAVEPOINT input_preparation_read');
|
|
32
|
+
try {
|
|
33
|
+
const result = this.collect(graphId, nodeId, excludeFiles);
|
|
34
|
+
this.db.exec('RELEASE input_preparation_read');
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
this.db.exec('ROLLBACK TO input_preparation_read; RELEASE input_preparation_read');
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
build(graphId, nodeId, options) {
|
|
43
|
+
const preview = this.inputPreview(graphId, nodeId, options.imageRoute?.type === 'api');
|
|
44
|
+
if (options.expectedExecutionRevision !== undefined && options.expectedExecutionRevision !== preview.executionRevision)
|
|
45
|
+
throw new ServiceError('REVISION_CONFLICT', 'Graph execution revision changed');
|
|
46
|
+
if (preview.issues.length)
|
|
47
|
+
throw new ServiceError(preview.issues[0].code, preview.issues.map(i => `${i.nodeId}: ${i.reason}`).join('; '));
|
|
48
|
+
const image = preview.resources.some(item => item.kind === 'image' && item.resource);
|
|
49
|
+
const inputMode = image ? (preview.prompt.trim() ? 'text_image' : 'image') : 'text';
|
|
50
|
+
const base = { executionRevision: preview.executionRevision, prompt: preview.prompt, resources: preview.resources, projectFiles: preview.projectFiles };
|
|
51
|
+
let payload;
|
|
52
|
+
if (options.imageRoute?.type === 'api') {
|
|
53
|
+
const route = options.imageRoute;
|
|
54
|
+
if (!route.providerId || !route.modelId || !Number.isSafeInteger(route.configRevision) || route.configRevision < 1 || !Number.isSafeInteger(route.credentialRevision) || route.credentialRevision < 1 || options.model)
|
|
55
|
+
throw new ServiceError('INVALID_REQUEST', 'Frozen API image configuration is invalid');
|
|
56
|
+
payload = { ...base, imageRoute: structuredClone(route), inputMode };
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
if (!options.model || typeof options.model.model !== 'string' || !options.model.model.trim() || (options.model.reasoningEffort !== null && typeof options.model.reasoningEffort !== 'string'))
|
|
60
|
+
throw new ServiceError('MODEL_UNAVAILABLE', 'A valid model selection is required');
|
|
61
|
+
payload = { ...base, model: { model: options.model.model, reasoningEffort: options.model.reasoningEffort }, ...(options.imageRoute ? { imageRoute: options.imageRoute, inputMode } : {}) };
|
|
62
|
+
}
|
|
63
|
+
// Include exact source revisions and edge provenance in the digest without sending ancestors to the model.
|
|
64
|
+
const inputDigest = createHash('sha256').update(JSON.stringify({ graphId, nodeId, ...payload, sources: preview.sources })).digest('hex');
|
|
65
|
+
return freeze({ inputDigest, ...payload });
|
|
66
|
+
}
|
|
67
|
+
collect(graphId, nodeId, excludeFiles) {
|
|
68
|
+
const graph = this.db.prepare('SELECT execution_revision,archived,trashed,project_id FROM graphs WHERE id=?').get(graphId);
|
|
69
|
+
if (!graph)
|
|
70
|
+
throw new ServiceError('NOT_FOUND', 'Graph not found');
|
|
71
|
+
// Schema 4 compatibility for resource fixtures; schema 5 and later filter tombstones.
|
|
72
|
+
const hasDeleted = this.db.prepare('PRAGMA table_info(nodes)').all().some(c => c['name'] === 'deleted');
|
|
73
|
+
const live = hasDeleted ? ' AND n.deleted=0' : '';
|
|
74
|
+
const target = this.db.prepare(`SELECT n.type,n.schema_version,v.content FROM nodes n JOIN node_versions v ON v.node_id=n.id AND v.version=n.current_version WHERE n.id=? AND n.graph_id=?${live}`).get(nodeId, graphId);
|
|
75
|
+
if (!target)
|
|
76
|
+
throw new ServiceError('NOT_FOUND', 'Live target node not found in graph');
|
|
77
|
+
const content = JSON.parse(String(target['content']));
|
|
78
|
+
const promptValue = readField(content, 'prompt');
|
|
79
|
+
const prompt = typeof promptValue === 'string' ? promptValue : '';
|
|
80
|
+
const result = { graphId, nodeId, executionRevision: Number(graph['execution_revision']), prompt, resources: [], projectFiles: [], sources: [], issues: [], canSubmit: false, predecessorCount: 0, totalBytes: Buffer.byteLength(prompt) };
|
|
81
|
+
let textBudgetBytes = result.totalBytes, resourceBudgetBytes = 0;
|
|
82
|
+
const issue = (id, reason, code = 'INPUT_BLOCKED') => result.issues.push({ code, nodeId: id, reason });
|
|
83
|
+
if (graph['archived'] || graph['trashed'])
|
|
84
|
+
issue(nodeId, 'Graph is archived or trashed');
|
|
85
|
+
if (!['text', 'image', 'execution'].includes(String(target['type'])))
|
|
86
|
+
issue(nodeId, 'Target is not generatable');
|
|
87
|
+
if (this.plugins.inspect(String(target['type']), Number(target['schema_version']), content).state !== 'available')
|
|
88
|
+
issue(nodeId, 'Target contract is unavailable', 'PLUGIN_UNAVAILABLE');
|
|
89
|
+
if (Buffer.byteLength(prompt) > this.budget.maxItemBytes)
|
|
90
|
+
issue(nodeId, '提示词超过单项文本预算。', 'INPUT_BUDGET_EXCEEDED');
|
|
91
|
+
const edgeColumns = this.db.prepare('PRAGMA table_info(edges)').all();
|
|
92
|
+
const order = edgeColumns.some(c => c['name'] === 'input_order') ? 'e.input_order,e.id' : 'e.id';
|
|
93
|
+
const sources = this.db.prepare(`SELECT e.id AS edge_id,e.source_id,n.type,n.schema_version,n.current_version,v.content FROM edges e LEFT JOIN nodes n ON n.id=e.source_id AND n.graph_id=e.graph_id${live} LEFT JOIN node_versions v ON v.node_id=n.id AND v.version=n.current_version WHERE e.graph_id=? AND e.target_id=? AND e.kind='reference' ORDER BY ${order}`).all(graphId, nodeId);
|
|
94
|
+
result.predecessorCount = new Set(sources.map(s => String(s['source_id']))).size;
|
|
95
|
+
if (result.predecessorCount > 8)
|
|
96
|
+
issue(nodeId, 'More than 8 direct predecessors');
|
|
97
|
+
const dedup = new Map();
|
|
98
|
+
const projectFileDedup = new Map();
|
|
99
|
+
for (let source of sources) {
|
|
100
|
+
let preview;
|
|
101
|
+
if (source['type'] === 'preview') {
|
|
102
|
+
const previewId = String(source['source_id']);
|
|
103
|
+
if (this.plugins.inspect('preview', Number(source['schema_version']), JSON.parse(String(source['content']))).state !== 'available') {
|
|
104
|
+
issue(previewId, 'Preview contract is unavailable', 'PLUGIN_UNAVAILABLE');
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const originals = this.db.prepare(`SELECT e.id AS edge_id,e.source_id,n.type,n.schema_version,n.current_version,v.content FROM edges e LEFT JOIN nodes n ON n.id=e.source_id AND n.graph_id=e.graph_id${live} LEFT JOIN node_versions v ON v.node_id=n.id AND v.version=n.current_version WHERE e.graph_id=? AND e.target_id=? AND e.kind='reference'`).all(graphId, previewId);
|
|
108
|
+
if (originals.length === 0)
|
|
109
|
+
continue;
|
|
110
|
+
if (originals.length !== 1 || originals[0]['type'] === 'preview') {
|
|
111
|
+
issue(previewId, 'Invalid preview topology');
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
preview = { nodeId: previewId, contentVersion: Number(source['current_version']), edgeId: String(originals[0]['edge_id']) };
|
|
115
|
+
source = { ...originals[0], edge_id: source['edge_id'] };
|
|
116
|
+
}
|
|
117
|
+
const id = String(source['source_id']);
|
|
118
|
+
if (source['content'] === null) {
|
|
119
|
+
issue(id, 'Source node is missing or deleted');
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (source['type'] === 'execution' || source['type'] === 'group') {
|
|
123
|
+
issue(id, 'Execution and layout group nodes cannot provide input resources');
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// An API image route sees file edges only as graph structure, never as a request input.
|
|
127
|
+
if (excludeFiles && source['type'] === 'file')
|
|
128
|
+
continue;
|
|
129
|
+
let info;
|
|
130
|
+
try {
|
|
131
|
+
info = this.plugins.inspect(String(source['type']), Number(source['schema_version']), JSON.parse(String(source['content'])));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
issue(id, error instanceof Error ? error.message : 'Invalid source content');
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (info.state !== 'available' || !info.contract) {
|
|
138
|
+
issue(id, info.reason ?? 'Missing contract', 'PLUGIN_UNAVAILABLE');
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const audit = { edgeId: String(source['edge_id']), sourceNodeId: id, contentVersion: Number(source['current_version']), resourceIndexes: [], projectFileIndexes: [], ...(preview ? { preview } : {}) };
|
|
142
|
+
result.sources.push(audit);
|
|
143
|
+
const sourceDescriptor = info.content && typeof info.content === 'object' && !Array.isArray(info.content) && info.content.source && typeof info.content.source === 'object' && !Array.isArray(info.content.source) ? info.content.source : null;
|
|
144
|
+
if (sourceDescriptor?.kind === 'project-file-empty') {
|
|
145
|
+
issue(id, '空引用节点尚未关联项目文件。');
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (sourceDescriptor?.kind === 'project-file') {
|
|
149
|
+
if (sourceDescriptor.projectId !== graph['project_id']) {
|
|
150
|
+
issue(id, '项目文件引用不属于当前项目。');
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
let relativePath;
|
|
154
|
+
try {
|
|
155
|
+
relativePath = normalizeProjectPath(sourceDescriptor.relativePath);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
issue(id, error instanceof Error ? error.message : '无效的项目文件路径。');
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!relativePath) {
|
|
162
|
+
issue(id, '项目文件引用路径为空。');
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const kind = String(source['type']);
|
|
166
|
+
if (!['text', 'image', 'document', 'video', 'file'].includes(kind)) {
|
|
167
|
+
issue(id, '节点类型不能作为项目文件输入。');
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (excludeFiles) {
|
|
171
|
+
issue(id, 'API 生图不能读取项目文件引用,请先转为独立工作图副本或使用 Codex。');
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const key = JSON.stringify([kind, relativePath]);
|
|
175
|
+
const existing = projectFileDedup.get(key);
|
|
176
|
+
if (existing !== undefined) {
|
|
177
|
+
const input = result.projectFiles[existing];
|
|
178
|
+
if (!input.sourceNodeIds.includes(id))
|
|
179
|
+
input.sourceNodeIds.push(id);
|
|
180
|
+
if (!input.edgeIds.includes(audit.edgeId))
|
|
181
|
+
input.edgeIds.push(audit.edgeId);
|
|
182
|
+
audit.projectFileIndexes.push(existing);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const index = result.projectFiles.length;
|
|
186
|
+
projectFileDedup.set(key, index);
|
|
187
|
+
audit.projectFileIndexes.push(index);
|
|
188
|
+
result.projectFiles.push({ kind, relativePath, sourceNodeIds: [id], edgeIds: [audit.edgeId] });
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!info.contract.resources.length)
|
|
193
|
+
issue(id, 'Contract provides no resources');
|
|
194
|
+
if (source['type'] === 'file' && !readField(info.content, 'resourceId'))
|
|
195
|
+
continue;
|
|
196
|
+
for (const mapping of info.contract.resources) {
|
|
197
|
+
const rawText = mapping.textField ? readField(info.content, mapping.textField) : undefined;
|
|
198
|
+
if (rawText !== undefined && typeof rawText !== 'string') {
|
|
199
|
+
issue(id, 'Mapped text must be a string');
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
let text = typeof rawText === 'string' ? rawText : null;
|
|
203
|
+
let resource = null;
|
|
204
|
+
const resourceId = mapping.resourceIdField ? readField(info.content, mapping.resourceIdField) : undefined;
|
|
205
|
+
const version = mapping.resourceVersionField ? readField(info.content, mapping.resourceVersionField) : undefined;
|
|
206
|
+
if (resourceId !== undefined || version !== undefined) {
|
|
207
|
+
if (typeof resourceId !== 'string' || typeof version !== 'number' || !Number.isSafeInteger(version) || version < 1) {
|
|
208
|
+
issue(id, 'Invalid canvas resource identity/version');
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const row = this.db.prepare('SELECT v.*,b.bytes FROM canvas_resources r JOIN canvas_resource_versions v ON v.resource_id=r.id JOIN blobs b ON b.sha256=v.sha256 WHERE r.id=? AND r.graph_id=? AND v.version=?').get(resourceId, graphId, version);
|
|
212
|
+
if (!row || !/^[a-f0-9]{64}$/.test(String(row['sha256']))) {
|
|
213
|
+
issue(id, 'Canvas resource/version is missing or belongs to another graph');
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
resource = { resourceId, version, mime: String(row['mime']), bytes: Number(row['bytes']), sha256: String(row['sha256']), representationVersion: row['representation_version'] === null ? null : Number(row['representation_version']) };
|
|
217
|
+
if ((mapping.kind === 'image' && !resource.mime.startsWith('image/')) || (mapping.kind === 'video' && !resource.mime.startsWith('video/'))) {
|
|
218
|
+
issue(id, 'Resource MIME does not match declared kind');
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
let prepared;
|
|
222
|
+
try {
|
|
223
|
+
prepared = this.options.readRepresentation?.(resource);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
issue(id, 'Prepared representation is unavailable');
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (!prepared || prepared.state !== 'ready') {
|
|
230
|
+
issue(id, `Resource is ${prepared?.state ?? 'not prepared'}; ready representation required`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (prepared.resourceSha256 !== resource.sha256 || prepared.representationVersion !== resource.representationVersion) {
|
|
234
|
+
issue(id, 'Prepared resource version/hash mismatch');
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (prepared.text !== null) {
|
|
238
|
+
if (typeof prepared.text === 'string' && Buffer.byteLength(prepared.text) > this.budget.maxItemBytes) {
|
|
239
|
+
issue(id, '解析后的文本超过单项文本预算。', 'INPUT_BUDGET_EXCEEDED');
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (typeof prepared.text !== 'string' || createHash('sha256').update(prepared.text).digest('hex') !== prepared.contentHash) {
|
|
243
|
+
issue(id, 'Prepared text hash mismatch');
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (text === null)
|
|
247
|
+
text = prepared.text;
|
|
248
|
+
}
|
|
249
|
+
if ((mapping.kind === 'video' || mapping.kind === 'document') && (!text?.trim() || resource.representationVersion === null || !prepared.text?.trim())) {
|
|
250
|
+
issue(id, 'Document/video requires a versioned text representation');
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (['image', 'video', 'file'].includes(mapping.kind) && !resource) {
|
|
255
|
+
issue(id, 'File/media requires an independent canvas resource');
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (!resource && !text?.trim()) {
|
|
259
|
+
issue(id, 'Resource has no readable content');
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const textBytes = Buffer.byteLength(text ?? ''), resourceBytes = resource?.bytes ?? 0, itemBytes = textBytes + resourceBytes;
|
|
263
|
+
if (!Number.isSafeInteger(itemBytes) || itemBytes < 0)
|
|
264
|
+
issue(id, 'Resource size is invalid', 'INPUT_BUDGET_EXCEEDED');
|
|
265
|
+
if (textBytes > this.budget.maxItemBytes)
|
|
266
|
+
issue(id, '参考文本超过单项文本预算。', 'INPUT_BUDGET_EXCEEDED');
|
|
267
|
+
if (resourceBytes > this.budget.maxResourceItemBytes)
|
|
268
|
+
issue(id, '参考文件超过单项文件预算。', 'INPUT_BUDGET_EXCEEDED');
|
|
269
|
+
const key = JSON.stringify([mapping.kind, text, resource]);
|
|
270
|
+
const existing = dedup.get(key);
|
|
271
|
+
if (existing !== undefined) {
|
|
272
|
+
const envelope = result.resources[existing];
|
|
273
|
+
if (!envelope.sourceNodeIds.includes(id))
|
|
274
|
+
envelope.sourceNodeIds.push(id);
|
|
275
|
+
audit.resourceIndexes.push(existing);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
const index = result.resources.length;
|
|
279
|
+
dedup.set(key, index);
|
|
280
|
+
audit.resourceIndexes.push(index);
|
|
281
|
+
result.resources.push({ kind: mapping.kind, sourceNodeIds: [id], text, resource });
|
|
282
|
+
result.totalBytes += itemBytes;
|
|
283
|
+
textBudgetBytes += textBytes;
|
|
284
|
+
resourceBudgetBytes += resourceBytes;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (result.resources.length > this.budget.maxResources)
|
|
289
|
+
issue(nodeId, '参考资源数量超过限制。', 'INPUT_BUDGET_EXCEEDED');
|
|
290
|
+
if (textBudgetBytes > this.budget.maxTotalBytes)
|
|
291
|
+
issue(nodeId, '文本内容总量超过预算。', 'INPUT_BUDGET_EXCEEDED');
|
|
292
|
+
if (resourceBudgetBytes > this.budget.maxResourceTotalBytes)
|
|
293
|
+
issue(nodeId, '参考文件总量超过预算。', 'INPUT_BUDGET_EXCEEDED');
|
|
294
|
+
// Image-only generation needs an actual frozen direct-predecessor image, not merely an edge or text.
|
|
295
|
+
if (!prompt.trim() && !(target['type'] === 'image' && (result.resources.some(item => item.kind === 'image' && item.resource) || result.projectFiles.some(item => item.kind === 'image'))))
|
|
296
|
+
issue(nodeId, 'Own prompt must be nonempty unless an image node has a reference image');
|
|
297
|
+
result.canSubmit = result.issues.length === 0;
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=inputs.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Auth } from './auth.js';
|
|
2
|
+
export declare function localControl(dataDir?: string): {
|
|
3
|
+
auth: Auth;
|
|
4
|
+
pairingCode: (origin: string, clientCode?: string) => Promise<{
|
|
5
|
+
code: string;
|
|
6
|
+
origin: string;
|
|
7
|
+
expiresAt: string;
|
|
8
|
+
serviceId: string;
|
|
9
|
+
endpoints: string[];
|
|
10
|
+
instructions: string[];
|
|
11
|
+
}>;
|
|
12
|
+
close: () => void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { initializeDirectories } from './directories.js';
|
|
3
|
+
import { openDatabase } from './persistence/database.js';
|
|
4
|
+
import { Repositories } from './persistence/repositories.js';
|
|
5
|
+
import { Auth } from './auth.js';
|
|
6
|
+
import { ServiceError } from './errors.js';
|
|
7
|
+
export function localControl(dataDir) {
|
|
8
|
+
const directories = initializeDirectories(dataDir);
|
|
9
|
+
if (!existsSync(directories.database))
|
|
10
|
+
throw new ServiceError('SERVICE_NOT_RUNNING', 'This data directory has not been initialized. Start serve with the same --data-dir first.');
|
|
11
|
+
const db = openDatabase(directories.database);
|
|
12
|
+
const repo = new Repositories(db);
|
|
13
|
+
const serviceId = repo.identity();
|
|
14
|
+
const auth = new Auth(db, serviceId);
|
|
15
|
+
async function pairingCode(origin, clientCode) {
|
|
16
|
+
const stored = repo.setting('listener');
|
|
17
|
+
if (!stored || typeof stored !== 'object' || Array.isArray(stored) || typeof stored['localEndpoint'] !== 'string')
|
|
18
|
+
throw new ServiceError('SERVICE_NOT_RUNNING', 'The runtime is not listening. Run start or serve before requesting a pairing code; do not specify the directory again.');
|
|
19
|
+
const listener = stored;
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(listener.localEndpoint + '/v1/info', { headers: { 'X-Workgraph-Service-Id': serviceId }, signal: AbortSignal.timeout(2000), redirect: 'error' });
|
|
22
|
+
if (!response.ok || (await response.json()).serviceId !== serviceId)
|
|
23
|
+
throw new Error();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new ServiceError('SERVICE_NOT_RUNNING', 'The local runtime could not be verified. Run status and, if needed, start before retrying.');
|
|
27
|
+
}
|
|
28
|
+
return { ...(clientCode ? auth.issueClientCode(clientCode) : auth.issueCode(origin)), endpoints: listener.endpoints, instructions: [
|
|
29
|
+
'Enter the runtime address, serviceId, and pairing code on the web page. The pairing code is valid for 5 minutes and can be redeemed only once.',
|
|
30
|
+
'The browser remembers the session. Pair again after 30 days of inactivity or after the session is revoked.',
|
|
31
|
+
'All paired browsers have the same runtime management access and can view runtime details or revoke other sessions. This does not grant the Agent access to the whole machine.',
|
|
32
|
+
(clientCode ? 'Enter the authorization code only on the original page that generated the client code. Refreshing or regenerating the client code requires new authorization. The client code binds the origin automatically. ' : 'This legacy origin-pairing entry point is retained for compatibility. New clients should use pair --client-code. ') + 'Pairing does not encrypt transport. Use HTTP only on a trusted local machine or LAN; never transmit credentials over the public internet or an untrusted network.'
|
|
33
|
+
] };
|
|
34
|
+
}
|
|
35
|
+
return { auth, pairingCode, close: () => db.close() };
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=local-control.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AddressInfo } from 'node:net';
|
|
2
|
+
/** HTTP is allowed only for literal private/link-local addresses and localhost. */
|
|
3
|
+
export declare function isPrivateNetworkHost(hostname: string): boolean;
|
|
4
|
+
export interface ListenerInfo {
|
|
5
|
+
serviceId: string;
|
|
6
|
+
instanceId: string;
|
|
7
|
+
localEndpoint: string;
|
|
8
|
+
endpoints: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function listenerInfo(address: AddressInfo, serviceId: string, instanceId: string): ListenerInfo;
|
|
11
|
+
export declare function acceptsHost(header: string | undefined, _address: AddressInfo): boolean;
|
package/dist/network.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { networkInterfaces } from 'node:os';
|
|
2
|
+
import { BlockList, isIP } from 'node:net';
|
|
3
|
+
const privateNetworkAddresses = new BlockList();
|
|
4
|
+
privateNetworkAddresses.addSubnet('10.0.0.0', 8, 'ipv4');
|
|
5
|
+
privateNetworkAddresses.addSubnet('172.16.0.0', 12, 'ipv4');
|
|
6
|
+
privateNetworkAddresses.addSubnet('192.168.0.0', 16, 'ipv4');
|
|
7
|
+
privateNetworkAddresses.addSubnet('169.254.0.0', 16, 'ipv4');
|
|
8
|
+
privateNetworkAddresses.addSubnet('127.0.0.0', 8, 'ipv4');
|
|
9
|
+
privateNetworkAddresses.addSubnet('fc00::', 7, 'ipv6');
|
|
10
|
+
privateNetworkAddresses.addSubnet('fe80::', 10, 'ipv6');
|
|
11
|
+
privateNetworkAddresses.addAddress('::1', 'ipv6');
|
|
12
|
+
/** HTTP is allowed only for literal private/link-local addresses and localhost. */
|
|
13
|
+
export function isPrivateNetworkHost(hostname) {
|
|
14
|
+
const host = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
|
|
15
|
+
if (host.toLowerCase() === 'localhost')
|
|
16
|
+
return true;
|
|
17
|
+
const family = isIP(host);
|
|
18
|
+
return family === 4 ? privateNetworkAddresses.check(host, 'ipv4') : family === 6 ? privateNetworkAddresses.check(host, 'ipv6') : false;
|
|
19
|
+
}
|
|
20
|
+
function url(host, port) { return `http://${host.includes(':') ? '[' + host + ']' : host}:${port}`; }
|
|
21
|
+
export function listenerInfo(address, serviceId, instanceId) {
|
|
22
|
+
let hosts;
|
|
23
|
+
if (address.address === '0.0.0.0' || address.address === '::') {
|
|
24
|
+
const ipv6 = address.address === '::';
|
|
25
|
+
hosts = [ipv6 ? '::1' : '127.0.0.1'];
|
|
26
|
+
for (const entries of Object.values(networkInterfaces())) {
|
|
27
|
+
for (const item of entries ?? [])
|
|
28
|
+
if (!item.internal && !item.address.includes('%') && (item.family === 'IPv4' || ipv6) && !item.address.startsWith('fe80:'))
|
|
29
|
+
hosts.push(item.address);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
hosts = [address.address];
|
|
34
|
+
const endpoints = [...new Set(hosts)].map(host => url(host, address.port));
|
|
35
|
+
return { serviceId, instanceId, localEndpoint: endpoints[0], endpoints };
|
|
36
|
+
}
|
|
37
|
+
export function acceptsHost(header, _address) {
|
|
38
|
+
if (!header)
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = new URL('http://' + header);
|
|
42
|
+
return parsed.host.toLowerCase() === header.toLowerCase() && parsed.pathname === '/' && !parsed.username && !parsed.password;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=network.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ImageProviderModel, ImageRoute, ResourceEnvelope } from "./vendor/protocol/index.js";
|
|
2
|
+
type ApiRoute = Extract<ImageRoute, {
|
|
3
|
+
type: 'api';
|
|
4
|
+
}>;
|
|
5
|
+
type ReferenceImage = {
|
|
6
|
+
bytes: Buffer;
|
|
7
|
+
mime: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
8
|
+
};
|
|
9
|
+
export interface OpenAiImageInput {
|
|
10
|
+
prompt: string;
|
|
11
|
+
references?: readonly ResourceEnvelope[];
|
|
12
|
+
referenceTexts?: readonly string[];
|
|
13
|
+
images: readonly ReferenceImage[];
|
|
14
|
+
route: ApiRoute;
|
|
15
|
+
model: ImageProviderModel;
|
|
16
|
+
}
|
|
17
|
+
export interface PlannedImageRequest {
|
|
18
|
+
path: '/images/generations' | '/images/edits' | '/images/variations';
|
|
19
|
+
body: object | FormData;
|
|
20
|
+
inputMode: 'text' | 'image' | 'text_image';
|
|
21
|
+
outputSize?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Driver-owned request contract. Model declarations can narrow, never expand, an endpoint's capabilities. */
|
|
24
|
+
export declare function planOpenAiImage(input: OpenAiImageInput): PlannedImageRequest;
|
|
25
|
+
/** Never expose the raw response or encoded image in Run history or errors. */
|
|
26
|
+
export declare function decodeOpenAiImage(value: unknown): {
|
|
27
|
+
bytes: Buffer;
|
|
28
|
+
mime: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
29
|
+
};
|
|
30
|
+
/** Enforce the frozen output dimensions even when an OpenAI-compatible image editor ignores size. */
|
|
31
|
+
export declare function enforceOpenAiImageSize(result: ReferenceImage, size?: string): Promise<ReferenceImage>;
|
|
32
|
+
export declare function discoverOpenAiModels(endpoint: string, secret: string, signal: AbortSignal, fetcher?: typeof fetch): Promise<{
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
}[]>;
|
|
36
|
+
/** Caller supplies a frozen registered API base URL and a protected credential version. */
|
|
37
|
+
export declare function executeOpenAiImage(plan: PlannedImageRequest, endpoint: string, secret: string, signal: AbortSignal, fetcher?: typeof fetch): Promise<{
|
|
38
|
+
bytes: Buffer;
|
|
39
|
+
mime: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
40
|
+
requestId?: string;
|
|
41
|
+
}>;
|
|
42
|
+
export {};
|