super-dsh 0.1.3 → 0.1.4

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 (115) hide show
  1. package/README.md +9 -8
  2. package/agent-agy/cordis.patch.yml +8 -4
  3. package/agent-agy/dist/agent-preset-agy.d.ts +23 -43
  4. package/agent-agy/dist/agent-preset-agy.js +29 -166
  5. package/agent-agy/dist/agent.d.ts +2 -1
  6. package/agent-agy/dist/agent.js +3 -2
  7. package/agent-agy/dist/agy-sessions.d.ts +2 -1
  8. package/agent-agy/dist/agy-sessions.js +2 -11
  9. package/agent-agy/dist/index.js +13 -12
  10. package/agent-agy/dist/world-plugin.d.ts +2 -1
  11. package/agent-agy/dist/world-plugin.js +16 -21
  12. package/agent-agy/package.json +6 -6
  13. package/agent-claude/cordis.patch.yml +8 -4
  14. package/agent-claude/dist/agent-preset-claude.js +29 -166
  15. package/agent-claude/dist/agent.js +10 -10
  16. package/agent-claude/dist/claude-client.js +22 -3
  17. package/agent-claude/dist/claude-home.js +0 -8
  18. package/agent-claude/dist/index.js +12 -15
  19. package/agent-claude/dist/session-map.js +2 -5
  20. package/agent-claude/dist/world-plugin.js +3 -3
  21. package/agent-claude/package.json +9 -9
  22. package/agent-codex/README.md +8 -8
  23. package/agent-codex/cordis.patch.yml +9 -5
  24. package/agent-codex/dist/agent-preset-codex.js +29 -168
  25. package/agent-codex/dist/agent.js +17 -12
  26. package/agent-codex/dist/codex-store.js +0 -9
  27. package/agent-codex/dist/index.js +13 -16
  28. package/agent-codex/dist/session-map.js +2 -5
  29. package/agent-codex/dist/world-plugin.js +15 -20
  30. package/agent-codex/package.json +14 -11
  31. package/agent-hermes/README.md +9 -8
  32. package/agent-hermes/cordis.patch.yml +8 -4
  33. package/agent-hermes/dist/agent-preset-hermes.js +29 -168
  34. package/agent-hermes/dist/agent.js +33 -19
  35. package/agent-hermes/dist/hermes-client.js +5 -5
  36. package/agent-hermes/dist/hermes-store.js +2 -11
  37. package/agent-hermes/dist/index.js +13 -12
  38. package/agent-hermes/dist/world-plugin.js +16 -21
  39. package/agent-hermes/package.json +14 -11
  40. package/agent-omp/README.md +2 -2
  41. package/agent-omp/cordis.patch.yml +12 -7
  42. package/agent-omp/dist/agent-preset-omp.js +30 -169
  43. package/agent-omp/dist/agent.js +13 -5
  44. package/agent-omp/dist/index.js +12 -22
  45. package/agent-omp/dist/mobile/gesture.js +2 -2
  46. package/agent-omp/dist/mobile/zoom-guard.js +4 -4
  47. package/agent-omp/dist/mobile-boot.js +2 -2
  48. package/agent-omp/dist/omp-cli.js +1 -1
  49. package/agent-omp/dist/omp-disk-discovery.js +1 -1
  50. package/agent-omp/dist/protocol.js +1 -1
  51. package/agent-omp/dist/sidecar-client.js +1 -1
  52. package/agent-omp/dist/world-plugin.js +22 -19
  53. package/agent-omp/package.json +18 -14
  54. package/agent-omp/sidecar/main.ts +2 -2
  55. package/agent-pi/README.md +5 -5
  56. package/agent-pi/cordis.patch.yml +8 -4
  57. package/agent-pi/dist/agent-preset-pi.js +29 -166
  58. package/agent-pi/dist/agent.js +18 -12
  59. package/agent-pi/dist/index.js +14 -16
  60. package/agent-pi/dist/pi-home.js +2 -12
  61. package/agent-pi/dist/world-plugin.js +16 -21
  62. package/agent-pi/package.json +14 -11
  63. package/cordis.patch.yml +31 -21
  64. package/package.json +30 -38
  65. package/{agent-hub → superd-hub}/cordis.patch.yml +3 -3
  66. package/{agent-hub → superd-hub}/dist/carrier.d.ts +9 -3
  67. package/{agent-hub → superd-hub}/dist/carrier.js +29 -3
  68. package/{agent-hub → superd-hub}/dist/gateway.d.ts +2 -2
  69. package/{agent-hub → superd-hub}/dist/gateway.js +1 -1
  70. package/{agent-hub → superd-hub}/dist/index.d.ts +5 -3
  71. package/{agent-hub → superd-hub}/dist/index.js +5 -3
  72. package/{agent-hub → superd-hub}/dist/roster.d.ts +2 -2
  73. package/{agent-hub → superd-hub}/dist/rpc.js +1 -1
  74. package/{agent-hub → superd-hub}/dist/spawn-world.d.ts +27 -6
  75. package/superd-hub/dist/spawn-world.js +277 -0
  76. package/superd-hub/dist/system-head.d.ts +69 -0
  77. package/superd-hub/dist/system-head.js +194 -0
  78. package/superd-hub/dist/web-trust.d.ts +112 -0
  79. package/superd-hub/dist/web-trust.js +126 -0
  80. package/{agent-hub → superd-hub}/dist/world-entry.d.ts +2 -2
  81. package/{agent-hub → superd-hub}/dist/world-entry.js +4 -4
  82. package/{agent-hub → superd-hub}/dist/world-host.d.ts +1 -1
  83. package/{agent-hub → superd-hub}/dist/world-join.d.ts +2 -1
  84. package/{agent-hub → superd-hub}/dist/world-join.js +16 -21
  85. package/{agent-hub → superd-hub}/dist/world-mount.d.ts +14 -6
  86. package/{agent-hub → superd-hub}/dist/world-mount.js +28 -7
  87. package/{agent-hub → superd-hub}/dist/world-mux.d.ts +21 -6
  88. package/superd-hub/dist/world-mux.js +392 -0
  89. package/{agent-hub → superd-hub}/dist/world-provision.d.ts +20 -4
  90. package/superd-hub/dist/world-provision.js +196 -0
  91. package/{agent-hub → superd-hub}/dist/world-web-server.js +7 -1
  92. package/{agent-hub → superd-hub}/package.json +9 -16
  93. package/agent-hub/dist/spawn-world.js +0 -72
  94. package/agent-hub/dist/world-mux.js +0 -224
  95. package/agent-hub/dist/world-provision.js +0 -122
  96. package/agent-hub/lib/client/index.js +0 -242
  97. package/client/index.js +0 -242
  98. /package/{agent-hub → superd-hub}/dist/agent-roster.d.ts +0 -0
  99. /package/{agent-hub → superd-hub}/dist/agent-roster.js +0 -0
  100. /package/{agent-hub → superd-hub}/dist/client-shim.d.ts +0 -0
  101. /package/{agent-hub → superd-hub}/dist/client-shim.js +0 -0
  102. /package/{agent-hub → superd-hub}/dist/envelope.d.ts +0 -0
  103. /package/{agent-hub → superd-hub}/dist/envelope.js +0 -0
  104. /package/{agent-hub → superd-hub}/dist/index-pass.d.ts +0 -0
  105. /package/{agent-hub → superd-hub}/dist/index-pass.js +0 -0
  106. /package/{agent-hub → superd-hub}/dist/labels.d.ts +0 -0
  107. /package/{agent-hub → superd-hub}/dist/labels.js +0 -0
  108. /package/{agent-hub → superd-hub}/dist/ownership.d.ts +0 -0
  109. /package/{agent-hub → superd-hub}/dist/ownership.js +0 -0
  110. /package/{agent-hub → superd-hub}/dist/roster.js +0 -0
  111. /package/{agent-hub → superd-hub}/dist/rpc.d.ts +0 -0
  112. /package/{agent-hub → superd-hub}/dist/targets.d.ts +0 -0
  113. /package/{agent-hub → superd-hub}/dist/targets.js +0 -0
  114. /package/{agent-hub → superd-hub}/dist/world-host.js +0 -0
  115. /package/{agent-hub → superd-hub}/dist/world-web-server.d.ts +0 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Nested-world profile provisioning — the publish story.
