@volter/world-runtime 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/LICENSE +202 -0
  2. package/dist/known-external-services.json +1108 -0
  3. package/dist/src/ancestry.d.ts +2 -0
  4. package/dist/src/ancestry.js +42 -0
  5. package/dist/src/app-url.d.ts +47 -0
  6. package/dist/src/app-url.js +239 -0
  7. package/dist/src/attach.d.ts +48 -0
  8. package/dist/src/attach.js +87 -0
  9. package/dist/src/branch.d.ts +20 -0
  10. package/dist/src/branch.js +65 -0
  11. package/dist/src/browser-proxy-cli.d.ts +2 -0
  12. package/dist/src/browser-proxy-cli.js +41 -0
  13. package/dist/src/ca-trust.d.ts +5 -0
  14. package/dist/src/ca-trust.js +64 -0
  15. package/dist/src/catalog.d.ts +31 -0
  16. package/dist/src/catalog.js +148 -0
  17. package/dist/src/changeset.d.ts +142 -0
  18. package/dist/src/changeset.js +570 -0
  19. package/dist/src/cli.d.ts +2 -0
  20. package/dist/src/cli.js +1262 -0
  21. package/dist/src/command-lifetime.d.ts +15 -0
  22. package/dist/src/command-lifetime.js +98 -0
  23. package/dist/src/configs.d.ts +18 -0
  24. package/dist/src/configs.js +119 -0
  25. package/dist/src/console-apart.d.ts +38 -0
  26. package/dist/src/console-apart.js +107 -0
  27. package/dist/src/consumers.d.ts +46 -0
  28. package/dist/src/consumers.js +200 -0
  29. package/dist/src/covers.d.ts +183 -0
  30. package/dist/src/covers.js +800 -0
  31. package/dist/src/fixture-env.d.ts +42 -0
  32. package/dist/src/fixture-env.js +221 -0
  33. package/dist/src/host-cli.d.ts +2 -0
  34. package/dist/src/host-cli.js +92 -0
  35. package/dist/src/host-fault-fixture.d.ts +32 -0
  36. package/dist/src/host-fault-fixture.js +100 -0
  37. package/dist/src/host-worker.d.ts +1 -0
  38. package/dist/src/host-worker.js +23 -0
  39. package/dist/src/host.d.ts +38 -0
  40. package/dist/src/host.js +135 -0
  41. package/dist/src/index.d.ts +48 -0
  42. package/dist/src/index.js +35 -0
  43. package/dist/src/infra-cli.d.ts +2 -0
  44. package/dist/src/infra-cli.js +136 -0
  45. package/dist/src/init.d.ts +227 -0
  46. package/dist/src/init.js +1117 -0
  47. package/dist/src/inject-map.d.ts +34 -0
  48. package/dist/src/inject-map.js +56 -0
  49. package/dist/src/lifecycle-record.d.ts +47 -0
  50. package/dist/src/lifecycle-record.js +196 -0
  51. package/dist/src/origin.d.ts +31 -0
  52. package/dist/src/origin.js +139 -0
  53. package/dist/src/pack-facts.d.ts +75 -0
  54. package/dist/src/pack-facts.js +98 -0
  55. package/dist/src/pglite-backing.d.ts +21 -0
  56. package/dist/src/pglite-backing.js +158 -0
  57. package/dist/src/pglite-host.mjs +147 -0
  58. package/dist/src/placeholder.d.ts +20 -0
  59. package/dist/src/placeholder.js +100 -0
  60. package/dist/src/prerequisites.d.ts +21 -0
  61. package/dist/src/prerequisites.js +49 -0
  62. package/dist/src/process-groups.d.ts +4 -0
  63. package/dist/src/process-groups.js +49 -0
  64. package/dist/src/project-inspect.d.ts +109 -0
  65. package/dist/src/project-inspect.js +827 -0
  66. package/dist/src/proxy-daemon.d.ts +2 -0
  67. package/dist/src/proxy-daemon.js +18 -0
  68. package/dist/src/redirect-proxy.d.ts +105 -0
  69. package/dist/src/redirect-proxy.js +665 -0
  70. package/dist/src/reflect.d.ts +74 -0
  71. package/dist/src/reflect.js +392 -0
  72. package/dist/src/resources.d.ts +26 -0
  73. package/dist/src/resources.js +22 -0
  74. package/dist/src/root.d.ts +114 -0
  75. package/dist/src/root.js +312 -0
  76. package/dist/src/run-task-worker.d.ts +1 -0
  77. package/dist/src/run-task-worker.js +38 -0
  78. package/dist/src/run-task.d.ts +18 -0
  79. package/dist/src/run-task.js +48 -0
  80. package/dist/src/runtime-test-support.d.ts +59 -0
  81. package/dist/src/runtime-test-support.js +205 -0
  82. package/dist/src/runtime.d.ts +256 -0
  83. package/dist/src/runtime.js +3502 -0
  84. package/dist/src/schema.d.ts +449 -0
  85. package/dist/src/schema.js +605 -0
  86. package/dist/src/serve.d.ts +30 -0
  87. package/dist/src/serve.js +82 -0
  88. package/dist/src/served-world.d.ts +194 -0
  89. package/dist/src/served-world.js +986 -0
  90. package/dist/src/service-exit.d.ts +46 -0
  91. package/dist/src/service-exit.js +195 -0
  92. package/dist/src/service-recorder.d.ts +1 -0
  93. package/dist/src/service-recorder.js +121 -0
  94. package/dist/src/sibling.d.ts +1 -0
  95. package/dist/src/sibling.js +9 -0
  96. package/dist/src/signals.d.ts +1 -0
  97. package/dist/src/signals.js +11 -0
  98. package/dist/src/storage-capacity.d.ts +8 -0
  99. package/dist/src/storage-capacity.js +61 -0
  100. package/dist/src/tail.d.ts +30 -0
  101. package/dist/src/tail.js +160 -0
  102. package/dist/src/tcp-port.d.ts +2 -0
  103. package/dist/src/tcp-port.js +36 -0
  104. package/dist/src/up-task-worker.d.ts +1 -0
  105. package/dist/src/up-task-worker.js +61 -0
  106. package/dist/src/up-task.d.ts +17 -0
  107. package/dist/src/up-task.js +49 -0
  108. package/dist/src/websocket-relay.d.ts +3 -0
  109. package/dist/src/websocket-relay.js +40 -0
  110. package/known-external-services.json +1108 -0
  111. package/package.json +83 -0
  112. package/src/ancestry.ts +36 -0
  113. package/src/app-url.ts +253 -0
  114. package/src/attach.ts +117 -0
  115. package/src/branch.ts +63 -0
  116. package/src/browser-proxy-cli.ts +44 -0
  117. package/src/ca-trust.ts +57 -0
  118. package/src/catalog.ts +156 -0
  119. package/src/changeset.ts +627 -0
  120. package/src/cli.ts +1111 -0
  121. package/src/command-lifetime.ts +79 -0
  122. package/src/configs.ts +110 -0
  123. package/src/console-apart.ts +90 -0
  124. package/src/consumers.ts +185 -0
  125. package/src/covers.ts +934 -0
  126. package/src/fixture-env.ts +230 -0
  127. package/src/host-cli.ts +90 -0
  128. package/src/host-worker.ts +23 -0
  129. package/src/host.ts +169 -0
  130. package/src/index.ts +171 -0
  131. package/src/infra-cli.ts +133 -0
  132. package/src/init.ts +1316 -0
  133. package/src/inject-map.ts +72 -0
  134. package/src/lifecycle-record.ts +168 -0
  135. package/src/origin.ts +134 -0
  136. package/src/pack-facts.ts +128 -0
  137. package/src/pglite-backing.ts +141 -0
  138. package/src/pglite-host.mjs +147 -0
  139. package/src/placeholder.ts +89 -0
  140. package/src/prerequisites.ts +66 -0
  141. package/src/process-groups.ts +33 -0
  142. package/src/project-inspect.ts +770 -0
  143. package/src/proxy-daemon.ts +21 -0
  144. package/src/redirect-proxy.ts +684 -0
  145. package/src/reflect.ts +440 -0
  146. package/src/resources.ts +22 -0
  147. package/src/root.ts +290 -0
  148. package/src/run-task-worker.ts +27 -0
  149. package/src/run-task.ts +44 -0
  150. package/src/runtime-test-support.ts +208 -0
  151. package/src/runtime.ts +3357 -0
  152. package/src/schema.ts +922 -0
  153. package/src/serve.ts +102 -0
  154. package/src/served-world.ts +812 -0
  155. package/src/service-exit.ts +175 -0
  156. package/src/service-recorder.ts +89 -0
  157. package/src/sibling.ts +10 -0
  158. package/src/signals.ts +10 -0
  159. package/src/storage-capacity.ts +60 -0
  160. package/src/tail.ts +205 -0
  161. package/src/tcp-port.ts +35 -0
  162. package/src/up-task-worker.ts +40 -0
  163. package/src/up-task.ts +45 -0
  164. package/src/websocket-relay.ts +32 -0
