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.
Files changed (178) hide show
  1. package/README.md +64 -0
  2. package/dist/api-image-agent.d.ts +32 -0
  3. package/dist/api-image-agent.js +132 -0
  4. package/dist/api-image-output.d.ts +8 -0
  5. package/dist/api-image-output.js +41 -0
  6. package/dist/api.d.ts +35 -0
  7. package/dist/api.js +153 -0
  8. package/dist/auth.d.ts +58 -0
  9. package/dist/auth.js +273 -0
  10. package/dist/backend/adapter.d.ts +35 -0
  11. package/dist/backend/adapter.js +439 -0
  12. package/dist/backend/capabilities.d.ts +23 -0
  13. package/dist/backend/capabilities.js +75 -0
  14. package/dist/backend/diagnostics.d.ts +9 -0
  15. package/dist/backend/diagnostics.js +80 -0
  16. package/dist/backend/index.d.ts +6 -0
  17. package/dist/backend/index.js +7 -0
  18. package/dist/backend/models.d.ts +15 -0
  19. package/dist/backend/models.js +43 -0
  20. package/dist/backend/paths.d.ts +6 -0
  21. package/dist/backend/paths.js +69 -0
  22. package/dist/backend/stdio.d.ts +66 -0
  23. package/dist/backend/stdio.js +172 -0
  24. package/dist/backend/types.d.ts +90 -0
  25. package/dist/backend/types.js +11 -0
  26. package/dist/backup-api.d.ts +10 -0
  27. package/dist/backup-api.js +40 -0
  28. package/dist/blob-store.d.ts +40 -0
  29. package/dist/blob-store.js +218 -0
  30. package/dist/cli-help.d.ts +1 -0
  31. package/dist/cli-help.js +63 -0
  32. package/dist/cli-output.d.ts +4 -0
  33. package/dist/cli-output.js +130 -0
  34. package/dist/cli.d.ts +2 -0
  35. package/dist/cli.js +284 -0
  36. package/dist/client-pairing.d.ts +8 -0
  37. package/dist/client-pairing.js +30 -0
  38. package/dist/directories.d.ts +14 -0
  39. package/dist/directories.js +34 -0
  40. package/dist/errors.d.ts +12 -0
  41. package/dist/errors.js +7 -0
  42. package/dist/events.d.ts +22 -0
  43. package/dist/events.js +155 -0
  44. package/dist/execution-chains.d.ts +24 -0
  45. package/dist/execution-chains.js +231 -0
  46. package/dist/execution-settings.d.ts +10 -0
  47. package/dist/execution-settings.js +38 -0
  48. package/dist/graph-document-history.d.ts +43 -0
  49. package/dist/graph-document-history.js +275 -0
  50. package/dist/graph-files.d.ts +14 -0
  51. package/dist/graph-files.js +59 -0
  52. package/dist/graph-transfer.d.ts +53 -0
  53. package/dist/graph-transfer.js +501 -0
  54. package/dist/graphs.d.ts +68 -0
  55. package/dist/graphs.js +771 -0
  56. package/dist/history.d.ts +20 -0
  57. package/dist/history.js +61 -0
  58. package/dist/http.d.ts +2 -0
  59. package/dist/http.js +42 -0
  60. package/dist/image-credentials.d.ts +11 -0
  61. package/dist/image-credentials.js +130 -0
  62. package/dist/image-providers.d.ts +51 -0
  63. package/dist/image-providers.js +271 -0
  64. package/dist/input-materialization.d.ts +17 -0
  65. package/dist/input-materialization.js +95 -0
  66. package/dist/inputs.d.ts +74 -0
  67. package/dist/inputs.js +301 -0
  68. package/dist/local-control.d.ts +13 -0
  69. package/dist/local-control.js +37 -0
  70. package/dist/network.d.ts +11 -0
  71. package/dist/network.js +48 -0
  72. package/dist/openai-image-driver.d.ts +42 -0
  73. package/dist/openai-image-driver.js +368 -0
  74. package/dist/operations/backups.d.ts +40 -0
  75. package/dist/operations/backups.js +391 -0
  76. package/dist/operations/capacity.d.ts +7 -0
  77. package/dist/operations/capacity.js +35 -0
  78. package/dist/operations/cli-context.d.ts +7 -0
  79. package/dist/operations/cli-context.js +58 -0
  80. package/dist/operations/daemon.d.ts +36 -0
  81. package/dist/operations/daemon.js +159 -0
  82. package/dist/operations/files.d.ts +12 -0
  83. package/dist/operations/files.js +84 -0
  84. package/dist/operations/lifecycle.d.ts +28 -0
  85. package/dist/operations/lifecycle.js +155 -0
  86. package/dist/operations/local-admin.d.ts +6 -0
  87. package/dist/operations/local-admin.js +38 -0
  88. package/dist/operations/logs.d.ts +16 -0
  89. package/dist/operations/logs.js +37 -0
  90. package/dist/operations/plugins.d.ts +10 -0
  91. package/dist/operations/plugins.js +11 -0
  92. package/dist/operations/snapshot-worker.d.ts +1 -0
  93. package/dist/operations/snapshot-worker.js +18 -0
  94. package/dist/operations/snapshot.d.ts +1 -0
  95. package/dist/operations/snapshot.js +14 -0
  96. package/dist/operations/update.d.ts +26 -0
  97. package/dist/operations/update.js +216 -0
  98. package/dist/persistence/asset-business-schema.d.ts +2 -0
  99. package/dist/persistence/asset-business-schema.js +36 -0
  100. package/dist/persistence/database.d.ts +7 -0
  101. package/dist/persistence/database.js +91 -0
  102. package/dist/persistence/plugin-schema.d.ts +9 -0
  103. package/dist/persistence/plugin-schema.js +38 -0
  104. package/dist/persistence/repositories.d.ts +20 -0
  105. package/dist/persistence/repositories.js +106 -0
  106. package/dist/persistence/resource-schema.d.ts +2 -0
  107. package/dist/persistence/resource-schema.js +44 -0
  108. package/dist/persistence/schema.d.ts +6 -0
  109. package/dist/persistence/schema.js +236 -0
  110. package/dist/persistence/workflow-schema.d.ts +2 -0
  111. package/dist/persistence/workflow-schema.js +19 -0
  112. package/dist/plugin-api.d.ts +10 -0
  113. package/dist/plugin-api.js +40 -0
  114. package/dist/plugin-management.d.ts +36 -0
  115. package/dist/plugin-management.js +112 -0
  116. package/dist/plugins.d.ts +55 -0
  117. package/dist/plugins.js +239 -0
  118. package/dist/project-discovery.d.ts +3 -0
  119. package/dist/project-discovery.js +57 -0
  120. package/dist/project-files-api.d.ts +16 -0
  121. package/dist/project-files-api.js +243 -0
  122. package/dist/project-files.d.ts +97 -0
  123. package/dist/project-files.js +557 -0
  124. package/dist/project-lock.d.ts +4 -0
  125. package/dist/project-lock.js +35 -0
  126. package/dist/project-probe-worker.d.ts +1 -0
  127. package/dist/project-probe-worker.js +17 -0
  128. package/dist/project-probe.d.ts +22 -0
  129. package/dist/project-probe.js +67 -0
  130. package/dist/projects.d.ts +29 -0
  131. package/dist/projects.js +219 -0
  132. package/dist/publication.d.ts +31 -0
  133. package/dist/publication.js +642 -0
  134. package/dist/resource-api.d.ts +15 -0
  135. package/dist/resource-api.js +256 -0
  136. package/dist/resources-orphans.d.ts +41 -0
  137. package/dist/resources-orphans.js +235 -0
  138. package/dist/resources.d.ts +246 -0
  139. package/dist/resources.js +714 -0
  140. package/dist/run-api.d.ts +10 -0
  141. package/dist/run-api.js +308 -0
  142. package/dist/run-context.d.ts +54 -0
  143. package/dist/run-context.js +137 -0
  144. package/dist/runs.d.ts +82 -0
  145. package/dist/runs.js +506 -0
  146. package/dist/runtime-backend.d.ts +24 -0
  147. package/dist/runtime-backend.js +214 -0
  148. package/dist/runtime.d.ts +97 -0
  149. package/dist/runtime.js +553 -0
  150. package/dist/scheduler.d.ts +85 -0
  151. package/dist/scheduler.js +344 -0
  152. package/dist/service.d.ts +29 -0
  153. package/dist/service.js +515 -0
  154. package/dist/skills-api.d.ts +18 -0
  155. package/dist/skills-api.js +120 -0
  156. package/dist/storage-api.d.ts +21 -0
  157. package/dist/storage-api.js +92 -0
  158. package/dist/transfer-api.d.ts +10 -0
  159. package/dist/transfer-api.js +60 -0
  160. package/dist/vendor/protocol/contracts.typecheck.d.ts +1 -0
  161. package/dist/vendor/protocol/contracts.typecheck.js +35 -0
  162. package/dist/vendor/protocol/execution-chain.d.ts +6 -0
  163. package/dist/vendor/protocol/execution-chain.js +26 -0
  164. package/dist/vendor/protocol/graph-binary.d.ts +15 -0
  165. package/dist/vendor/protocol/graph-binary.js +121 -0
  166. package/dist/vendor/protocol/graph-title.d.ts +2 -0
  167. package/dist/vendor/protocol/graph-title.js +23 -0
  168. package/dist/vendor/protocol/index.d.ts +617 -0
  169. package/dist/vendor/protocol/index.js +20 -0
  170. package/dist/vendor/protocol/pairing.d.ts +29 -0
  171. package/dist/vendor/protocol/pairing.js +71 -0
  172. package/dist/vendor/protocol/preview.d.ts +2 -0
  173. package/dist/vendor/protocol/preview.js +9 -0
  174. package/dist/vendor/protocol/resource-mime.d.ts +2 -0
  175. package/dist/vendor/protocol/resource-mime.js +35 -0
  176. package/dist/video-thumbnail.d.ts +2 -0
  177. package/dist/video-thumbnail.js +34 -0
  178. package/package.json +9 -0
