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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { formatHelp } from './cli-help.js';
|
|
4
|
+
import { SERVICE_VERSION, normalizeShortPairingCode } from "./vendor/protocol/index.js";
|
|
5
|
+
import { ServiceError } from './errors.js';
|
|
6
|
+
import { localControl } from './local-control.js';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { acquireOperationsLock, manage, readInstance, requestInstance, rootPath, startBackground, waitUntilStopped } from './operations/lifecycle.js';
|
|
9
|
+
import { serveManaged } from './operations/daemon.js';
|
|
10
|
+
import { OperationsLog } from './operations/logs.js';
|
|
11
|
+
import { withOfflineBackups, withReadBackups } from './operations/local-admin.js';
|
|
12
|
+
import { writeNew, readSafe } from './operations/files.js';
|
|
13
|
+
import { localCapacity } from './operations/capacity.js';
|
|
14
|
+
import { registerLocalPlugin } from './operations/plugins.js';
|
|
15
|
+
import { Repositories } from './persistence/repositories.js';
|
|
16
|
+
import { inspectClientCode } from './client-pairing.js';
|
|
17
|
+
import { createInterface } from 'node:readline/promises';
|
|
18
|
+
import { acquireServiceSingleton, rememberServiceDirectory, selectedServiceDirectory } from './operations/cli-context.js';
|
|
19
|
+
import { formatCliError, formatPairingResult, printOutput } from './cli-output.js';
|
|
20
|
+
import { updateAgentService } from './operations/update.js';
|
|
21
|
+
const rawOutputJson = process.argv.slice(2).some(argument => argument === '--output-json' || argument === '--json');
|
|
22
|
+
function printPairingResult(value, outputJson = false) { console.log(formatPairingResult(value, outputJson)); }
|
|
23
|
+
async function main() {
|
|
24
|
+
const { values, positionals } = parseArgs({ allowPositionals: true, options: { 'data-dir': { type: 'string' }, origin: { type: 'string' }, 'client-code': { type: 'string' }, yes: { type: 'boolean' }, json: { type: 'boolean' }, 'output-json': { type: 'boolean' }, host: { type: 'string', default: '0.0.0.0' }, port: { type: 'string', default: '14317' }, sha256: { type: 'string' }, output: { type: 'string' }, 'idempotency-key': { type: 'string' }, 'interrupt-and-stop': { type: 'boolean' }, version: { type: 'boolean' }, help: { type: 'boolean' } } });
|
|
25
|
+
const outputJson = values['output-json'] || values.json || false;
|
|
26
|
+
if (values.version) {
|
|
27
|
+
console.log(SERVICE_VERSION);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (values.help || positionals.length === 0) {
|
|
31
|
+
console.log(formatHelp());
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const command = positionals[0];
|
|
35
|
+
if (command === 'version') {
|
|
36
|
+
if (positionals.length !== 1)
|
|
37
|
+
throw new ServiceError('INVALID_REQUEST', 'The version command does not accept arguments.');
|
|
38
|
+
console.log(SERVICE_VERSION);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (values['data-dir'] !== undefined && command !== 'start' && command !== 'serve')
|
|
42
|
+
throw new ServiceError('INVALID_REQUEST', '--data-dir is only available for start and serve; this command uses the saved local service directory.');
|
|
43
|
+
if (!['start', 'serve', 'restart', 'update', 'stop', 'status', 'logs', 'pair', 'pair-code', 'sessions', 'revoke-session', 'capacity', 'plugin', 'backup', 'restore'].includes(command))
|
|
44
|
+
throw new ServiceError('INVALID_REQUEST', 'Unknown command. Use --help to see available commands.');
|
|
45
|
+
const starting = command === 'start' || command === 'serve';
|
|
46
|
+
const dataDir = (starting || command === 'restart' || command === 'update')
|
|
47
|
+
? values['data-dir'] ?? await selectedServiceDirectory().catch(error => {
|
|
48
|
+
if (error instanceof ServiceError && error.code === 'SERVICE_NOT_RUNNING')
|
|
49
|
+
return undefined;
|
|
50
|
+
if (command === 'update' && error.code === 'ENOENT')
|
|
51
|
+
return undefined;
|
|
52
|
+
throw error;
|
|
53
|
+
})
|
|
54
|
+
: await selectedServiceDirectory();
|
|
55
|
+
if (command === 'pair' || (command === 'pair-code' && values['client-code'])) {
|
|
56
|
+
if (positionals.length !== 1 || !values['client-code'] || values.origin !== undefined)
|
|
57
|
+
throw new ServiceError('INVALID_REQUEST', 'Use pair --client-code <CLIENT_CODE>; --origin is not required.');
|
|
58
|
+
const enteredCode = values['client-code'].trim();
|
|
59
|
+
const shortCode = normalizeShortPairingCode(enteredCode);
|
|
60
|
+
const clientCode = shortCode ? await manage(dataDir, 'resolve-client', shortCode) : enteredCode;
|
|
61
|
+
const { client, fingerprint } = inspectClientCode(clientCode);
|
|
62
|
+
console.error(`Client origin: ${client.origin}\nPublic key fingerprint SHA-256: ${fingerprint}\nThis client will receive runtime management access. Verify the origin and fingerprint shown in the browser.`);
|
|
63
|
+
if (!values.yes) {
|
|
64
|
+
if (!process.stdin.isTTY)
|
|
65
|
+
throw new ServiceError('INVALID_REQUEST', 'Non-interactive terminals must pass --yes to approve this client. No authorization was issued.');
|
|
66
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
67
|
+
let accepted;
|
|
68
|
+
try {
|
|
69
|
+
accepted = await prompt.question('Allow this client to pair? [y/N] ');
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
prompt.close();
|
|
73
|
+
}
|
|
74
|
+
if (!/^(y|yes)$/i.test(accepted.trim())) {
|
|
75
|
+
console.error('Cancelled. No authorization was issued.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Only the live owner may issue a new client grant. Never bypass IPC failure
|
|
80
|
+
// by opening a running database from a second process.
|
|
81
|
+
const result = await manage(dataDir, shortCode ? 'pair-short' : 'pair-client', shortCode ? JSON.stringify({ code: shortCode, clientCode }) : clientCode);
|
|
82
|
+
printPairingResult(result, outputJson);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (command === 'plugin') {
|
|
86
|
+
if (positionals[1] !== 'register' || positionals.length !== 3)
|
|
87
|
+
throw new Error('Use plugin register TRUSTED_MANIFEST.json');
|
|
88
|
+
const manifest = (await readSafe(positionals[2], 1024 * 1024)).toString();
|
|
89
|
+
let result;
|
|
90
|
+
try {
|
|
91
|
+
result = await manage(dataDir, 'plugin-register', manifest);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
result = await withOfflineBackups(dataDir, async (backups) => registerLocalPlugin(backups.db, new Repositories(backups.db).identity(), manifest));
|
|
95
|
+
}
|
|
96
|
+
printOutput('plugin register', result, outputJson);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (command === 'capacity') {
|
|
100
|
+
if (positionals.length > 2)
|
|
101
|
+
throw new Error('Unexpected capacity arguments');
|
|
102
|
+
let result;
|
|
103
|
+
try {
|
|
104
|
+
result = await manage(dataDir, 'capacity', positionals[1]);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
result = await withOfflineBackups(dataDir, async (backups) => localCapacity(backups.db, positionals[1]));
|
|
108
|
+
}
|
|
109
|
+
printOutput('capacity', result, outputJson);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (['start', 'restart', 'update', 'stop', 'status', 'logs'].includes(command)) {
|
|
113
|
+
if (positionals.length !== 1)
|
|
114
|
+
throw new Error('Unexpected arguments');
|
|
115
|
+
if (command === 'update') {
|
|
116
|
+
printOutput('update', await updateAgentService(dataDir, fileURLToPath(import.meta.url), Boolean(values['interrupt-and-stop']), outputJson), outputJson);
|
|
117
|
+
}
|
|
118
|
+
else if (command === 'restart') {
|
|
119
|
+
if (!/^\d+$/.test(values.port) || Number(values.port) > 65535)
|
|
120
|
+
throw new ServiceError('INVALID_REQUEST', 'Port must be an integer from 0 to 65535');
|
|
121
|
+
let record;
|
|
122
|
+
try {
|
|
123
|
+
record = await readInstance(dataDir);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code !== 'ENOENT')
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
let status;
|
|
130
|
+
if (record)
|
|
131
|
+
try {
|
|
132
|
+
status = await requestInstance(record, 'status');
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error.code !== 'ECONNREFUSED')
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
if (!record || !status) {
|
|
139
|
+
const args = ['--host', values.host, '--port', values.port];
|
|
140
|
+
if (dataDir)
|
|
141
|
+
args.push('--data-dir', dataDir);
|
|
142
|
+
printOutput('restart', await startBackground(fileURLToPath(import.meta.url), args), outputJson);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Older daemons expose reachable endpoints, which cannot distinguish a
|
|
146
|
+
// wildcard listener from a loopback-only listener. Never guess its scope.
|
|
147
|
+
const hostSpecified = process.argv.slice(2).some(arg => arg === '--host' || arg.startsWith('--host='));
|
|
148
|
+
const portSpecified = process.argv.slice(2).some(arg => arg === '--port' || arg.startsWith('--port='));
|
|
149
|
+
const host = hostSpecified ? values.host : status.listenHost;
|
|
150
|
+
const port = portSpecified ? values.port : new URL(status.localEndpoint).port || '80';
|
|
151
|
+
if (!host)
|
|
152
|
+
throw new ServiceError('INVALID_REQUEST', 'The older runtime did not report its listen address. Use restart --host <ORIGINAL_ADDRESS>; the runtime has not been stopped.');
|
|
153
|
+
if (!/^\d+$/.test(port) || Number(port) > 65535)
|
|
154
|
+
throw new ServiceError('INVALID_REQUEST', 'Port must be an integer from 0 to 65535');
|
|
155
|
+
await requestInstance(record, values['interrupt-and-stop'] ? 'interrupt-and-stop' : 'stop');
|
|
156
|
+
await waitUntilStopped(record);
|
|
157
|
+
printOutput('restart', await startBackground(fileURLToPath(import.meta.url), ['--data-dir', dataDir, '--host', host, '--port', port]), outputJson);
|
|
158
|
+
}
|
|
159
|
+
else if (command === 'start') {
|
|
160
|
+
const args = ['--host', values.host, '--port', values.port];
|
|
161
|
+
if (dataDir)
|
|
162
|
+
args.push('--data-dir', dataDir);
|
|
163
|
+
printOutput('start', await startBackground(fileURLToPath(import.meta.url), args), outputJson);
|
|
164
|
+
}
|
|
165
|
+
else if (command === 'logs')
|
|
166
|
+
printOutput('logs', await new OperationsLog(rootPath(dataDir)).read(), outputJson);
|
|
167
|
+
else
|
|
168
|
+
printOutput(command, await manage(dataDir, command === 'status' ? 'status' : values['interrupt-and-stop'] ? 'interrupt-and-stop' : 'stop'), outputJson);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (command === 'backup' || command === 'restore') {
|
|
172
|
+
const action = positionals[1], path = positionals[2];
|
|
173
|
+
if (command === 'backup' && action === 'create') {
|
|
174
|
+
if (positionals.length !== 2)
|
|
175
|
+
throw new Error('Unexpected backup arguments');
|
|
176
|
+
// Online IPC is authenticated; on refusal, offline acquisition fails closed
|
|
177
|
+
// if any instance (including a maintenance operation) owns the directory.
|
|
178
|
+
let result;
|
|
179
|
+
try {
|
|
180
|
+
result = await manage(dataDir, 'backup-create', values['idempotency-key']);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
result = await withOfflineBackups(dataDir, backups => backups.create(values['idempotency-key']));
|
|
184
|
+
}
|
|
185
|
+
printOutput('backup create', result, outputJson);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const useBackups = command === 'restore' && action === 'apply' ? withOfflineBackups : withReadBackups;
|
|
189
|
+
const result = await useBackups(dataDir, async (backups) => {
|
|
190
|
+
if (command === 'backup' && action === 'list' && positionals.length === 2)
|
|
191
|
+
return backups.list();
|
|
192
|
+
if (command === 'backup' && action === 'download' && path && values.output && positionals.length === 3) {
|
|
193
|
+
const download = await backups.readDownload(path);
|
|
194
|
+
await writeNew(values.output, download.body);
|
|
195
|
+
return { output: values.output, bytes: download.body.length };
|
|
196
|
+
}
|
|
197
|
+
if (command === 'restore' && action === 'preview' && path && positionals.length === 3)
|
|
198
|
+
return backups.previewRestore(path);
|
|
199
|
+
if (command === 'restore' && action === 'apply' && path && values.sha256 && positionals.length === 3) {
|
|
200
|
+
await backups.restore(path, values.sha256);
|
|
201
|
+
return { restored: true, invalidatesAllSessions: true, pausesQueuedRuns: true };
|
|
202
|
+
}
|
|
203
|
+
throw new Error('Invalid backup/restore arguments; use --help');
|
|
204
|
+
});
|
|
205
|
+
printOutput(command + ' ' + action, result, outputJson);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (['pair-code', 'sessions', 'revoke-session'].includes(positionals[0])) {
|
|
209
|
+
const command = positionals[0];
|
|
210
|
+
if (command === 'pair-code' && !values.origin)
|
|
211
|
+
throw new ServiceError('INVALID_REQUEST', 'pair-code requires the web origin in --origin.');
|
|
212
|
+
if (command === 'pair-code')
|
|
213
|
+
console.error('Legacy pairing compatibility: new clients should use pair --client-code <CLIENT_CODE>; --origin is not required.');
|
|
214
|
+
if (positionals.length !== (command === 'revoke-session' ? 2 : 1))
|
|
215
|
+
throw new ServiceError('INVALID_REQUEST', 'Invalid command arguments. Use --help.');
|
|
216
|
+
// Live admin commands run inside the owning instance. Offline session
|
|
217
|
+
// inspection/revocation acquires maintenance ownership before SQLite opens.
|
|
218
|
+
try {
|
|
219
|
+
const result = await manage(dataDir, command, command === 'pair-code' ? values.origin : positionals[1]);
|
|
220
|
+
if (command === 'pair-code')
|
|
221
|
+
printPairingResult(result, outputJson);
|
|
222
|
+
else
|
|
223
|
+
printOutput(command, result, outputJson);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
catch { /* The kernel lock below distinguishes offline from an IPC refusal. */ }
|
|
227
|
+
const lock = await acquireOperationsLock(dataDir);
|
|
228
|
+
let control;
|
|
229
|
+
try {
|
|
230
|
+
control = localControl(lock.root);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
await lock.release();
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
if (command === 'pair-code')
|
|
238
|
+
printPairingResult(await control.pairingCode(values.origin), outputJson);
|
|
239
|
+
else if (command === 'sessions')
|
|
240
|
+
printOutput('sessions', control.auth.list(), outputJson);
|
|
241
|
+
else {
|
|
242
|
+
control.auth.revokeLocal(positionals[1]);
|
|
243
|
+
printOutput('revoke-session', { revoked: positionals[1], acceptedTasksUnaffected: true }, outputJson);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
control.close();
|
|
248
|
+
await lock.release();
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (positionals.length !== 1 || positionals[0] !== 'serve')
|
|
253
|
+
throw new ServiceError('INVALID_REQUEST', 'Unknown command. Use --help to see available commands.');
|
|
254
|
+
if (!/^\d+$/.test(values.port) || Number(values.port) > 65535)
|
|
255
|
+
throw new ServiceError('INVALID_REQUEST', 'Port must be an integer from 0 to 65535');
|
|
256
|
+
const singleton = await acquireServiceSingleton();
|
|
257
|
+
let daemon;
|
|
258
|
+
try {
|
|
259
|
+
daemon = await serveManaged(dataDir, values.host, Number(values.port), () => singleton.release());
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
await singleton.release();
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
await rememberServiceDirectory(daemon.lock.root);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
await daemon.stop(true);
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
const status = { ...daemon.status(), mode: 'pairing', pairingCommand: 'openworkgraph pair --client-code <CLIENT_CODE>' };
|
|
273
|
+
printOutput('status', status, outputJson);
|
|
274
|
+
if (process.send)
|
|
275
|
+
process.send({ ok: true, status });
|
|
276
|
+
}
|
|
277
|
+
main().catch(error => {
|
|
278
|
+
const message = error instanceof Error ? error.message : 'Startup failed';
|
|
279
|
+
if (process.send)
|
|
280
|
+
process.send({ ok: false, error: message });
|
|
281
|
+
console.error(formatCliError(error, rawOutputJson));
|
|
282
|
+
process.exitCode = 1;
|
|
283
|
+
});
|
|
284
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function clientCodeHash(code: string): string;
|
|
2
|
+
export declare function inspectClientCode(code: string): {
|
|
3
|
+
client: import("./vendor/protocol/index.js").PairingClient;
|
|
4
|
+
key: import("crypto").KeyObject;
|
|
5
|
+
fingerprint: string;
|
|
6
|
+
clientHash: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function requirePairingProof(clientCode: string, proof: unknown, serviceId: string, origin: string, authorization: string, browserName: string): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify } from 'node:crypto';
|
|
2
|
+
import { decodeBase64Url, parseClientCode, pairingProofMessage } from "./vendor/protocol/index.js";
|
|
3
|
+
import { ServiceError } from './errors.js';
|
|
4
|
+
export function clientCodeHash(code) { return createHash('sha256').update(code).digest('hex'); }
|
|
5
|
+
export function inspectClientCode(code) {
|
|
6
|
+
try {
|
|
7
|
+
const client = parseClientCode(code);
|
|
8
|
+
const raw = Buffer.from(decodeBase64Url(client.publicKey, 65));
|
|
9
|
+
// OpenSSL validates that the exact uncompressed point is on P-256.
|
|
10
|
+
const key = createPublicKey({ format: 'jwk', key: { kty: 'EC', crv: 'P-256', x: raw.subarray(1, 33).toString('base64url'), y: raw.subarray(33).toString('base64url') } });
|
|
11
|
+
return { client, key, fingerprint: createHash('sha256').update(raw).digest('hex'), clientHash: clientCodeHash(code) };
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new ServiceError('PAIRING_CODE_INVALID', 'The client code is invalid or unsupported. Copy it from the browser again.');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function requirePairingProof(clientCode, proof, serviceId, origin, authorization, browserName) {
|
|
18
|
+
const { client, key } = inspectClientCode(clientCode);
|
|
19
|
+
if (client.origin !== origin)
|
|
20
|
+
throw new ServiceError('ORIGIN_DENIED', 'The client code does not belong to the current web origin.');
|
|
21
|
+
try {
|
|
22
|
+
const signature = decodeBase64Url(proof, 64);
|
|
23
|
+
if (signature.length !== 64 || !verify('sha256', pairingProofMessage(serviceId, origin, authorization, clientCode, browserName), { key, dsaEncoding: 'ieee-p1363' }, signature))
|
|
24
|
+
throw new Error();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new ServiceError('PAIRING_CODE_INVALID', 'The client private-key proof is invalid. Pair from the original page that generated the client code.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=client-pairing.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DataDirectories {
|
|
2
|
+
root: string;
|
|
3
|
+
database: string;
|
|
4
|
+
config: string;
|
|
5
|
+
blobs: string;
|
|
6
|
+
staging: string;
|
|
7
|
+
runs: string;
|
|
8
|
+
backups: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function initializeDirectories(path?: string): DataDirectories;
|
|
11
|
+
export declare function runDirectories(directories: DataDirectories, runId: string): {
|
|
12
|
+
input: string;
|
|
13
|
+
output: string;
|
|
14
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { lstatSync, mkdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { ServiceError } from './errors.js';
|
|
5
|
+
function privateDirectory(path) {
|
|
6
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
7
|
+
const stat = lstatSync(path);
|
|
8
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
9
|
+
throw new ServiceError('INVALID_REQUEST', 'Service directories must be real directories, not symlinks');
|
|
10
|
+
return realpathSync(path);
|
|
11
|
+
}
|
|
12
|
+
export function initializeDirectories(path = join(homedir(), '.openworkgraph')) {
|
|
13
|
+
const root = privateDirectory(resolve(path));
|
|
14
|
+
const result = { root, database: join(root, 'service.sqlite'), config: privateDirectory(join(root, 'config')), blobs: privateDirectory(join(root, 'blobs')), staging: privateDirectory(join(root, 'staging')), runs: privateDirectory(join(root, 'runs')), backups: privateDirectory(join(root, 'backups')) };
|
|
15
|
+
for (const file of [result.database, result.database + '-wal', result.database + '-shm']) {
|
|
16
|
+
try {
|
|
17
|
+
if (lstatSync(file).isSymbolicLink())
|
|
18
|
+
throw new ServiceError('INVALID_REQUEST', 'Database files cannot be symlinks');
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code !== 'ENOENT')
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
export function runDirectories(directories, runId) {
|
|
28
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(runId))
|
|
29
|
+
throw new ServiceError('INVALID_REQUEST', 'Invalid Run identifier');
|
|
30
|
+
const runs = privateDirectory(directories.runs);
|
|
31
|
+
const root = privateDirectory(join(runs, runId));
|
|
32
|
+
return { input: privateDirectory(join(root, 'input')), output: privateDirectory(join(root, 'output')) };
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=directories.js.map
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ErrorCode, Json } from "./vendor/protocol/index.js";
|
|
2
|
+
interface ServiceErrorOptions extends ErrorOptions {
|
|
3
|
+
details?: Json;
|
|
4
|
+
retryable?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare class ServiceError extends Error {
|
|
7
|
+
readonly code: ErrorCode;
|
|
8
|
+
readonly details: Json | undefined;
|
|
9
|
+
readonly retryable: boolean;
|
|
10
|
+
constructor(code: ErrorCode, message: string, options?: ServiceErrorOptions);
|
|
11
|
+
}
|
|
12
|
+
export {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export class ServiceError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
retryable;
|
|
5
|
+
constructor(code, message, options) { super(message, options); this.name = 'ServiceError'; this.code = code; this.details = options?.details; this.retryable = options?.retryable ?? false; }
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=errors.js.map
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import type { ServiceEvent } from "./vendor/protocol/index.js";
|
|
4
|
+
import { Auth } from "./auth.js";
|
|
5
|
+
export declare class Events {
|
|
6
|
+
readonly db: DatabaseSync;
|
|
7
|
+
readonly serviceId: string;
|
|
8
|
+
readonly auth: Auth;
|
|
9
|
+
private readonly closers;
|
|
10
|
+
constructor(db: DatabaseSync, serviceId: string, auth: Auth);
|
|
11
|
+
watermark(): number;
|
|
12
|
+
cursor(sessionId: string, sequence: number | string): string;
|
|
13
|
+
/** Capture before reading lists: authenticate and read the watermark in one transaction. */
|
|
14
|
+
currentCursor(token: string, origin: string): {
|
|
15
|
+
cursor: string;
|
|
16
|
+
};
|
|
17
|
+
decode(sessionId: string, cursor?: string): number;
|
|
18
|
+
replay(sessionId: string, sequence: number, limit?: number): ServiceEvent[];
|
|
19
|
+
decorate(value: unknown, sessionId: string): unknown;
|
|
20
|
+
stream(request: IncomingMessage, response: ServerResponse, token: string, origin: string): void;
|
|
21
|
+
close(): void;
|
|
22
|
+
}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Auth } from "./auth.js";
|
|
2
|
+
import { ServiceError } from "./errors.js";
|
|
3
|
+
import { atomic } from "./persistence/database.js";
|
|
4
|
+
export class Events {
|
|
5
|
+
db;
|
|
6
|
+
serviceId;
|
|
7
|
+
auth;
|
|
8
|
+
closers = new Set();
|
|
9
|
+
constructor(db, serviceId, auth) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
this.serviceId = serviceId;
|
|
12
|
+
this.auth = auth;
|
|
13
|
+
}
|
|
14
|
+
watermark() {
|
|
15
|
+
return Number(this.db
|
|
16
|
+
.prepare("SELECT COALESCE((SELECT seq FROM sqlite_sequence WHERE name='events'),0) n")
|
|
17
|
+
.get()["n"]);
|
|
18
|
+
}
|
|
19
|
+
cursor(sessionId, sequence) {
|
|
20
|
+
return Buffer.from(JSON.stringify([this.serviceId, sessionId, String(sequence)])).toString("base64url");
|
|
21
|
+
}
|
|
22
|
+
/** Capture before reading lists: authenticate and read the watermark in one transaction. */
|
|
23
|
+
currentCursor(token, origin) {
|
|
24
|
+
return this.auth.withSession(token, origin, (session) => ({
|
|
25
|
+
cursor: this.cursor(session.id, this.watermark()),
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
decode(sessionId, cursor) {
|
|
29
|
+
if (!cursor)
|
|
30
|
+
return 0;
|
|
31
|
+
try {
|
|
32
|
+
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
33
|
+
if (!Array.isArray(value) ||
|
|
34
|
+
value.length !== 3 ||
|
|
35
|
+
value[0] !== this.serviceId ||
|
|
36
|
+
value[1] !== sessionId ||
|
|
37
|
+
typeof value[2] !== "string" ||
|
|
38
|
+
!/^\d+$/.test(value[2]))
|
|
39
|
+
throw new Error();
|
|
40
|
+
const n = Number(value[2]);
|
|
41
|
+
if (!Number.isSafeInteger(n))
|
|
42
|
+
throw new Error();
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new ServiceError("INVALID_REQUEST", "事件游标不属于当前服务/会话或格式无效,请重读快照。");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
replay(sessionId, sequence, limit = 256) {
|
|
50
|
+
return atomic(this.db, () => {
|
|
51
|
+
const floor = Number(JSON.parse(String(this.db
|
|
52
|
+
.prepare("SELECT value FROM settings WHERE key='eventFloor'")
|
|
53
|
+
.get()?.["value"] ?? "0")));
|
|
54
|
+
if (sequence < floor)
|
|
55
|
+
throw new ServiceError("CURSOR_EXPIRED", "事件游标已过期,请读取权威快照后重新订阅。");
|
|
56
|
+
if (sequence > this.watermark())
|
|
57
|
+
throw new ServiceError("INVALID_REQUEST", "事件游标超过当前水位。");
|
|
58
|
+
return this.db
|
|
59
|
+
.prepare("SELECT * FROM events WHERE sequence>? ORDER BY sequence LIMIT ?")
|
|
60
|
+
.all(sequence, limit)
|
|
61
|
+
.map((row) => ({
|
|
62
|
+
serviceId: this.serviceId,
|
|
63
|
+
cursor: this.cursor(sessionId, String(row["sequence"])),
|
|
64
|
+
eventId: String(row["id"]),
|
|
65
|
+
type: row["type"],
|
|
66
|
+
projectId: row["project_id"],
|
|
67
|
+
graphId: row["graph_id"],
|
|
68
|
+
entityId: String(row["entity_id"]),
|
|
69
|
+
revision: Number(row["revision"]),
|
|
70
|
+
occurredAt: String(row["occurred_at"]),
|
|
71
|
+
payload: JSON.parse(String(row["payload"])),
|
|
72
|
+
}));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
decorate(value, sessionId) {
|
|
76
|
+
if (Array.isArray(value))
|
|
77
|
+
return value.map((v) => this.decorate(v, sessionId));
|
|
78
|
+
if (value && typeof value === "object" && "eventCursor" in value) {
|
|
79
|
+
const row = value;
|
|
80
|
+
return {
|
|
81
|
+
...row,
|
|
82
|
+
eventCursor: this.cursor(sessionId, String(row["eventCursor"])),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
stream(request, response, token, origin) {
|
|
88
|
+
const session = this.auth.withSession(token, origin, (current) => current);
|
|
89
|
+
let sequence = this.decode(session.id, request.headers["last-event-id"]);
|
|
90
|
+
this.replay(session.id, sequence);
|
|
91
|
+
response.writeHead(200, {
|
|
92
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
93
|
+
"Cache-Control": "no-store",
|
|
94
|
+
"X-Accel-Buffering": "no",
|
|
95
|
+
});
|
|
96
|
+
let stopped = false;
|
|
97
|
+
let lastHeartbeat = 0;
|
|
98
|
+
const close = () => {
|
|
99
|
+
if (stopped)
|
|
100
|
+
return;
|
|
101
|
+
stopped = true;
|
|
102
|
+
clearInterval(timer);
|
|
103
|
+
this.closers.delete(close);
|
|
104
|
+
response.end();
|
|
105
|
+
};
|
|
106
|
+
const flush = () => {
|
|
107
|
+
if (stopped || response.destroyed) {
|
|
108
|
+
close();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
this.auth.assertActive(token, origin);
|
|
113
|
+
if (response.writableLength > 1024 * 1024) {
|
|
114
|
+
close();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const event of this.replay(session.id, sequence)) {
|
|
118
|
+
this.auth.assertActive(token, origin);
|
|
119
|
+
response.write("id: " +
|
|
120
|
+
event.cursor +
|
|
121
|
+
"\nevent: workgraph\ndata: " +
|
|
122
|
+
JSON.stringify(event) +
|
|
123
|
+
"\n\n");
|
|
124
|
+
sequence = this.decode(session.id, event.cursor);
|
|
125
|
+
}
|
|
126
|
+
if (Date.now() - lastHeartbeat > 5000) {
|
|
127
|
+
this.auth.assertActive(token, origin);
|
|
128
|
+
response.write(": heartbeat\n\n");
|
|
129
|
+
lastHeartbeat = Date.now();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (!response.destroyed) {
|
|
134
|
+
response.write("event: stream-error\ndata: " +
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
code: error instanceof ServiceError ? error.code : "INTERNAL_ERROR",
|
|
137
|
+
reloadSnapshot: true,
|
|
138
|
+
}) +
|
|
139
|
+
"\n\n");
|
|
140
|
+
}
|
|
141
|
+
close();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const timer = setInterval(flush, 100);
|
|
145
|
+
timer.unref();
|
|
146
|
+
this.closers.add(close);
|
|
147
|
+
response.on("close", close);
|
|
148
|
+
flush();
|
|
149
|
+
}
|
|
150
|
+
close() {
|
|
151
|
+
for (const close of [...this.closers])
|
|
152
|
+
close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ExecutionPlan, type GraphSnapshot, type Json, type Run, type SubmitRun } from "./vendor/protocol/index.js";
|
|
2
|
+
import { Runs } from './runs.js';
|
|
3
|
+
import { Graphs } from './graphs.js';
|
|
4
|
+
import { Resources } from './resources.js';
|
|
5
|
+
/** Accepted runs are durable submissions. Only confirmed, dependency-ready runs enter the queue. */
|
|
6
|
+
export declare class ExecutionChains {
|
|
7
|
+
readonly runs: Runs;
|
|
8
|
+
readonly graphs: Graphs;
|
|
9
|
+
readonly resources: Resources;
|
|
10
|
+
private pumping;
|
|
11
|
+
private stopped;
|
|
12
|
+
stop(): void;
|
|
13
|
+
constructor(runs: Runs, graphs: Graphs, resources: Resources);
|
|
14
|
+
private get db();
|
|
15
|
+
private histories;
|
|
16
|
+
plan(graph: GraphSnapshot, targetId: string, exclude?: string): ExecutionPlan;
|
|
17
|
+
forRun(id: string): ExecutionPlan;
|
|
18
|
+
cancel(id: string, key: string, principal: string): Run;
|
|
19
|
+
selection(plan: ExecutionPlan, seeds: string[]): string[];
|
|
20
|
+
setMode(id: string, executionStart: Run['executionStart'], extra?: Record<string, Json>): void;
|
|
21
|
+
/** Prepare every missing run before entering the auth transaction. No partial batch on failure. */
|
|
22
|
+
prepareMembers(graph: GraphSnapshot, plan: ExecutionPlan, selected: string[], targetRunId: string | undefined, prepare: (request: SubmitRun) => Promise<(principal: string) => Run>): Promise<(principal: string, target?: Run) => Run>;
|
|
23
|
+
pump(): Promise<void>;
|
|
24
|
+
}
|