3
+ *
4
+ * The dev line (test/smoke.mjs) hand-provisions every world's nested profile
5
+ * (`<home>/agents/<label>/profiles/web`) and the `@pgmi-builds/*` links its
6
+ * resolution needs. A foreign install has NO line bootstrap:
7
+ *
8
+ * dsh plugin --profile <name> add super-dsh
9
+ *
10
+ * mounts the hub + adapter world plugins into the consumer's profile, and the
11
+ * spawn call sites (adapter world-plugins, hub join) must provision the same
12
+ * layout THEMSELVES or every world spawn fails on a missing profile.
13
+ *
14
+ * This module is the library form of that bootstrap, called by all six spawn
15
+ * sites. Layout invariants (super-dsh/AGENTS.md S7):
16
+ *
17
+ * - `<worldHome>/profiles/web/package.json` — bundles `[dsh-base, dsh-web-app,
18
+ * adapterPkg]`; the world tree is a full sibling root, nothing shared.
19
+ * - `<worldHome>/profiles/web/cordis.patch.yml` — per-world home pinning:
20
+ * every home-resolving DSH plugin reads THIS tree's nested root.
21
+ * - `<worldHome>/profiles/web/node_modules/@pgmi-builds/{adapter,hub}` —
22
+ * the plugin-add shape: with the host table widened (spawnWorld, spec
23
+ * §2.1), these profile-local links serve the local-physical leg for the
24
+ * world's own packages. No `@deepseek-ai` scope anywhere — those names
25
+ * ride the host's installation entries.
26
+ *
27
+ * Link identity rule: both links point INSIDE the running line tree — the hub
28
+ * at this module's own package root, the adapter at its sibling
29
+ * `agent-<key>` directory — identical in the dev repo layout and inside the
30
+ * published single `super-dsh` package, so ctx0 and every world root share
31
+ * one hub/adapter instance with zero registry involvement.
32
+ *
33
+ * Returns whether the layout was provisioned; false (logged) when the
34
+ * adapter sibling does not exist next to the hub — a foreign composition
35
+ * this module does not own.
36
+ */
37
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
38
+ import { dirname, join } from 'node:path';
39
+ import { fileURLToPath } from 'node:url';
40
+ import { createRequire } from 'node:module';
41
+ const HUB_SCOPE = '@pgmi-builds';
42
+ /** The profile bundles every world tree composes besides its adapter. */
43
+ const WORLD_BASE_BUNDLES = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'];
44
+ /**
45
+ * Resolve the running dsh installation's package.json — the anchor worlds
46
+ * resolve `@deepseek-ai` bundles and the fallback closure from.
47
+ *
48
+ * Precedence: `$SUPERD_DSH_ANCHOR` (the repo dev lines' contract — the
49
+ * launcher pins the checkout build), then a walk UP from the
50
+ * `@deepseek-ai/dsh-app-boot` this module resolved (the host always provides
51
+ * it) to the nearest ancestor package named `@deepseek-ai/dsh` — the
52
+ * packaged-install layout (`<install>/@deepseek-ai/dsh/node_modules/
53
+ * @deepseek-ai/dsh-app-boot`) lands on the install root in two hops. No
54
+ * machine-specific path ships in code.
55
+ * @throws when neither yields an installation (mis-launched composition).
56
+ */
57
+ export function resolveInstallAnchor(host) {
58
+ // Spec §2.3: the host's launcher facts win — a world plugin runs in ctx0,
59
+ // where `profileContext` carries the exact anchor the live table was
60
+ // computed from. Env and the realpath walk remain for table-less harnesses.
61
+ const launcher = host?.get('profileContext');
62
+ if (launcher?.installAnchor !== undefined)
63
+ return launcher.installAnchor;
64
+ const fromEnv = process.env.SUPERD_DSH_ANCHOR;
65
+ if (fromEnv !== undefined && fromEnv !== '')
66
+ return fromEnv;
67
+ let dir = dirname(resolveHostPackageDir('@deepseek-ai/dsh-app-boot'));
68
+ while (true) {
69
+ const manifest = join(dir, 'package.json');
70
+ if (existsSync(manifest)) {
71
+ try {
72
+ const name = JSON.parse(readFileSync(manifest, 'utf8')).name;
73
+ if (name === '@deepseek-ai/dsh')
74
+ return manifest;
75
+ }
76
+ catch { /* malformed intermediate manifest — keep walking */ }
77
+ }
78
+ const parent = dirname(dir);
79
+ if (parent === dir) {
80
+ throw new Error('super-dsh: cannot derive the dsh installation anchor (set SUPERD_DSH_ANCHOR)');
81
+ }
82
+ dir = parent;
83
+ }
84
+ }
85
+ /** First `node_modules` search-path candidate that exists for a specifier,
86
+ * resolved to its REALPATH — farm and pnpm links must not leak into the
87
+ * ancestor walk (the install tree only exists behind the symlink). */
88
+ function resolveHostPackageDir(spec) {
89
+ const require = createRequire(import.meta.url);
90
+ // Interception first: under the launcher's resolution table the profile
91
+ // tree holds NO physical @deepseek-ai, but require.resolve goes through
92
+ // the patched Module._resolveFilename and lands on the installation's
93
+ // copy directly. resolve.paths() cannot see the table — it only lists
94
+ // physical walk-up candidates (the retired profiles-level farm is what
95
+ // used to plug this gap on consumer machines; 0.1.3-r prod evidence).
96
+ try {
97
+ return realpathSync(dirname(require.resolve(`${spec}/package.json`)));
98
+ }
99
+ catch { /* not intercepted or ./package.json not exported — walk */ }
100
+ for (const searchPath of require.resolve.paths(`${spec}/x`) ?? []) {
101
+ const candidate = join(searchPath, spec);
102
+ if (existsSync(join(candidate, 'package.json')))
103
+ return realpathSync(candidate);
104
+ }
105
+ throw new Error(`super-dsh: cannot resolve ${spec} from the running host (set SUPERD_DSH_ANCHOR)`);
106
+ }
107
+ /**
108
+ * Provision one world's nested profile (idempotent: rewrites profile files,
109
+ * re-links packages — lstat-safe against dangling links). Returns whether
110
+ * the layout landed.
111
+ *
112
+ * Never throws: any provisioning failure is logged and returns false so the
113
+ * calling world plugin degrades to a not-ready roster row instead of failing
114
+ * the loader entry — a failed apply would take down the whole host boot.
115
+ */
116
+ export function provisionWorldProfile(opts) {
117
+ try {
118
+ return provisionWorldProfileUnchecked(opts);
119
+ }
120
+ catch (cause) {
121
+ console.error(`[super-dsh] provisioning world '${opts.key}' failed:`, cause);
122
+ return false;
123
+ }
124
+ }
125
+ function provisionWorldProfileUnchecked(opts) {
126
+ // Single-pack topology: this module lives at `<line>/superd-hub/dist/`, and
127
+ // every adapter is a SIBLING directory (`<line>/agent-<key>`) — the same
128
+ // layout in the repo (dev link: lines) and inside the published `super-dsh`
129
+ // package. The adapter link therefore points at the sibling; no registry
130
+ // resolution is involved anywhere.
131
+ const hubRoot = dirname(dirname(fileURLToPath(import.meta.url))); // <line>/superd-hub
132
+ const lineRoot = dirname(hubRoot); // monolith root / repo super-dsh/
133
+ const adapterDir = join(lineRoot, `agent-${opts.key}`);
134
+ if (!existsSync(join(adapterDir, 'package.json')))
135
+ return false;
136
+ const profileName = opts.profileName ?? 'web';
137
+ const profDir = join(opts.worldHome, 'profiles', profileName);
138
+ mkdirSync(profDir, { recursive: true });
139
+ writeFileSync(join(profDir, 'package.json'), JSON.stringify({
140
+ name: `superd-${opts.key}-world-profile`,
141
+ private: true,
142
+ dsh: { profile: { bundles: [...WORLD_BASE_BUNDLES, opts.adapterPkg] } },
143
+ }, null, 2) + '\n');
144
+ // Per-world home pinning (smoke.mjs parity): settings, credentials,
145
+ // attachments, shell env and skills all land inside `agents/<key>/`.
146
+ // (`agent-instructions` keeps its base `maxBytes` home — instruction
147
+ // discovery root, not agent data.)
148
+ //
149
+ // 0.1.7: the profile hmr row arms itself wherever a profileContext exists,
150
+ // and its config reload recomposes the tree from THE PROFILE LAYERS ALONE —
151
+ // spawn-time extraPatches do not survive it. The zero-listener posture is
152
+ // therefore PERSISTED in this layer (webserver disabled + ephemeral port,
153
+ // hmr itself off — a world's composition is a spawn-time fact and must not
154
+ // be re-derived), exactly mirroring worldMountPatches.
155
+ writeFileSync(join(profDir, 'cordis.patch.yml'), [
156
+ '# Per-world home pinning (dshHomePath is this tree\'s nested home).',
157
+ ...['settings', 'credentials', 'attachment-local', 'shell-env', 'skill-filesystem'].flatMap((id) => [`- id: ${id}`, ' config:', ' dshHome: !!js dshHomePath()']),
158
+ '# Zero-listener posture (0.1.7): persisted so hmr/launcher recomposition',
159
+ '# cannot re-enable the world\'s own listener (default port 3080).',
160
+ '- id: webserver',
161
+ ' disabled: true',
162
+ ' config:',
163
+ ' host: 127.0.0.1',
164
+ ' port: 0',
165
+ '- id: hmr',
166
+ ' disabled: true',
167
+ '',
168
+ ].join('\n'));
169
+ // @pgmi-builds links, plugin-add shape: adapter + hub into the world
170
+ // profile's OWN node_modules (resolveBundleDir consults the profile
171
+ // directory; the local-physical leg of the widened host table consumes
172
+ // the same links for row imports — spec §2.1). Dangling links first
173
+ // (lstat: existsSync is false for them, which skipped rmSync and crashed
174
+ // symlinkSync with EEXIST after repo moves).
175
+ const links = [
176
+ [opts.adapterPkg.slice(HUB_SCOPE.length + 1), adapterDir],
177
+ ['superd-hub', hubRoot],
178
+ ];
179
+ const linkRoot = join(profDir, 'node_modules');
180
+ mkdirSync(join(linkRoot, HUB_SCOPE), { recursive: true });
181
+ for (const [name, target] of links) {
182
+ const dst = join(linkRoot, HUB_SCOPE, name);
183
+ const st = lstatSync(dst, { throwIfNoEntry: false });
184
+ if (st)
185
+ rmSync(dst, { recursive: true, force: true });
186
+ symlinkSync(target, dst);
187
+ }
188
+ // Retired machinery (spec §3): scrub the profiles-level farm this function
189
+ // wrote before 2026-09-25. Stale links there sit inside the widened
190
+ // local-physical walk and would shadow the host table's installation
191
+ // entries with frozen-generation targets.
192
+ const staleFarm = join(opts.worldHome, 'profiles', 'node_modules');
193
+ rmSync(join(staleFarm, '@deepseek-ai'), { recursive: true, force: true });
194
+ rmSync(join(staleFarm, HUB_SCOPE), { recursive: true, force: true });
195
+ return true;
196
+ }
@@ -204,11 +204,17 @@ export class WorldWebServer {
204
204
  * happily re-root them under this world's label.
205
205
  */
206
206
  renderIndex(html) {
207
+ // Injection rows FIRST, the client shim LAST: index-inject rows may need
208
+ // to own the transport global before anything else materializes — the
209
+ // page-trust component's `window.__DSH_TRANSPORT__ = { ownsHost: true }`
210
+ // guards on absence, while the shim MERGES into an existing object
211
+ // (`globalThis.__DSH_TRANSPORT__ = globalThis.__DSH_TRANSPORT__ || {}`).
212
+ // Shim-first would create the object and silently eat the verdict.
207
213
  const rows = [
208
- { kind: 'script', placement: 'head', text: renderClientShim(this.labelPath) },
209
214
  // No hub-invented widget here: the world composes the hub's own client
210
215
  // plugin (see worldMountPatches), so its selector IS the native one.
211
216
  ...this.collectIndexInjections(),
217
+ { kind: 'script', placement: 'head', text: renderClientShim(this.labelPath) },
212
218
  ];
213
219
  const rendered = this.applyIndexTaps(renderIndexInjections(html, rows));
214
220
  // The roster rides the outbound pass (htmlRewrite), not this one: the mount
@@ -1,43 +1,36 @@
1
1
  {
2
- "name": "@pgmi-builds/agent-hub",
2
+ "name": "@pgmi-builds/superd-hub",
3
3
  "version": "0.1.0",
4
- "description": "Agent Worlds hub host half: world inventory control plane, roster, world spawner and the single-origin per-world mount carrier (no server-side selection; the addressed world is the request's mount path)",
4
+ "description": "Super D hub host half: world inventory control plane, roster, world spawner and the single-origin per-world mount carrier (no server-side selection; the addressed world is the request's mount path)",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./dist/index.js",
8
- "./client": "./lib/client/index.js",
9
8
  "./world": "./dist/world-entry.js",
10
- "./join": "./dist/world-join.js"
9
+ "./join": "./dist/world-join.js",
10
+ "./web-trust": "./dist/web-trust.js"
11
11
  },
12
12
  "files": [
13
13
  "dist",
14
- "lib",
15
14
  "cordis.patch.yml"
16
15
  ],
17
16
  "dsh": {
18
17
  "bundle": {
19
18
  "patch": "./cordis.patch.yml"
20
- },
21
- "client": {
22
- "inject": [
23
- "@deepseek-ai/dsh-client-ui-slots"
24
- ],
25
- "platform": "web"
26
19
  }
27
20
  },
28
21
  "scripts": {
29
22
  "build": "tsc -p .",
30
- "build-client": "node scripts/build-client.mjs",
31
23
  "test": "node --test --test-force-exit test/*.test.mjs"
32
24
  },
33
25
  "dependencies": {
26
+ "js-yaml": "^4.3.2",
34
27
  "ws": "^8.21.0"
35
28
  },
36
29
  "peerDependencies": {
37
30
  "@deepseek-ai/cordis": "*",
38
- "@deepseek-ai/dsh-api-gateway": "*",
39
- "@deepseek-ai/dsh-host-webserver": "*",
40
- "@deepseek-ai/dsh-typert-protocol": "*"
31
+ "@deepseek-ai/dsh-api-gateway": ">=0.1.5-0 <0.3.0-0",
32
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.5-0 <0.3.0-0",
33
+ "@deepseek-ai/dsh-typert-protocol": ">=0.1.5-0 <0.3.0-0"
41
34
  },
42
35
  "peerDependenciesMeta": {
43
36
  "@deepseek-ai/cordis": {
@@ -54,8 +47,8 @@
54
47
  }
55
48
  },
56
49
  "devDependencies": {
50
+ "@types/js-yaml": "^4.0.9",
57
51
  "@types/node": "^26.5.1",
58
- "tsdown": "^0.15.12",
59
52
  "typescript": "5.9.3"
60
53
  },
61
54
  "publishConfig": {
@@ -1,72 +0,0 @@
1
- /**
2
- * spawnWorld (S2): spawn a sibling ROOT context for a foreign agent world
3
- * from the CTX0 tree — the library form of the PoC (ADR 0008 mechanism,
4
- * .scratch/multi-context-poc/spawner2.mjs collapsed into a function).
5
- *
6
- * AW-A posture: SHARED home (ADR 0008 constraint 2 fallback). The ctxN
7
- * `dshHomePath` service override is an AW-C1 item (spec §三/§十一.2) — the
8
- * boot() provide/prepare ordering fact is pinned by
9
- * test/boot-prepare-ordering.test.mjs. Data detachment at this stage comes
10
- * from the adapter side (foreign app-home redirection, spec §七).
11
- *
12
- * Profile resolution: loadProfile(name, profileName, installAnchor, home)
13
- * reads profiles under `<home>/profiles/<profileName>` and heals the
14
- * `@deepseek-ai/*` fallback farm into `<home>/profiles/node_modules` from
15
- * the install anchor (single instance — same physical installation).
16
- */
17
- import { loadProfile, boot } from '@deepseek-ai/dsh-app-boot';
18
- import { provideCmdline } from '@deepseek-ai/dsh-cmdline';
19
- import { writeFileSync } from 'node:fs';
20
- import { join } from 'node:path';
21
- import { pathToFileURL } from 'node:url';
22
- const EMPTY_ROOT = '[]\n';
23
- export async function spawnWorld(opts) {
24
- const profile = loadProfile(opts.appName, opts.profileName, opts.installAnchor, opts.home);
25
- // Empty include root: the tree is composed entirely as patch layers
26
- // (bundle rows in profile order + profile patch + extras).
27
- writeFileSync(join(profile.dir, 'cordis.yml'), EMPTY_ROOT);
28
- const patches = [
29
- ...profile.layers.flatMap((l) => l.patches),
30
- ...profile.patches,
31
- ...(opts.extraPatches ?? []),
32
- ];
33
- // Bare plugin names resolve from the installation's node_modules (the
34
- // anchor owns the complete plugin closure). Same cordis on both sides —
35
- // no cross-copy Symbol split.
36
- // Loader wants a file:// URL base; accept a plain path and normalize.
37
- const rawBase = opts.bareModuleBaseUrl ?? join(opts.installAnchor, '..', 'node_modules', '/');
38
- const bareModuleBaseUrl = rawBase.startsWith('file:')
39
- ? rawBase
40
- : pathToFileURL(rawBase.endsWith('/') ? rawBase : rawBase + '/').href;
41
- // Embedding-host facts via the PUBLIC cmdline API (risk-2 probe: boot owns
42
- // dshHomePath, so the prepare slot is for OUR provides — cmdlineArgs is not
43
- // boot-provided and upstream documents the empty-args embedding path).
44
- // Worlds take no flags and never open a browser; exit = dispose own fiber.
45
- // LAN authorities for the world's trust fence (deploy-level, env-driven):
46
- // the world webserver must accept the Host the browser actually sends.
47
- const trusted = (process.env.AW_TRUSTED_HOSTS ?? '')
48
- .split(/[\s,]+/).filter(Boolean)
49
- .flatMap((h) => ['--trusted-host', h]);
50
- let fiber;
51
- const ctxW = await boot(opts.appName, join(profile.dir, 'cordis.yml'), patches, (ctx) => {
52
- provideCmdline(ctx, {
53
- args: ['--no-open', ...trusted],
54
- exit: () => { void fiber?.dispose(); },
55
- });
56
- // This tree reads its own home from here on: sessions, settings.yaml,
57
- // workspace store — all under the world's nested root.
58
- //
59
- // `set` (not `provide`): boot already registered `dshHomePath` on this
60
- // root, and cordis refuses a second `provide` in the same scope
61
- // ("service … has been registered at <root>"). `set` is the documented
62
- // "overwrite a provided service's value" path for the owning fiber, which
63
- // the prepare slot is — and ADR 0008 constraint 2 prescribes exactly this
64
- // in-tree override (process.env is a shared resolver and must not change).
65
- if (opts.dataHome !== undefined) {
66
- const dataHome = opts.dataHome;
67
- ctx.set('dshHomePath', (...segments) => join(dataHome, ...segments));
68
- }
69
- }, bareModuleBaseUrl);
70
- fiber = ctxW.fiber;
71
- return ctxW;
72
- }
@@ -1,224 +0,0 @@
1
- /**
2
- * Per-world Remote stream mux server (AW-B DL6).
3
- *
4
- * Upstream's `RemoteStreamMuxServer` owns `/api/remote.mux` for the native
5
- * gateway and is NOT reachable from outside its package (root exports omit it;
6
- * `./src/*` cannot be imported at runtime). So this is our own wire-compatible
7
- * implementation of the same protocol, bound to the ADDRESSED world's
8
- * `openWireStream`:
9
- *
10
- * browser → server { type:'open', streamId, endpoint, payload }
11
- * { type:'cancel', streamId }
12
- * server → browser { type:'item', streamId, value }
13
- * { type:'end', streamId }
14
- * { type:'error', streamId, error:{code,message,details} }
15
- *
16
- * Heartbeat: ping every interval, terminate after two missed pongs (same
17
- * policy as upstream).
18
- */
19
- /** Specifier is widened on purpose: `ws` is a runtime peer, not a typed dep. */
20
- const WS_MODULE = 'ws';
21
- const MAX_MISSED_HEARTBEATS = 2;
22
- const DEFAULT_HEARTBEAT_MS = 30_000;
23
- function isRecord(value) {
24
- return typeof value === 'object' && value !== null && !Array.isArray(value);
25
- }
26
- function exactKeys(value, keys) {
27
- const own = Object.keys(value);
28
- return own.length === keys.length && keys.every(key => Object.hasOwn(value, key));
29
- }
30
- function validId(value) {
31
- return typeof value === 'string' && value.length > 0;
32
- }
33
- /**
34
- * Parse one browser text frame.
35
- * @param text - complete WebSocket text message.
36
- * @returns the validated request.
37
- * @throws {Error} on a malformed frame.
38
- */
39
- export function parseClientMessage(text) {
40
- let raw;
41
- try {
42
- raw = JSON.parse(text);
43
- }
44
- catch {
45
- throw new Error('world-mux: invalid client message');
46
- }
47
- if (!isRecord(raw))
48
- throw new Error('world-mux: invalid client message');
49
- if (raw['type'] === 'cancel' && exactKeys(raw, ['type', 'streamId']) && validId(raw['streamId'])) {
50
- return { type: 'cancel', streamId: raw['streamId'] };
51
- }
52
- if (raw['type'] === 'open'
53
- && exactKeys(raw, ['type', 'streamId', 'endpoint', 'payload'])
54
- && validId(raw['streamId'])
55
- && typeof raw['endpoint'] === 'string'
56
- && raw['endpoint'].length > 0) {
57
- return { type: 'open', streamId: raw['streamId'], endpoint: raw['endpoint'], payload: raw['payload'] };
58
- }
59
- throw new Error('world-mux: invalid client message');
60
- }
61
- /** Default failure mapper: keep the error code when it looks like one. */
62
- export function defaultFailure(error) {
63
- if (isRecord(error)) {
64
- const code = typeof error['code'] === 'string' ? error['code'] : undefined;
65
- const message = typeof error['message'] === 'string' ? error['message'] : undefined;
66
- if (code !== undefined)
67
- return { code, message: message ?? code, details: {} };
68
- }
69
- return { code: 'gateway/stream-error', message: error instanceof Error ? error.message : String(error), details: {} };
70
- }
71
- /** One world's mux endpoint. */
72
- export class WorldMuxServer {
73
- open;
74
- options;
75
- server;
76
- ws;
77
- heartbeat;
78
- connections = new Set();
79
- missed = new WeakMap();
80
- closed = false;
81
- constructor(open, options = {}) {
82
- this.open = open;
83
- this.options = options;
84
- }
85
- /** Take over one accepted upgrade request. */
86
- async handleUpgrade(req, socket, head) {
87
- const { server, ws } = await this.load();
88
- server.handleUpgrade(req, socket, head, (client) => {
89
- this.missed.set(client, 0);
90
- client.on('pong', () => { this.missed.set(client, 0); });
91
- this.startHeartbeat();
92
- const done = this.runConnection(client, ws);
93
- this.connections.add(done);
94
- void done.then(() => { this.connections.delete(done); });
95
- });
96
- }
97
- /** Terminate every socket and wait for the active streams to unwind. */
98
- async close() {
99
- this.closed = true;
100
- clearInterval(this.heartbeat);
101
- this.heartbeat = undefined;
102
- const server = this.server;
103
- if (server !== undefined) {
104
- for (const client of server.clients)
105
- client.terminate();
106
- await new Promise((resolve) => { server.close(() => resolve()); });
107
- }
108
- await Promise.all([...this.connections]);
109
- }
110
- async load() {
111
- if (this.server !== undefined && this.ws !== undefined)
112
- return { server: this.server, ws: this.ws };
113
- const mod = (await import(WS_MODULE));
114
- const ws = typeof mod.WebSocketServer === 'function' ? mod : mod.default;
115
- const server = new ws.WebSocketServer({ noServer: true });
116
- this.server = server;
117
- this.ws = ws;
118
- return { server, ws };
119
- }
120
- async runConnection(client, ws) {
121
- const streams = new Map();
122
- let writes = Promise.resolve();
123
- const send = (message) => {
124
- let text;
125
- try {
126
- text = JSON.stringify(message);
127
- }
128
- catch (cause) {
129
- return Promise.reject(new Error('world-mux: item is not JSON serializable', { cause }));
130
- }
131
- const delivery = writes.then(() => new Promise((resolve, reject) => {
132
- if (client.readyState !== ws.WebSocket.OPEN) {
133
- reject(new Error('world-mux: socket is closed'));
134
- return;
135
- }
136
- client.send(text, (error) => { if (error)
137
- reject(error);
138
- else
139
- resolve(); });
140
- }));
141
- writes = delivery.catch(() => undefined);
142
- return delivery;
143
- };
144
- const pump = async (streamId, endpoint, payload, active) => {
145
- try {
146
- const source = await this.open(endpoint, payload, active.abort.signal);
147
- for await (const value of source) {
148
- await send({ type: 'item', streamId, value });
149
- }
150
- if (!active.abort.signal.aborted)
151
- await send({ type: 'end', streamId });
152
- }
153
- catch (error) {
154
- if (!active.abort.signal.aborted && client.readyState === ws.WebSocket.OPEN) {
155
- const failure = (this.options.failure ?? defaultFailure)(error);
156
- try {
157
- await send({ type: 'error', streamId, error: failure });
158
- }
159
- catch {
160
- client.close(1011, 'Remote stream failure could not be delivered');
161
- }
162
- }
163
- }
164
- };
165
- await new Promise((resolve) => {
166
- client.once('close', () => { resolve(); });
167
- client.on('error', () => { client.terminate(); });
168
- client.on('message', (data, isBinary) => {
169
- if (isBinary === true) {
170
- client.close(1003, 'text messages required');
171
- return;
172
- }
173
- let message;
174
- try {
175
- message = parseClientMessage(String(data));
176
- }
177
- catch {
178
- client.close(1008, 'invalid Remote stream request');
179
- return;
180
- }
181
- if (message.type === 'cancel') {
182
- streams.get(message.streamId)?.abort.abort(new Error('Remote stream cancelled'));
183
- return;
184
- }
185
- if (streams.has(message.streamId)) {
186
- client.close(1008, 'duplicate Remote stream id');
187
- return;
188
- }
189
- const abort = new AbortController();
190
- const active = { abort, done: Promise.resolve() };
191
- streams.set(message.streamId, active);
192
- const done = pump(message.streamId, message.endpoint, message.payload, active);
193
- active.done = done;
194
- void done.then(() => streams.delete(message.streamId), () => streams.delete(message.streamId));
195
- });
196
- });
197
- const active = [...streams.values()];
198
- for (const stream of active)
199
- stream.abort.abort(new Error('Remote stream socket closed'));
200
- await Promise.all(active.map(stream => stream.done));
201
- }
202
- startHeartbeat() {
203
- if (this.heartbeat !== undefined || this.closed)
204
- return;
205
- const interval = this.options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
206
- this.heartbeat = setInterval(() => {
207
- const server = this.server;
208
- if (server === undefined)
209
- return;
210
- for (const client of server.clients) {
211
- if (client.readyState !== (this.ws?.WebSocket.OPEN ?? 1))
212
- continue;
213
- const missed = this.missed.get(client) ?? 0;
214
- if (missed >= MAX_MISSED_HEARTBEATS) {
215
- client.terminate();
216
- continue;
217
- }
218
- this.missed.set(client, missed + 1);
219
- client.ping();
220
- }
221
- }, interval);
222
- this.heartbeat.unref();
223
- }
224
- }