@@ -0,0 +1,34 @@
1
+ /** The slice of `@volter/world-core/inject` reused here (data + pure functions only). */
2
+ export type InjectModule = {
3
+ readMap(env: Record<string, string | undefined>): Record<string, string>;
4
+ /** `pathname` is optional; it only disambiguates hosts two vendors share (see inject.cjs). */
5
+ resolveTwin(host: string, map: Record<string, string>, pathname?: string): {
6
+ vendor: string;
7
+ origin: string;
8
+ } | null;
9
+ /** `S3_TWIN_URL` → 's3'; a `*_TWIN_URL` name readMap would never read → null. */
10
+ twinUrlVendor(name: string): string | null;
11
+ /** the headers a request routed to the application carries: the host and scheme it was made with */
12
+ appForwardHeaders(url: URL): Record<string, string>;
13
+ VENDOR_HOSTS: Record<string, (host: string, pathname?: string) => boolean>;
14
+ restore(): void;
15
+ };
16
+ /** Lazily load the injector's host→twin table. Loading the CJS auto-installs its http/fetch
17
+ * patches as a side effect; we immediately `restore()` so requiring it here is inert (we only
18
+ * want the data + pure functions, not to patch THIS process). */
19
+ export declare function loadInject(): InjectModule;
20
+ /** The vendor keys the injector can redirect for (the keys of VENDOR_HOSTS). */
21
+ export declare function injectableVendorKeys(): Set<string>;
22
+ /** Does the injector actually read this env var name? (`S3_TWIN_URL` → 's3'; `AWS_TWIN_URL`
23
+ * → null — the consolidated aws twin answers under the s3/dynamodb/timestream keys.) */
24
+ export declare function twinUrlVendorFor(name: string): string | null;
25
+ /** WARN lines for twin-shaped injectEnv vars the injector will never read — an injectEnv like
26
+ * `AWS_TWIN_URL` is written into the world env, looks wired, and does NOTHING (readMap iterates
27
+ * VENDOR_HOSTS keys looking for `<VENDOR>_TWIN_URL`; there is no `aws` vendor key). `up` prints
28
+ * these loudly so the disagreement is visible at boot instead of surfacing as silently-real
29
+ * vendor traffic later. Vars that do not end in `_TWIN_URL` are app-read config (SUPABASE_URL,
30
+ * LIVEKIT_URL, …), not injector input — never warned about. */
31
+ export declare function inertInjectEnvWarnings(services: Array<{
32
+ id: string;
33
+ injectEnv?: string;
34
+ }>): string[];
@@ -0,0 +1,56 @@
1
+ // The ONE world-runtime home for the injector's host→twin knowledge (`@volter/world-core/inject`:
2
+ // `VENDOR_HOSTS` + `readMap` + `resolveTwin` + `twinUrlVendor`). The redirect proxy, `covers`,
3
+ // and `up`'s inert-injectEnv warning all read the injector THROUGH this module, so none of them
4
+ // can re-encode (and silently drift from) what the injector actually redirects — the exact
5
+ // failure mode of the LibreChat blind-adoption run, where a world injected AWS_TWIN_URL that no
6
+ // injector vendor reads and nothing said so.
7
+ import { createRequire } from 'node:module';
8
+ let injectCache = null;
9
+ /** Lazily load the injector's host→twin table. Loading the CJS auto-installs its http/fetch
10
+ * patches as a side effect; we immediately `restore()` so requiring it here is inert (we only
11
+ * want the data + pure functions, not to patch THIS process). */
12
+ export function loadInject() {
13
+ if (injectCache)
14
+ return injectCache;
15
+ const require = createRequire(import.meta.url);
16
+ const mod = require('@volter/world-core/inject');
17
+ try {
18
+ mod.restore();
19
+ }
20
+ catch { /* nothing was installed */ }
21
+ injectCache = mod;
22
+ return mod;
23
+ }
24
+ /** The vendor keys the injector can redirect for (the keys of VENDOR_HOSTS). */
25
+ export function injectableVendorKeys() {
26
+ return new Set(Object.keys(loadInject().VENDOR_HOSTS));
27
+ }
28
+ /** Does the injector actually read this env var name? (`S3_TWIN_URL` → 's3'; `AWS_TWIN_URL`
29
+ * → null — the consolidated aws twin answers under the s3/dynamodb/timestream keys.) */
30
+ export function twinUrlVendorFor(name) {
31
+ return loadInject().twinUrlVendor(name);
32
+ }
33
+ /** WARN lines for twin-shaped injectEnv vars the injector will never read — an injectEnv like
34
+ * `AWS_TWIN_URL` is written into the world env, looks wired, and does NOTHING (readMap iterates
35
+ * VENDOR_HOSTS keys looking for `<VENDOR>_TWIN_URL`; there is no `aws` vendor key). `up` prints
36
+ * these loudly so the disagreement is visible at boot instead of surfacing as silently-real
37
+ * vendor traffic later. Vars that do not end in `_TWIN_URL` are app-read config (SUPABASE_URL,
38
+ * LIVEKIT_URL, …), not injector input — never warned about. */
39
+ export function inertInjectEnvWarnings(services) {
40
+ const warnings = [];
41
+ for (const service of services) {
42
+ const name = service.injectEnv;
43
+ if (!name || !/_TWIN_URL$/.test(name))
44
+ continue;
45
+ if (twinUrlVendorFor(name) !== null)
46
+ continue;
47
+ const stem = name.replace(/_TWIN_URL$/, '');
48
+ const hint = stem.toUpperCase() === 'AWS'
49
+ ? ' The consolidated aws twin is read under its per-AREA keys — S3_TWIN_URL / DYNAMODB_TWIN_URL / TIMESTREAM_TWIN_URL / SESV2_TWIN_URL / SECRETSMANAGER_TWIN_URL / BEDROCK_TWIN_URL — point those at the aws twin instead.'
50
+ : '';
51
+ warnings.push(`[volter-world] WARN: service "${service.id}" injects ${name}, but the injector reads no such vendor` +
52
+ ` ("${stem.toLowerCase()}" is not a VENDOR_HOSTS key in @volter/world-core/inject) — the var is INERT and this` +
53
+ ` vendor's SDK traffic will NOT be redirected to the twin.${hint}`);
54
+ }
55
+ return warnings;
56
+ }
@@ -0,0 +1,47 @@
1
+ export type WorldLifecycleRecord = {
2
+ version: 1;
3
+ world: string;
4
+ root: string;
5
+ identity: string;
6
+ instanceCreatedAt: string;
7
+ owner?: string;
8
+ ownerUid?: number;
9
+ ownerPid: number;
10
+ hostname: string;
11
+ startedAt: string;
12
+ phase: 'starting' | 'running' | 'cleanup-pending';
13
+ pids: number[];
14
+ log: string;
15
+ };
16
+ export declare const lifecycleLogPath: (root: string, world: string) => string;
17
+ /** Called under the instance lock, before replacing data. Only this identity is read. */
18
+ export declare function assertLifecycleStartable(root: string, world: string): void;
19
+ /** A missing process is not successful teardown; records persist until explicit release. */
20
+ export declare function beginWorldLifecycle(root: string, world: string, instanceCreatedAt: string, owner?: string): void;
21
+ export declare function handoffWorldLifecycle(root: string, world: string, pids: number[]): void;
22
+ export declare function retainWorldLifecycleForCleanup(root: string, world: string): void;
23
+ export declare function recordWorldLifecycleEvent(root: string, world: string, message: string): void;
24
+ /** Called only after verified cleanup. Never touches legacy records. */
25
+ export declare function releaseWorldLifecycle(root: string, world: string): void;
26
+ /**
27
+ * Releases the record of a World whose instance metadata is gone when nothing it
28
+ * recorded can still run: the record is this host's and this user's, and its owner
29
+ * and every process it handed off have exited. There is nothing left to tear down,
30
+ * so the record is the only thing left to release. Anything alive, another host's,
31
+ * another user's, or unreadable keeps the record. Returns whether it released.
32
+ */
33
+ export declare function releaseAbandonedWorldLifecycle(root: string, world: string): boolean;
34
+ export declare function hasWorldLifecycle(root: string, world: string): boolean;
35
+ export declare function inspectWorldLifecycles(): {
36
+ worlds: WorldLifecycleRecord[];
37
+ legacyRecords: {
38
+ path: string;
39
+ world?: string;
40
+ root?: string;
41
+ cleanupPending?: boolean;
42
+ readable: boolean;
43
+ }[];
44
+ warnings: string[];
45
+ };
46
+ /** Runtime phase is meaningful only for the same instance generation; it is not a health probe. */
47
+ export declare function isWorldLifecycleRunning(root: string, world: string, instanceCreatedAt: string): boolean;
@@ -0,0 +1,196 @@
1
+ // Durable ownership of one World. This inventory does not allocate or reserve capacity.
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { volterHome } from '@volter/world-core';
5
+ import { hostname } from 'node:os';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { stateDirName, withFileLock } from '@volter/world-core';
8
+ import { checkedOwner } from "./consumers.js";
9
+ const directory = () => join(volterHome(), 'world-lifecycles');
10
+ const identity = (root, world) => createHash('sha256').update(`${resolve(root)}\0${world}`).digest('hex');
11
+ const recordPath = (root, world) => join(directory(), `${identity(root, world)}.json`);
12
+ const legacyPath = (root, world) => join(volterHome(), 'world-resource-claims', `${identity(root, world)}.json`);
13
+ export const lifecycleLogPath = (root, world) => join(resolve(root), stateDirName(), 'worlds', '.lifecycle', `${world}.log`);
14
+ function read(path) {
15
+ const record = JSON.parse(readFileSync(path, 'utf8'));
16
+ if (record?.version !== 1 || typeof record.world !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(record.world)
17
+ || typeof record.root !== 'string' || !record.root || record.identity !== identity(record.root, record.world)
18
+ || !Number.isFinite(Date.parse(record.instanceCreatedAt)) || !Number.isFinite(Date.parse(record.startedAt))
19
+ || !Number.isInteger(record.ownerPid) || record.ownerPid <= 0 || typeof record.hostname !== 'string'
20
+ || !['starting', 'running', 'cleanup-pending'].includes(record.phase) || !Array.isArray(record.pids)
21
+ || record.pids.some(pid => !Number.isInteger(pid) || pid <= 0)
22
+ || record.log !== lifecycleLogPath(record.root, record.world))
23
+ throw new Error('invalid lifecycle record');
24
+ checkedOwner(record.owner);
25
+ return record;
26
+ }
27
+ function write(path, record) {
28
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
29
+ const temporary = `${path}.${randomUUID()}.tmp`;
30
+ try {
31
+ writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
32
+ renameSync(temporary, path);
33
+ }
34
+ finally {
35
+ rmSync(temporary, { force: true });
36
+ }
37
+ }
38
+ /** Called under the instance lock, before replacing data. Only this identity is read. */
39
+ export function assertLifecycleStartable(root, world) {
40
+ if (existsSync(legacyPath(root, world)))
41
+ throw new Error(`World "${world}" has a legacy ownership record; finish teardown using its pinned old runtime before migration. Record retained: ${legacyPath(root, world)}`);
42
+ if (existsSync(recordPath(root, world)))
43
+ throw new Error(`World "${world}" still owns a lifecycle record; complete volter-world down before replacing it. Record retained: ${recordPath(root, world)}`);
44
+ }
45
+ /** A missing process is not successful teardown; records persist until explicit release. */
46
+ export function beginWorldLifecycle(root, world, instanceCreatedAt, owner) {
47
+ checkedOwner(owner);
48
+ assertLifecycleStartable(root, world);
49
+ const record = {
50
+ version: 1, world, root: resolve(root), identity: identity(root, world), instanceCreatedAt,
51
+ owner, ownerUid: process.getuid?.(), ownerPid: process.pid, hostname: hostname(),
52
+ startedAt: new Date().toISOString(), phase: 'starting', pids: [], log: lifecycleLogPath(root, world),
53
+ };
54
+ recordWorldLifecycleEvent(root, world, 'recording startup ownership; no capacity reserved');
55
+ write(recordPath(root, world), record);
56
+ }
57
+ function update(root, world, change) {
58
+ const path = recordPath(root, world);
59
+ withFileLock(`${path}.lock`, () => {
60
+ if (!existsSync(path))
61
+ throw new Error(`World "${world}" lifecycle record is missing; ownership update refused`);
62
+ const record = read(path);
63
+ if (record.identity !== identity(root, world))
64
+ throw new Error('World lifecycle identity mismatch');
65
+ write(path, change(record));
66
+ });
67
+ }
68
+ export function handoffWorldLifecycle(root, world, pids) {
69
+ update(root, world, record => ({ ...record, phase: 'running', pids: [...new Set(pids.filter(pid => pid > 0))] }));
70
+ }
71
+ export function retainWorldLifecycleForCleanup(root, world) {
72
+ if (existsSync(legacyPath(root, world)))
73
+ throw new Error(`World "${world}" has legacy ownership; use its pinned runtime for teardown`);
74
+ if (!existsSync(recordPath(root, world)))
75
+ return;
76
+ update(root, world, record => ({ ...record, phase: 'cleanup-pending' }));
77
+ }
78
+ export function recordWorldLifecycleEvent(root, world, message) {
79
+ const path = lifecycleLogPath(root, world);
80
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
81
+ writeFileSync(path, `${new Date().toISOString()} ${message}\n`, { flag: 'a', mode: 0o600 });
82
+ }
83
+ /** Called only after verified cleanup. Never touches legacy records. */
84
+ export function releaseWorldLifecycle(root, world) {
85
+ const path = recordPath(root, world);
86
+ if (!existsSync(path))
87
+ return;
88
+ withFileLock(`${path}.lock`, () => {
89
+ if (!existsSync(path))
90
+ return;
91
+ const record = read(path);
92
+ if (record.identity !== identity(root, world))
93
+ throw new Error('World lifecycle identity mismatch');
94
+ // The receipt line is evidence, not a precondition: failing to append it must not keep ownership (and so the next
95
+ // `up`) blocked after a verified teardown.
96
+ try {
97
+ recordWorldLifecycleEvent(root, world, 'teardown verified; ownership released');
98
+ }
99
+ catch { /* released regardless */ }
100
+ rmSync(path);
101
+ });
102
+ }
103
+ /**
104
+ * Releases the record of a World whose instance metadata is gone when nothing it
105
+ * recorded can still run: the record is this host's and this user's, and its owner
106
+ * and every process it handed off have exited. There is nothing left to tear down,
107
+ * so the record is the only thing left to release. Anything alive, another host's,
108
+ * another user's, or unreadable keeps the record. Returns whether it released.
109
+ */
110
+ export function releaseAbandonedWorldLifecycle(root, world) {
111
+ const path = recordPath(root, world);
112
+ if (!existsSync(path))
113
+ return false;
114
+ return withFileLock(`${path}.lock`, () => {
115
+ if (!existsSync(path))
116
+ return false;
117
+ let record;
118
+ try {
119
+ record = read(path);
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ if (record.identity !== identity(root, world) || record.hostname !== hostname())
125
+ return false;
126
+ if (record.ownerUid !== undefined && record.ownerUid !== process.getuid?.())
127
+ return false;
128
+ if ([record.ownerPid, ...record.pids].some((pid) => processExists(pid)))
129
+ return false;
130
+ try {
131
+ recordWorldLifecycleEvent(root, world, 'instance metadata gone and every recorded process exited; ownership released');
132
+ }
133
+ catch { /* the root may be gone with it */ }
134
+ rmSync(path);
135
+ return true;
136
+ });
137
+ }
138
+ function processExists(pid) {
139
+ try {
140
+ process.kill(pid, 0);
141
+ return true;
142
+ }
143
+ catch (cause) {
144
+ return cause.code === 'EPERM';
145
+ }
146
+ }
147
+ export function hasWorldLifecycle(root, world) {
148
+ return existsSync(recordPath(root, world)) || existsSync(legacyPath(root, world));
149
+ }
150
+ export function inspectWorldLifecycles() {
151
+ const worlds = [];
152
+ const warnings = [];
153
+ for (const file of existsSync(directory()) ? readdirSync(directory()).sort() : []) {
154
+ if (!file.endsWith('.json'))
155
+ continue;
156
+ try {
157
+ const record = read(join(directory(), file));
158
+ if (file !== `${record.identity}.json`)
159
+ throw new Error('filename identity mismatch');
160
+ worlds.push(record);
161
+ }
162
+ catch {
163
+ warnings.push(`Unreadable lifecycle record ${file}; retained, ownership unknown.`);
164
+ }
165
+ }
166
+ const legacyDirectory = join(volterHome(), 'world-resource-claims');
167
+ const legacyRecords = [];
168
+ for (const file of existsSync(legacyDirectory) ? readdirSync(legacyDirectory).sort() : []) {
169
+ if (!file.endsWith('.json'))
170
+ continue;
171
+ const path = join(legacyDirectory, file);
172
+ try {
173
+ const old = JSON.parse(readFileSync(path, 'utf8'));
174
+ const root = old.root ?? (typeof old.log === 'string' ? resolve(dirname(old.log), '../../..') : undefined);
175
+ if (typeof root !== 'string' || typeof old.world !== 'string' || identity(root, old.world) !== old.identity
176
+ || file !== `${old.identity}.json`)
177
+ throw new Error('unknown identity');
178
+ legacyRecords.push({ path, world: old.world, root, cleanupPending: old.cleanupPending === true, readable: true });
179
+ }
180
+ catch {
181
+ legacyRecords.push({ path, readable: false });
182
+ warnings.push(`Unreadable legacy record ${file}; retained, not charged as capacity.`);
183
+ }
184
+ }
185
+ return { worlds, legacyRecords, warnings };
186
+ }
187
+ /** Runtime phase is meaningful only for the same instance generation; it is not a health probe. */
188
+ export function isWorldLifecycleRunning(root, world, instanceCreatedAt) {
189
+ try {
190
+ const record = read(recordPath(root, world));
191
+ return record.identity === identity(root, world) && record.instanceCreatedAt === instanceCreatedAt && record.phase === 'running';
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ }
@@ -0,0 +1,31 @@
1
+ import { type HistoryReference } from '@volter/world-core';
2
+ import type { WorldInstance } from './schema.js';
3
+ export type FetchOptions = {
4
+ root?: string;
5
+ url?: string;
6
+ namespace?: string;
7
+ key: string;
8
+ full?: boolean;
9
+ services?: string[];
10
+ views?: Record<string, HistoryReference>;
11
+ now?: Date;
12
+ };
13
+ export type FetchOutcome = {
14
+ /** per twin: entries in the cache past the branch's position — what a rebase folds */
15
+ snapshots: Record<string, HistoryReference & {
16
+ remoteView?: string;
17
+ }>;
18
+ behind: Record<string, number>;
19
+ world: string;
20
+ origin: {
21
+ url: string;
22
+ namespace: string;
23
+ };
24
+ full: boolean;
25
+ appended: Record<string, number>;
26
+ files?: number;
27
+ };
28
+ /** Bring in what the origin has past this world's recorded position, per twin; `full` starts from
29
+ * zero and records the origin as this world's. */
30
+ export declare function fetchFromOrigin(name: string, options: FetchOptions): Promise<FetchOutcome>;
31
+ export declare function worldOrigin(name: string, root?: string): WorldInstance['origin'] | null;
@@ -0,0 +1,139 @@
1
+ import { captureHistory, historyDigest, publishOriginHistory, readHistoryView, historyLength, worldPaths } from '@volter/world-core';
2
+ // `volter world clone` and `fetch` — a world's ORIGIN is another world, served (docs/concepts/the-model.md
3
+ // #a-branch-is-a-position-not-a-copy: a clone is a branch whose parent is at a URL): its whole log per twin is this world's PARENT. `clone` records the origin
4
+ // and brings every twin's log in from position zero; `fetch` appends what the origin has past the
5
+ // recorded position. Nothing here touches a vendor: an origin is always a world, the token is the
6
+ // served world's, and what arrives are entries — the parent log grows, the branch log is untouched.
7
+ import { join, resolve } from 'node:path';
8
+ import { getActiveWorldStore, readBranchMeta, withAncestryLock, writeBranchMeta } from '@volter/world-core';
9
+ import { saveWorldInstance, statusWorld } from "./runtime.js";
10
+ import { fetchReady, isPathRemote, localWorld, TOKEN_HEADER } from "./served-world.js";
11
+ function originOf(instance, options) {
12
+ const url = options.url ?? instance.origin?.url;
13
+ const namespace = options.namespace ?? instance.origin?.namespace;
14
+ if (!url || !namespace)
15
+ throw new Error(`World "${instance.name}" has no origin recorded — \`volter remote add origin <url>\` or \`volter world clone <url>\` first`);
16
+ if (!isPathRemote(url) && !/^[a-z0-9_-]+\/[a-z0-9_-]+$/i.test(namespace))
17
+ throw new Error(`a served world is <org>/<world>, got ${JSON.stringify(namespace)}`);
18
+ return { url: url.replace(/\/+$/, ''), namespace };
19
+ }
20
+ /** A twin's whole log from the origin, from a position: over http from a served world's door, or
21
+ * straight off the disk of a world at a path. */
22
+ /** The state service a twin records under: its own name, else the one state dir under its control root (slack → chat). */
23
+ function stateServiceOf(controlRoot, service) {
24
+ const stateRoot = join(controlRoot, '.volter', 'world');
25
+ const store = getActiveWorldStore();
26
+ const states = store.list(stateRoot).filter((name) => store.stat(join(stateRoot, name))?.isDirectory);
27
+ return states.includes(service) ? service : states.length === 1 ? states[0] : service;
28
+ }
29
+ async function pageOf(origin, service, after, key, view) {
30
+ return door(origin, `log/${service}?after=${after}&limit=500${view ? `&view=${encodeURIComponent(view)}` : ''}`, key);
31
+ }
32
+ async function door(origin, path, key) {
33
+ const res = await fetchReady(`${origin.url}/-/${origin.namespace}/${path}`, { headers: { [TOKEN_HEADER]: key } });
34
+ if (!res.ok)
35
+ throw new Error(`origin ${origin.url}/-/${origin.namespace}/${path} answered ${res.status}: ${(await res.text()).slice(0, 200)}`);
36
+ return (await res.json());
37
+ }
38
+ /** Bring in what the origin has past this world's recorded position, per twin; `full` starts from
39
+ * zero and records the origin as this world's. */
40
+ export async function fetchFromOrigin(name, options) {
41
+ const root = resolve(options.root ?? process.cwd());
42
+ const instance = statusWorld(name, root);
43
+ const origin = originOf(instance, options);
44
+ const at_ = (options.now ?? new Date()).toISOString();
45
+ const services = (options.services ?? Object.keys(instance.services)).sort();
46
+ const appended = {};
47
+ const behind = {};
48
+ const snapshots = {};
49
+ withAncestryLock(() => {
50
+ for (const service of services) {
51
+ const controlRoot = join(instance.dirs.data, service);
52
+ const meta = readBranchMeta(stateServiceOf(controlRoot, service), controlRoot);
53
+ if (meta?.parent && !meta.origin)
54
+ throw new Error('Cannot attach a first origin to an existing local fork; clone the origin before branching');
55
+ }
56
+ });
57
+ for (const service of services) {
58
+ const controlRoot = join(instance.dirs.data, service);
59
+ const state = stateServiceOf(controlRoot, service);
60
+ if (isPathRemote(origin.url)) {
61
+ // A PARENT ON THIS MACHINE is read live through the pointer, like a local branch (contract
62
+ // "Just like Neon", 1): nothing is copied; a clone points at the parent's head, a fetch only
63
+ // says how far the parent has moved past this branch's position
64
+ withAncestryLock(() => {
65
+ const target = localWorld(origin.url);
66
+ const at = join(target.instance.dirs.data, service);
67
+ const parentState = stateServiceOf(at, service);
68
+ const head = options.views?.[service] ?? captureHistory(parentState, at);
69
+ const parentLength = historyLength(readHistoryView(worldPaths(parentState, at).dir, head.view).layout);
70
+ if (head.position !== parentLength)
71
+ throw new Error('Origin view and position disagree');
72
+ snapshots[service] = { view: head.view, position: parentLength };
73
+ const meta = readBranchMeta(state, controlRoot);
74
+ if (meta?.parent && !meta.origin)
75
+ throw new Error('Cannot attach a first origin to an existing local fork; clone the origin before branching');
76
+ const before = meta?.origin?.position ?? meta?.parent?.position ?? 0;
77
+ const descriptor = readHistoryView(worldPaths(parentState, at).dir, head.view);
78
+ const tracking = { at, directory: descriptor.owner.directory, generation: descriptor.owner.generation, view: head.view, position: parentLength, depth: 0 };
79
+ // a world with no pointer adopts this origin as its parent: a clone starts at the origin's head,
80
+ // a world that only added the remote starts at zero and rebases onto it
81
+ if (!meta?.parent)
82
+ writeBranchMeta(state, { parent: { at, position: options.full ? parentLength : 0, view: head.view }, origin: { ...tracking, ...(!options.full ? { position: 0, incomplete: true } : {}) }, fetchedOrigin: tracking, branchedAt: at_ }, controlRoot);
83
+ else
84
+ writeBranchMeta(state, { ...meta, fetchedOrigin: tracking }, controlRoot);
85
+ const selected = readBranchMeta(state, controlRoot);
86
+ const pointer = selected?.origin ?? selected?.parent;
87
+ appended[service] = Math.max(0, parentLength - before);
88
+ behind[service] = pointer ? Math.max(0, parentLength - pointer.position) || (pointer.view === head.view ? 0 : 1) : 0;
89
+ });
90
+ continue;
91
+ }
92
+ // Download one immutable view, then publish its complete cache head. No mixed pages.
93
+ const requested = options.views?.[service];
94
+ const first = await pageOf(origin, service, 0, options.key, requested?.view);
95
+ if (requested && (first.view !== requested.view || first.position !== requested.position))
96
+ throw new Error('Origin did not return the requested history snapshot');
97
+ if (!first.view || !Number.isInteger(first.position) || first.position < 0 || first.after !== 0)
98
+ throw new Error('Origin did not return an immutable history view');
99
+ const entries = [];
100
+ let page = first;
101
+ for (;;) {
102
+ if (page.view !== first.view || page.position !== first.position || page.after !== entries.length || page.state !== first.state || page.digest !== first.digest || historyDigest(page.layout) !== historyDigest(first.layout))
103
+ throw new Error('Origin changed history view during pagination');
104
+ entries.push(...page.entries);
105
+ if (entries.length > first.position)
106
+ throw new Error('Origin history exceeds its declared position');
107
+ if (entries.length === first.position)
108
+ break;
109
+ if (page.entries.length === 0)
110
+ throw new Error('Origin history page ended before its declared position');
111
+ page = await pageOf(origin, service, entries.length, options.key, first.view);
112
+ }
113
+ if (historyDigest({ layout: first.layout, entries }) !== first.digest)
114
+ throw new Error('Origin history digest mismatch');
115
+ withAncestryLock(() => {
116
+ const meta = readBranchMeta(state, controlRoot);
117
+ if (meta?.parent && !meta.origin)
118
+ throw new Error('Cannot attach a first origin to an existing local fork; clone the origin before branching');
119
+ const cached = publishOriginHistory(state, controlRoot, first.view, first.layout, entries);
120
+ appended[service] = cached.appended;
121
+ snapshots[service] = { view: cached.view, position: entries.length, remoteView: first.view };
122
+ const descriptor = readHistoryView(worldPaths(state, controlRoot).dir, cached.view);
123
+ const tracking = { at: `${origin.url}/${origin.namespace}`, directory: descriptor.owner.directory, generation: descriptor.owner.generation, view: cached.view, position: entries.length, remoteView: first.view, depth: 0 };
124
+ if (!meta?.parent)
125
+ writeBranchMeta(state, { parent: { at: tracking.at, position: options.full ? entries.length : 0, view: cached.view, remoteView: first.view }, origin: { ...tracking, ...(!options.full ? { position: 0, incomplete: true } : {}) }, fetchedOrigin: tracking, branchedAt: at_ }, controlRoot);
126
+ else
127
+ writeBranchMeta(state, { ...meta, fetchedOrigin: tracking }, controlRoot);
128
+ const selected = readBranchMeta(state, controlRoot);
129
+ const pointer = selected?.origin ?? selected?.parent;
130
+ // Changed same-length snapshots are still drift; offsets are scoped to a view.
131
+ behind[service] = pointer ? Math.max(0, entries.length - pointer.position) || (pointer.remoteView === first.view ? 0 : 1) : 0;
132
+ });
133
+ }
134
+ saveWorldInstance({ ...instance, origin: { ...origin, ...(options.full ? { clonedAt: at_ } : instance.origin?.clonedAt ? { clonedAt: instance.origin.clonedAt } : {}), fetchedAt: at_ } });
135
+ return { world: name, origin, full: options.full ?? false, appended, behind, snapshots };
136
+ }
137
+ export function worldOrigin(name, root) {
138
+ return statusWorld(name, resolve(root ?? process.cwd())).origin ?? null;
139
+ }
@@ -0,0 +1,75 @@
1
+ export type PackAdoption = {
2
+ sdks?: string[];
3
+ pypi?: string[];
4
+ scopes?: string[];
5
+ envStems?: string[];
6
+ worldIds?: string[];
7
+ tools?: Array<{
8
+ package: string;
9
+ usage: 'build' | 'deployment';
10
+ }>;
11
+ };
12
+ export type PackFacts = {
13
+ /** Existing descriptor transport, compiled into the same artifact as endpoint/adoption facts. */
14
+ transport?: string;
15
+ /** Optional native frontend of the same state owner, selected explicitly on the pack CLI. */
16
+ nativeTransport?: {
17
+ protocol: string;
18
+ flag: string;
19
+ upstreamEnv: string;
20
+ };
21
+ adoption?: PackAdoption;
22
+ hosts?: Array<{
23
+ host?: string;
24
+ suffix?: string;
25
+ hostPattern?: string;
26
+ pathPattern?: string;
27
+ key?: string;
28
+ exclude?: true;
29
+ }>;
30
+ hostsNone?: string;
31
+ endpointEnv?: {
32
+ name: string;
33
+ templates?: Record<string, string>;
34
+ note: string;
35
+ };
36
+ endpointEnvNone?: string;
37
+ /** The pack's colocatable `create<Name>TwinServer` factory export, derived (or descriptor-
38
+ * declared under ambiguity) by scripts/pack-facts.ts. Consumed by `init` to emit
39
+ * `colocate:` service entries so a world's twins boot inside ONE host process (R2a). */
40
+ serveExport?: string;
41
+ /** R16: the protocol major the package targets and its standing against the kernel that compiled the artifact. */
42
+ protocol?: {
43
+ declared?: string;
44
+ major: number;
45
+ standing: 'current' | 'deprecated' | 'refused' | 'assumed';
46
+ };
47
+ };
48
+ export declare function packFacts(): Record<string, PackFacts>;
49
+ /** Overlay descriptor-declared npm SDK names onto the central SDK_TWINS map (project-inspect.ts). */
50
+ /** The PyPI half of the SDK map, built the same way from each descriptor's `adoption.pypi`
51
+ * (PEP 503 names). One name belongs to one pack; a second claim refuses at load. */
52
+ export declare function overlayPypiTwins(pypiTwins: Record<string, {
53
+ vendor: string;
54
+ twin: string;
55
+ }>): void;
56
+ /** PEP 503: case-insensitive, runs of `-_.` collapse to one `-`. */
57
+ export declare function normalizePypiName(name: string): string;
58
+ export declare function overlaySdkTwins(sdkTwins: Record<string, {
59
+ vendor: string;
60
+ twin: string;
61
+ }>): void;
62
+ /** Overlay descriptor-declared scopes/env stems/world ids onto covers.ts's central maps. */
63
+ export declare function overlayCoversMaps(maps: {
64
+ scopeVendors: Record<string, string>;
65
+ envStemVendors: Record<string, string>;
66
+ vendorWorldIds: Record<string, string[]>;
67
+ }): void;
68
+ /** Overlay descriptor-declared endpoint-env wiring for packs with no injectable host.
69
+ * Packs that also declare hosts are wired through the injector; init reads their optional
70
+ * endpoint templates directly from pack facts without misclassifying them as app-read-only. */
71
+ export declare function overlayEndpointEnv(table: Record<string, {
72
+ injectEnv?: string;
73
+ injectEnvTemplates?: Record<string, string>;
74
+ note: string;
75
+ }>): void;