@@ -0,0 +1,159 @@
1
+ import { createService } from '../service.js';
2
+ import { listenerInfo } from '../network.js';
3
+ import { localControl } from '../local-control.js';
4
+ import { acquireOperationsLock } from './lifecycle.js';
5
+ import { OperationsLog } from './logs.js';
6
+ import { waitForBackups, clearStaleResourceLeases } from './backups.js';
7
+ import { localCapacity } from './capacity.js';
8
+ import { registerLocalPlugin } from './plugins.js';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { isGlobalNpmInstallation, startWebUpdate } from './update.js';
11
+ export function activeRunCount(db) {
12
+ return Number(db.prepare("SELECT count(*) AS n FROM runs WHERE status NOT IN ('accepted','queued','paused_restore','succeeded','failed','cancelled','interrupted')").get().n);
13
+ }
14
+ /** Public host wrapper: the lock is acquired BEFORE opening SQLite or listeners. */
15
+ export async function serveManaged(dataDir, host, port, onStopped) {
16
+ const lock = await acquireOperationsLock(dataDir), log = new OperationsLog(lock.root);
17
+ let service;
18
+ const cliPath = fileURLToPath(new URL('../cli.js', import.meta.url));
19
+ const npmInstallation = isGlobalNpmInstallation(cliPath);
20
+ try {
21
+ service = createService(lock.root, {
22
+ npmInstallation: () => npmInstallation,
23
+ updateRuntime: async (onExit) => {
24
+ if (state !== 'running')
25
+ throw new Error('Runtime is stopping.');
26
+ await startWebUpdate(cliPath, undefined, onExit);
27
+ },
28
+ });
29
+ }
30
+ catch (error) {
31
+ try {
32
+ await log.append('startup_failed');
33
+ }
34
+ finally {
35
+ await lock.release();
36
+ }
37
+ throw error;
38
+ }
39
+ let state = 'running';
40
+ let stopping = false;
41
+ const drains = new Set();
42
+ let closing;
43
+ let recoveryAttempted = false;
44
+ const onSignal = () => { void stop(false).catch(() => log.append('shutdown_failed')); };
45
+ const removeSignals = () => { process.removeListener('SIGINT', onSignal); process.removeListener('SIGTERM', onSignal); };
46
+ const status = () => ({ state, activeRuns: activeRunCount(service.db), listenHost: service.server.address().address, ...listenerInfo(service.server.address(), service.info.serviceId, service.instanceId) });
47
+ async function stop(interrupt) {
48
+ // Escalation remains available while an earlier drain waits indefinitely.
49
+ if (stopping && (!interrupt || state === 'interrupting'))
50
+ return;
51
+ state = interrupt ? 'interrupting' : 'draining';
52
+ stopping = true;
53
+ service.runtime.stopRuns();
54
+ await log.append(interrupt ? 'interrupting' : 'draining');
55
+ const drain = (async () => {
56
+ // Flush the command acknowledgment before any transport is closed.
57
+ await new Promise(resolve => setTimeout(resolve, 30));
58
+ await Promise.all([waitForBackups(service.db), service.runtime.drain({ interrupt: true })]);
59
+ })();
60
+ drains.add(drain);
61
+ void drain.then(async () => {
62
+ drains.delete(drain);
63
+ if (drains.size || closing || state === 'failed')
64
+ return;
65
+ closing = (async () => {
66
+ await service.close();
67
+ removeSignals();
68
+ try {
69
+ await log.append('stopped');
70
+ }
71
+ finally {
72
+ await lock.release();
73
+ await onStopped?.();
74
+ }
75
+ })();
76
+ try {
77
+ await closing;
78
+ }
79
+ catch {
80
+ state = 'failed';
81
+ await log.append('shutdown_failed');
82
+ }
83
+ }, async () => {
84
+ drains.delete(drain);
85
+ state = 'failed';
86
+ await log.append('shutdown_failed');
87
+ // Keep ownership while backend termination is uncertain.
88
+ }).catch(() => { });
89
+ }
90
+ try {
91
+ clearStaleResourceLeases(service.db, lock);
92
+ await service.api.backups.recoverCreating(lock);
93
+ await new Promise((done, reject) => { service.server.once('error', reject); service.server.listen(port, host, done); });
94
+ // Own the kernel lock before transferring scheduler epochs. Runtime.start
95
+ // resets acceptingRuns after a normal shutdown and starts its dispatch timer.
96
+ recoveryAttempted = true;
97
+ await service.runtime.recover();
98
+ await lock.publish(service.info.serviceId, async (command, argument) => {
99
+ if (command === 'status')
100
+ return status();
101
+ if (command === 'capacity')
102
+ return localCapacity(service.db, argument);
103
+ if (command === 'plugin-register')
104
+ return registerLocalPlugin(service.db, service.info.serviceId, argument ?? '');
105
+ if (command === 'sessions')
106
+ return service.auth.list();
107
+ if (command === 'resolve-client')
108
+ return service.auth.resolveClientPairing(argument ?? '');
109
+ if (command === 'pair-short') {
110
+ const request = JSON.parse(argument ?? '{}');
111
+ return service.auth.approveClientPairing(request.code, request.clientCode);
112
+ }
113
+ if (command === 'revoke-session') {
114
+ service.auth.revokeLocal(argument ?? '');
115
+ return { revoked: argument, acceptedTasksUnaffected: true };
116
+ }
117
+ if (command === 'pair-code' || command === 'pair-client') {
118
+ const control = localControl(lock.root);
119
+ try {
120
+ return await control.pairingCode(command === 'pair-code' ? argument ?? '' : '', command === 'pair-client' ? argument ?? '' : undefined);
121
+ }
122
+ finally {
123
+ control.close();
124
+ }
125
+ }
126
+ if (command === 'backup-create') {
127
+ if (state !== 'running')
128
+ throw new Error('Service is draining; new backups are unavailable');
129
+ const result = await service.api.backups.create(argument);
130
+ await log.append('backup_ready');
131
+ return result;
132
+ }
133
+ await stop(command === 'interrupt-and-stop');
134
+ return { state, serviceId: service.info.serviceId, instanceId: service.instanceId };
135
+ }, service.instanceId);
136
+ await log.append('started');
137
+ }
138
+ catch (error) {
139
+ removeSignals();
140
+ // Before epoch transfer there is no backend owned by this startup. Draining
141
+ // old active rows here would wait forever and could mutate the previous Run.
142
+ // Once recovery was attempted, retain the normal conservative drain path.
143
+ if (!recoveryAttempted)
144
+ await service.abortStartup();
145
+ else
146
+ await service.close();
147
+ try {
148
+ await log.append('startup_failed');
149
+ }
150
+ finally {
151
+ await lock.release();
152
+ }
153
+ throw error;
154
+ }
155
+ process.once('SIGINT', onSignal);
156
+ process.once('SIGTERM', onSignal);
157
+ return { service, lock, status, stop };
158
+ }
159
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1,12 @@
1
+ export declare const digest: (data: Uint8Array | string) => string;
2
+ export declare const identifier: (value: string) => string;
3
+ /** macOS exposes OS-owned /var and /tmp aliases. Normalize only these fixed
4
+ * aliases; arbitrary user-created symlink parents are still forbidden. */
5
+ export declare function normalizeSystemPath(path: string): string;
6
+ /** Check EVERY existing component; never follow a user supplied parent symlink. */
7
+ export declare function safePath(path: string, missing?: boolean): Promise<string>;
8
+ export declare function privateDir(path: string): Promise<string>;
9
+ export declare function readSafe(path: string, max?: number): Promise<Buffer>;
10
+ export declare function syncDir(path: string): Promise<void>;
11
+ export declare function writeNew(path: string, data: Uint8Array | string, mode?: number): Promise<void>;
12
+ export declare function atomicJson(path: string, data: unknown): Promise<void>;
@@ -0,0 +1,84 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, mkdir, open, realpath, rename } from 'node:fs/promises';
3
+ import { dirname, parse, resolve, join } from 'node:path';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ export const digest = (data) => createHash('sha256').update(data).digest('hex');
6
+ export const identifier = (value) => {
7
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(value))
8
+ throw new Error('Invalid operations identifier');
9
+ return value;
10
+ };
11
+ /** macOS exposes OS-owned /var and /tmp aliases. Normalize only these fixed
12
+ * aliases; arbitrary user-created symlink parents are still forbidden. */
13
+ export function normalizeSystemPath(path) {
14
+ const absolute = resolve(path);
15
+ return process.platform === 'darwin' && /^[/](var|tmp)([/]|$)/.test(absolute) ? '/private' + absolute : absolute;
16
+ }
17
+ /** Check EVERY existing component; never follow a user supplied parent symlink. */
18
+ export async function safePath(path, missing = false) {
19
+ const absolute = normalizeSystemPath(path);
20
+ let current = parse(absolute).root;
21
+ for (const part of absolute.slice(current.length).split(/[\\/]/).filter(Boolean)) {
22
+ current = join(current, part);
23
+ try {
24
+ if ((await lstat(current)).isSymbolicLink())
25
+ throw new Error('Operations paths cannot contain symlinks');
26
+ }
27
+ catch (error) {
28
+ if (!missing || error.code !== 'ENOENT')
29
+ throw error;
30
+ }
31
+ }
32
+ return absolute;
33
+ }
34
+ export async function privateDir(path) {
35
+ path = await safePath(path, true);
36
+ await mkdir(path, { recursive: true, mode: 0o700 });
37
+ return realpath(path);
38
+ }
39
+ export async function readSafe(path, max = 512 * 1024 * 1024) {
40
+ path = await safePath(path);
41
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
42
+ try {
43
+ const stat = await file.stat();
44
+ if (!stat.isFile() || stat.size > max)
45
+ throw new Error('Invalid or oversized operations file');
46
+ return await file.readFile();
47
+ }
48
+ finally {
49
+ await file.close();
50
+ }
51
+ }
52
+ export async function syncDir(path) {
53
+ // Windows does not support opening directories through fs.open. File flushes
54
+ // still apply; crash durability of rename must be verified on that platform.
55
+ if (process.platform === 'win32')
56
+ return;
57
+ const file = await open(path, constants.O_RDONLY);
58
+ try {
59
+ await file.sync();
60
+ }
61
+ finally {
62
+ await file.close();
63
+ }
64
+ }
65
+ export async function writeNew(path, data, mode = 0o400) {
66
+ path = await safePath(path, true);
67
+ const file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
68
+ try {
69
+ await file.writeFile(data);
70
+ await file.sync();
71
+ await file.chmod(mode);
72
+ }
73
+ finally {
74
+ await file.close();
75
+ }
76
+ }
77
+ export async function atomicJson(path, data) {
78
+ path = await safePath(path, true);
79
+ const temporary = path + '.' + randomUUID() + '.tmp';
80
+ await writeNew(temporary, JSON.stringify(data), 0o600);
81
+ await rename(temporary, path);
82
+ await syncDir(dirname(path));
83
+ }
84
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1,28 @@
1
+ export interface InstanceRecord {
2
+ instanceId: string;
3
+ serviceId: string;
4
+ token: string;
5
+ port: number;
6
+ pid: number;
7
+ startedAt: string;
8
+ }
9
+ export type ManagementCommand = 'status' | 'stop' | 'interrupt-and-stop' | 'pair-code' | 'pair-client' | 'resolve-client' | 'pair-short' | 'sessions' | 'revoke-session' | 'backup-create' | 'capacity' | 'plugin-register';
10
+ export type ManagementHandler = (command: ManagementCommand, argument?: string) => Promise<unknown>;
11
+ export declare function rootPath(dataDir?: string): string;
12
+ export declare function managementPort(root: string): number;
13
+ /** The kernel-held listener is the lifetime/maintenance lock. No PID killing,
14
+ * stale-file deletion, expiry heuristics, or check-then-create races. Hash port
15
+ * collisions fail closed. Only the loopback address is ever used. */
16
+ export declare function acquireOperationsLock(dataDir?: string): Promise<{
17
+ root: string;
18
+ record: InstanceRecord;
19
+ readonly held: boolean;
20
+ publish(serviceId: string, next: ManagementHandler, instanceId?: string): Promise<void>;
21
+ release(): Promise<void>;
22
+ }>;
23
+ export type OperationsLock = Awaited<ReturnType<typeof acquireOperationsLock>>;
24
+ export declare function readInstance(dataDir?: string): Promise<InstanceRecord>;
25
+ export declare function requestInstance(record: InstanceRecord, command: ManagementCommand, argument?: string): Promise<unknown>;
26
+ export declare function manage(dataDir: string | undefined, command: ManagementCommand, argument?: string): Promise<unknown>;
27
+ export declare function startBackground(cliPath: string, args: string[]): Promise<unknown>;
28
+ export declare function waitUntilStopped(record: InstanceRecord, timeoutMs?: number): Promise<void>;
@@ -0,0 +1,155 @@
1
+ import { createServer, createConnection } from 'node:net';
2
+ import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
3
+ import { join, resolve } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { spawn } from 'node:child_process';
6
+ import { realpath } from 'node:fs/promises';
7
+ import { setTimeout as delay } from 'node:timers/promises';
8
+ import { atomicJson, digest, privateDir, readSafe, safePath, normalizeSystemPath } from './files.js';
9
+ export function rootPath(dataDir) { return normalizeSystemPath(dataDir ?? join(homedir(), '.openworkgraph')); }
10
+ export function managementPort(root) { return 20000 + Number.parseInt(digest(root).slice(0, 8), 16) % 40000; }
11
+ const equal = (a, b) => typeof a === 'string' && Buffer.byteLength(a) === Buffer.byteLength(b) && timingSafeEqual(Buffer.from(a), Buffer.from(b));
12
+ /** The kernel-held listener is the lifetime/maintenance lock. No PID killing,
13
+ * stale-file deletion, expiry heuristics, or check-then-create races. Hash port
14
+ * collisions fail closed. Only the loopback address is ever used. */
15
+ export async function acquireOperationsLock(dataDir) {
16
+ const root = await privateDir(rootPath(dataDir));
17
+ const port = managementPort(root);
18
+ const record = { instanceId: randomUUID(), serviceId: '', token: randomBytes(32).toString('hex'), port, pid: process.pid, startedAt: new Date().toISOString() };
19
+ let handler;
20
+ let released = false;
21
+ const sockets = new Set();
22
+ const server = createServer(socket => {
23
+ sockets.add(socket);
24
+ socket.once('close', () => sockets.delete(socket));
25
+ socket.on('error', () => { });
26
+ socket.setTimeout(2000, () => socket.destroy());
27
+ let input = '';
28
+ let handled = false;
29
+ socket.on('data', chunk => {
30
+ if (handled)
31
+ return;
32
+ input += chunk.toString('utf8');
33
+ if (Buffer.byteLength(input) > 2 * 1024 * 1024) {
34
+ socket.destroy();
35
+ return;
36
+ }
37
+ if (!input.includes('\n'))
38
+ return;
39
+ handled = true;
40
+ void (async () => {
41
+ const request = JSON.parse(input.slice(0, input.indexOf('\n')));
42
+ if (!handler || !equal(request.token, record.token) || request.instanceId !== record.instanceId || request.serviceId !== record.serviceId)
43
+ throw new Error('Unauthenticated instance');
44
+ if (!['status', 'stop', 'interrupt-and-stop', 'pair-code', 'pair-client', 'resolve-client', 'pair-short', 'sessions', 'revoke-session', 'backup-create', 'capacity', 'plugin-register'].includes(String(request.command)))
45
+ throw new Error('Unsupported local command');
46
+ if (request.argument !== undefined && typeof request.argument !== 'string')
47
+ throw new Error('Invalid argument');
48
+ socket.setTimeout(0);
49
+ const result = await handler(request.command, request.argument);
50
+ socket.end(JSON.stringify({ ok: true, instanceId: record.instanceId, serviceId: record.serviceId, result }) + '\n');
51
+ })().catch(() => socket.end(JSON.stringify({ ok: false, error: 'Management request refused' }) + '\n'));
52
+ });
53
+ });
54
+ await new Promise((done, reject) => {
55
+ server.once('error', reject);
56
+ server.listen({ host: '127.0.0.1', port, exclusive: true }, () => { server.removeListener('error', reject); done(); });
57
+ }).catch(error => { throw new Error('Data directory is running or in maintenance, or its management port is occupied', { cause: error }); });
58
+ return {
59
+ root, record, get held() { return !released && server.listening; },
60
+ async publish(serviceId, next, instanceId = record.instanceId) {
61
+ record.serviceId = serviceId;
62
+ record.instanceId = instanceId;
63
+ handler = next;
64
+ await atomicJson(join(root, 'operations-instance.json'), record);
65
+ },
66
+ async release() {
67
+ if (released)
68
+ return;
69
+ released = true;
70
+ handler = undefined;
71
+ for (const socket of sockets)
72
+ socket.destroy();
73
+ await new Promise((done, reject) => server.close(error => error ? reject(error) : done()));
74
+ // Stale metadata is harmless. Retaining it avoids deleting a successor's record.
75
+ },
76
+ };
77
+ }
78
+ export async function readInstance(dataDir) {
79
+ const root = await realpath(await safePath(rootPath(dataDir)));
80
+ const record = JSON.parse((await readSafe(join(root, 'operations-instance.json'), 8192)).toString());
81
+ if (record.port !== managementPort(root) || !/^[a-f0-9]{64}$/.test(record.token))
82
+ throw new Error('Invalid instance metadata');
83
+ return record;
84
+ }
85
+ export async function requestInstance(record, command, argument) {
86
+ return new Promise((done, reject) => {
87
+ const socket = createConnection({ host: '127.0.0.1', port: record.port });
88
+ let input = '';
89
+ let complete = false;
90
+ socket.setTimeout(command === 'backup-create' ? 300000 : 3000, () => socket.destroy(new Error('Local management timed out')));
91
+ socket.once('error', reject);
92
+ socket.once('connect', () => socket.write(JSON.stringify({ ...record, command, argument }) + '\n'));
93
+ socket.on('data', chunk => {
94
+ input += chunk.toString();
95
+ if (input.length > 65536) {
96
+ socket.destroy(new Error('Oversized management response'));
97
+ return;
98
+ }
99
+ if (!input.includes('\n') || complete)
100
+ return;
101
+ complete = true;
102
+ try {
103
+ const response = JSON.parse(input.slice(0, input.indexOf('\n')));
104
+ if (!response.ok || response.serviceId !== record.serviceId || response.instanceId !== record.instanceId)
105
+ throw new Error('Management identity/token mismatch');
106
+ done(response.result);
107
+ }
108
+ catch (error) {
109
+ reject(error);
110
+ }
111
+ finally {
112
+ socket.destroy();
113
+ }
114
+ });
115
+ socket.once('close', () => { if (!complete)
116
+ reject(new Error('Service not running or management unavailable')); });
117
+ });
118
+ }
119
+ export async function manage(dataDir, command, argument) { return requestInstance(await readInstance(dataDir), command, argument); }
120
+ export async function startBackground(cliPath, args) {
121
+ // IPC carries the startup acknowledgment; stdio never inherits a terminal or
122
+ // writes unbounded backend output. The daemon emits allowlisted lifecycle logs.
123
+ const child = spawn(process.execPath, [cliPath, 'serve', ...args], { detached: true, windowsHide: true, stdio: ['ignore', 'ignore', 'ignore', 'ipc'], env: { ...process.env, OPENWORKGRAPH_BACKGROUND: '1' } });
124
+ return new Promise((done, reject) => {
125
+ const timeout = setTimeout(() => { child.disconnect(); child.unref(); reject(new Error('Startup acknowledgment timed out; inspect status before retrying')); }, 15000);
126
+ child.once('error', error => { clearTimeout(timeout); reject(error); });
127
+ child.once('exit', code => { clearTimeout(timeout); reject(new Error('Background startup failed (' + code + ')')); });
128
+ child.once('message', message => {
129
+ clearTimeout(timeout);
130
+ child.disconnect();
131
+ child.unref();
132
+ const result = message;
133
+ if (!result.ok)
134
+ reject(new Error('Background startup refused: ' + (result.error ?? 'inspect status and logs')));
135
+ else
136
+ done(result.status);
137
+ });
138
+ });
139
+ }
140
+ export async function waitUntilStopped(record, timeoutMs = 10000) {
141
+ const start = Date.now();
142
+ while (Date.now() - start < timeoutMs) {
143
+ try {
144
+ await requestInstance(record, 'status');
145
+ }
146
+ catch (error) {
147
+ // An auth failure or closing DB is NOT proof of kernel-lock release.
148
+ if (error.code === 'ECONNREFUSED')
149
+ return;
150
+ }
151
+ await delay(50);
152
+ }
153
+ throw new Error('Service is still draining; interactions/cancel remain available');
154
+ }
155
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1,6 @@
1
+ import { Backups } from './backups.js';
2
+ /** Opens only an EXISTING data directory. No migration or initialization of DB
3
+ * precedes restore verification. The entire local operation excludes serve. */
4
+ export declare function withOfflineBackups<T>(dataDir: string | undefined, work: (backups: Backups) => Promise<T>): Promise<T>;
5
+ /** Read-only catalogue/downloads are allowed while the owning service runs. */
6
+ export declare function withReadBackups<T>(dataDir: string | undefined, work: (backups: Backups) => Promise<T>): Promise<T>;
@@ -0,0 +1,38 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { join } from 'node:path';
3
+ import { initializeDirectories } from '../directories.js';
4
+ import { Backups } from './backups.js';
5
+ import { acquireOperationsLock, rootPath } from './lifecycle.js';
6
+ import { safePath } from './files.js';
7
+ /** Opens only an EXISTING data directory. No migration or initialization of DB
8
+ * precedes restore verification. The entire local operation excludes serve. */
9
+ export async function withOfflineBackups(dataDir, work) {
10
+ const lock = await acquireOperationsLock(dataDir);
11
+ try {
12
+ await safePath(join(lock.root, 'service.sqlite'));
13
+ const directories = initializeDirectories(lock.root);
14
+ const db = new DatabaseSync(directories.database, { allowExtension: false, timeout: 5000 });
15
+ try {
16
+ return await work(new Backups(db, directories, { maintenance: lock }));
17
+ }
18
+ finally {
19
+ db.close();
20
+ }
21
+ }
22
+ finally {
23
+ await lock.release();
24
+ }
25
+ }
26
+ /** Read-only catalogue/downloads are allowed while the owning service runs. */
27
+ export async function withReadBackups(dataDir, work) {
28
+ const root = await safePath(rootPath(dataDir)), database = await safePath(join(root, 'service.sqlite'));
29
+ const directories = { root, database, config: join(root, 'config'), blobs: join(root, 'blobs'), staging: join(root, 'staging'), runs: join(root, 'runs'), backups: join(root, 'backups') };
30
+ const db = new DatabaseSync(database, { readOnly: true, allowExtension: false, timeout: 5000 });
31
+ try {
32
+ return await work(new Backups(db, directories));
33
+ }
34
+ finally {
35
+ db.close();
36
+ }
37
+ }
38
+ //# sourceMappingURL=local-admin.js.map
@@ -0,0 +1,16 @@
1
+ declare const events: readonly ['started', 'draining', 'interrupting', 'stopped', 'startup_failed', 'shutdown_failed', 'backup_ready', 'backup_failed', 'restored'];
2
+ type LogEvent = typeof events[number];
3
+ interface Entry {
4
+ time: string;
5
+ event: LogEvent;
6
+ }
7
+ /** Allowlist, not regex redaction: never persist prompts, paths, tokens, backend
8
+ * stdout or arbitrary errors. Both entry count and read size are bounded. */
9
+ export declare class OperationsLog {
10
+ readonly root: string;
11
+ private pending;
12
+ constructor(root: string);
13
+ read(): Promise<Entry[]>;
14
+ append(event: LogEvent): Promise<void>;
15
+ }
16
+ export {};
@@ -0,0 +1,37 @@
1
+ import { join } from 'node:path';
2
+ import { atomicJson, readSafe } from './files.js';
3
+ const events = ['started', 'draining', 'interrupting', 'stopped', 'startup_failed', 'shutdown_failed', 'backup_ready', 'backup_failed', 'restored'];
4
+ /** Allowlist, not regex redaction: never persist prompts, paths, tokens, backend
5
+ * stdout or arbitrary errors. Both entry count and read size are bounded. */
6
+ export class OperationsLog {
7
+ root;
8
+ pending = Promise.resolve();
9
+ constructor(root) {
10
+ this.root = root;
11
+ }
12
+ async read() {
13
+ try {
14
+ const data = JSON.parse((await readSafe(join(this.root, 'operations-log.json'), 65536)).toString());
15
+ if (!Array.isArray(data))
16
+ return [];
17
+ return data.filter((v) => !!v && typeof v === 'object' && events.includes(v.event) && typeof v.time === 'string' && /^\d{4}-\d{2}-\d{2}T[0-9:.]+Z$/.test(v.time)).slice(-256).map(v => ({ time: v.time, event: v.event }));
18
+ }
19
+ catch (error) {
20
+ if (error.code === 'ENOENT')
21
+ return [];
22
+ throw error;
23
+ }
24
+ }
25
+ append(event) {
26
+ if (!events.includes(event))
27
+ return Promise.reject(new Error('Unknown lifecycle event'));
28
+ const task = this.pending.then(async () => {
29
+ const entries = await this.read();
30
+ entries.push({ time: new Date().toISOString(), event });
31
+ await atomicJson(join(this.root, 'operations-log.json'), entries.slice(-256));
32
+ });
33
+ this.pending = task.catch(() => { });
34
+ return task;
35
+ }
36
+ }
37
+ //# sourceMappingURL=logs.js.map
@@ -0,0 +1,10 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ /** Trusted local declarative registration only: no import(), executable files,
3
+ * dependency installation, or node migrations are implied by registration. */
4
+ export declare function registerLocalPlugin(db: DatabaseSync, serviceId: string, serialized: string): {
5
+ registered: boolean;
6
+ typeId: string;
7
+ pluginId: string;
8
+ version: string;
9
+ schemaVersion: number;
10
+ };
@@ -0,0 +1,11 @@
1
+ import { Graphs } from '../graphs.js';
2
+ import { PluginManagement } from '../plugin-management.js';
3
+ /** Trusted local declarative registration only: no import(), executable files,
4
+ * dependency installation, or node migrations are implied by registration. */
5
+ export function registerLocalPlugin(db, serviceId, serialized) {
6
+ if (Buffer.byteLength(serialized) > 1024 * 1024)
7
+ throw new Error('Plugin manifest exceeds 1 MiB');
8
+ const contract = new PluginManagement(new Graphs(db, serviceId)).registerTrustedLocal(JSON.parse(serialized));
9
+ return { registered: true, typeId: contract.typeId, pluginId: contract.pluginId, version: contract.version, schemaVersion: contract.schemaVersion };
10
+ }
11
+ //# sourceMappingURL=plugins.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { parentPort, workerData } from 'node:worker_threads';
3
+ // Dedicated worker: a large VACUUM must not block HTTP interactions/cancellation.
4
+ const { source, destination } = workerData;
5
+ try {
6
+ const db = new DatabaseSync(source, { readOnly: true, allowExtension: false, timeout: 5000 });
7
+ try {
8
+ db.prepare('VACUUM INTO ?').run(destination);
9
+ }
10
+ finally {
11
+ db.close();
12
+ }
13
+ parentPort.postMessage({ ok: true });
14
+ }
15
+ catch {
16
+ parentPort.postMessage({ ok: false });
17
+ }
18
+ //# sourceMappingURL=snapshot-worker.js.map
@@ -0,0 +1 @@
1
+ export declare function createDatabaseSnapshot(source: string, destination: string): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { safePath } from './files.js';
3
+ export async function createDatabaseSnapshot(source, destination) {
4
+ await safePath(source);
5
+ await safePath(destination, true);
6
+ const worker = new Worker(new URL('./snapshot-worker.js', import.meta.url), { workerData: { source, destination } });
7
+ await new Promise((done, reject) => {
8
+ let ok = false;
9
+ worker.on('message', result => { ok = result?.ok === true; });
10
+ worker.once('error', reject);
11
+ worker.once('exit', code => code === 0 && ok ? done() : reject(new Error('SQLite snapshot worker failed')));
12
+ });
13
+ }
14
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1,26 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readInstance, requestInstance, startBackground, waitUntilStopped } from './lifecycle.js';
3
+ /** Compare the running package against npm's actual global package location. */
4
+ export declare function isGlobalNpmInstallation(cliPath: string, spawnProcess?: SpawnProcess): Promise<boolean>;
5
+ /** The updater must outlive the Runtime that it stops. */
6
+ export declare function startWebUpdate(cliPath: string, spawnProcess?: SpawnProcess, onExit?: () => void): Promise<void>;
7
+ type SpawnProcess = typeof spawn;
8
+ type NpmInvocation = {
9
+ command: string;
10
+ args: string[];
11
+ };
12
+ export declare function compareSemVer(left: string, right: string): number;
13
+ export declare function npmInstallInvocation(platform?: NodeJS.Platform, environment?: NodeJS.ProcessEnv): NpmInvocation;
14
+ export declare function npmVersionInvocation(platform?: NodeJS.Platform, environment?: NodeJS.ProcessEnv): NpmInvocation;
15
+ export declare function latestAgentServiceVersion(spawnProcess?: SpawnProcess): Promise<string>;
16
+ export declare function installGlobalAgentService(outputJson?: boolean, spawnProcess?: SpawnProcess): Promise<void>;
17
+ export interface UpdateDependencies {
18
+ latestAgentServiceVersion: typeof latestAgentServiceVersion;
19
+ readInstance: typeof readInstance;
20
+ requestInstance: typeof requestInstance;
21
+ waitUntilStopped: typeof waitUntilStopped;
22
+ installGlobalAgentService: typeof installGlobalAgentService;
23
+ startBackground: typeof startBackground;
24
+ }
25
+ export declare function updateAgentService(dataDir: string | undefined, cliPath: string, interruptAndStop: boolean, outputJson: boolean, dependencies?: UpdateDependencies): Promise<Record<string, unknown>>;
26
+ export {};