super-dsh 0.1.3 → 0.1.5

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 (142) hide show
  1. package/README.md +3 -3
  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 +31 -51
  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 +31 -51
  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 +31 -52
  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 +37 -50
  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 +31 -51
  62. package/agent-pi/package.json +14 -11
  63. package/client/index.js +1994 -150
  64. package/cordis.patch.yml +38 -15
  65. package/package.json +32 -30
  66. package/{agent-hub → superd-hub}/cordis.patch.yml +3 -3
  67. package/{agent-hub → superd-hub}/dist/agent-roster.d.ts +3 -1
  68. package/{agent-hub → superd-hub}/dist/agent-roster.js +4 -2
  69. package/superd-hub/dist/carrier.d.ts +62 -0
  70. package/superd-hub/dist/carrier.js +213 -0
  71. package/superd-hub/dist/fusion-transport.d.ts +148 -0
  72. package/superd-hub/dist/fusion-transport.js +397 -0
  73. package/{agent-hub → superd-hub}/dist/gateway.d.ts +9 -4
  74. package/superd-hub/dist/gateway.js +44 -0
  75. package/superd-hub/dist/hub-route.d.ts +74 -0
  76. package/superd-hub/dist/hub-route.js +188 -0
  77. package/{agent-hub → superd-hub}/dist/index.d.ts +14 -6
  78. package/{agent-hub → superd-hub}/dist/index.js +14 -6
  79. package/{agent-hub → superd-hub}/dist/roster.d.ts +2 -2
  80. package/{agent-hub → superd-hub}/dist/rpc.js +1 -1
  81. package/{agent-hub → superd-hub}/dist/spawn-world.d.ts +27 -6
  82. package/superd-hub/dist/spawn-world.js +279 -0
  83. package/superd-hub/dist/switch-core.d.ts +630 -0
  84. package/superd-hub/dist/switch-core.js +734 -0
  85. package/superd-hub/dist/system-head.d.ts +69 -0
  86. package/superd-hub/dist/system-head.js +194 -0
  87. package/superd-hub/dist/web-trust.d.ts +112 -0
  88. package/superd-hub/dist/web-trust.js +126 -0
  89. package/superd-hub/dist/world-entry.d.ts +38 -0
  90. package/superd-hub/dist/world-entry.js +47 -0
  91. package/{agent-hub → superd-hub}/dist/world-host.d.ts +15 -20
  92. package/superd-hub/dist/world-host.js +65 -0
  93. package/{agent-hub → superd-hub}/dist/world-join.d.ts +2 -1
  94. package/{agent-hub → superd-hub}/dist/world-join.js +30 -50
  95. package/superd-hub/dist/world-mount.d.ts +82 -0
  96. package/superd-hub/dist/world-mount.js +105 -0
  97. package/{agent-hub → superd-hub}/dist/world-mux.d.ts +21 -6
  98. package/superd-hub/dist/world-mux.js +392 -0
  99. package/{agent-hub → superd-hub}/dist/world-provision.d.ts +20 -4
  100. package/superd-hub/dist/world-provision.js +203 -0
  101. package/superd-hub/dist/world-web-server.d.ts +113 -0
  102. package/superd-hub/dist/world-web-server.js +99 -0
  103. package/superd-hub/lib/client/index.js +2086 -0
  104. package/{agent-hub → superd-hub}/package.json +12 -7
  105. package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
  106. package/agent-agy/dist/agent-preset-projection.js +0 -33
  107. package/agent-claude/dist/agent-preset-projection.js +0 -30
  108. package/agent-codex/dist/agent-preset-projection.js +0 -37
  109. package/agent-codex/dist/app-home.js +0 -65
  110. package/agent-codex/dist/spike.js +0 -46
  111. package/agent-hermes/dist/agent-preset-projection.js +0 -34
  112. package/agent-hub/dist/carrier.d.ts +0 -43
  113. package/agent-hub/dist/carrier.js +0 -241
  114. package/agent-hub/dist/client-shim.d.ts +0 -28
  115. package/agent-hub/dist/client-shim.js +0 -251
  116. package/agent-hub/dist/gateway.js +0 -18
  117. package/agent-hub/dist/index-pass.d.ts +0 -22
  118. package/agent-hub/dist/index-pass.js +0 -47
  119. package/agent-hub/dist/labels.d.ts +0 -45
  120. package/agent-hub/dist/labels.js +0 -83
  121. package/agent-hub/dist/spawn-world.js +0 -72
  122. package/agent-hub/dist/world-entry.d.ts +0 -31
  123. package/agent-hub/dist/world-entry.js +0 -51
  124. package/agent-hub/dist/world-host.js +0 -44
  125. package/agent-hub/dist/world-mount.d.ts +0 -52
  126. package/agent-hub/dist/world-mount.js +0 -52
  127. package/agent-hub/dist/world-mux.js +0 -224
  128. package/agent-hub/dist/world-provision.js +0 -122
  129. package/agent-hub/dist/world-web-server.d.ts +0 -162
  130. package/agent-hub/dist/world-web-server.js +0 -248
  131. package/agent-hub/lib/client/index.js +0 -242
  132. package/agent-omp/dist/agent-preset-projection.js +0 -36
  133. package/agent-omp/dist/app-home.js +0 -86
  134. package/agent-pi/dist/agent-preset-projection.js +0 -29
  135. /package/{agent-hub → superd-hub}/dist/envelope.d.ts +0 -0
  136. /package/{agent-hub → superd-hub}/dist/envelope.js +0 -0
  137. /package/{agent-hub → superd-hub}/dist/ownership.d.ts +0 -0
  138. /package/{agent-hub → superd-hub}/dist/ownership.js +0 -0
  139. /package/{agent-hub → superd-hub}/dist/roster.js +0 -0
  140. /package/{agent-hub → superd-hub}/dist/rpc.d.ts +0 -0
  141. /package/{agent-hub → superd-hub}/dist/targets.d.ts +0 -0
  142. /package/{agent-hub → superd-hub}/dist/targets.js +0 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * The hub-owned `/hub` route family (native UI fusion, plan Task 2).
3
+ *
4
+ * ONE prefix route + ONE upgrade route on ctx0's webServer (design ratified
5
+ * 2026-09-26): every FOREIGN-agent request funnels into this family and the
6
+ * demux key rides the query (`?agent=<label>`) instead of the path. The
7
+ * native `/api` surface is never touched — this family is the only seam. It
8
+ * registers unconditionally (authorize-gated, inert until a request carries a
9
+ * registered foreign label); the fusion feature flag never gates server route
10
+ * registration.
11
+ *
12
+ * Semantics per request:
13
+ * - `?agent=<label>` with a registered gateway → the request is consumed and
14
+ * forwarded to that world: envelope POST → `dispatchRpc`, non-envelope
15
+ * GET/HEAD → the world's published `/api` fetch handler, WS upgrade → a
16
+ * `WorldMuxServer` bound to that gateway. The demux key is CONSUMED, so
17
+ * the world sees exactly the URL its native page would have sent
18
+ * (`/hub/api/x?agent=o&p=1` → world `/api/x?p=1`).
19
+ * - no `agent` param or `agent=native` → 404 JSON: the family serves foreign
20
+ * traffic only; native belongs to `/api`.
21
+ * - unknown / not-yet-spawned label → 404 JSON `{error:'unknown-agent',
22
+ * label}` — silent degradation, never a locked page.
23
+ *
24
+ * The ctx0 gate is the SAME authorize source the world mounts use
25
+ * (`connection.authorizeIndex`, the boolean face whose false path already
26
+ * wrote the answer — 401 or the token→cookie 303); the family then runs the
27
+ * SAME browser-trust fence the sibling `/<label>/api` mounts run
28
+ * (`requestRejection`, the connection's `isTrustedApiRequest` face) before it
29
+ * answers anything — the family is never weaker than the neighbors carrying
30
+ * identical traffic. Forwarding reuses the carrier kernel
31
+ * ({@link forwardApiCall} / {@link worldMuxOf}) unchanged: this module only
32
+ * gates, fences and demuxes.
33
+ *
34
+ * Mux lifecycle (review round 1): each accepted upgrade constructs ONE
35
+ * `WorldMuxServer`, which roots a heartbeat interval and a per-connection
36
+ * WebSocketServer — resources that would otherwise outlive the client forever
37
+ * (one 2s no-op timer per reconnect). The mux is therefore CLOSED the moment
38
+ * its socket dies: close-on-disconnect. The carrier's shared-mux-per-mount
39
+ * shape does not transfer (the hub rebinds per respawn); close-on-disconnect
40
+ * does.
41
+ */
42
+ import { forwardApiCall, worldMuxOf, writeJson } from './carrier.js';
43
+ import { stripLabelFromUrl } from './world-web-server.js';
44
+ /** The mount prefix the family owns. */
45
+ export const HUB_MOUNT_PATH = '/hub';
46
+ /** The WebSocket upgrade path (upstream matches upgrades by pathname, so `?agent=` rides along). */
47
+ export const HUB_MUX_PATH = '/hub/api/remote.mux';
48
+ /** The demux key of one family request; absent or empty means "not foreign-addressed". */
49
+ function agentOf(req) {
50
+ const url = new URL(req.url ?? '/', 'http://dsh.internal');
51
+ const value = url.searchParams.get('agent');
52
+ return value === null || value === '' ? undefined : value;
53
+ }
54
+ /**
55
+ * The world-relative URL for one family request: the `/hub` mount prefix
56
+ * stripped (same translation the `/<label>` mounts apply) and the demux key
57
+ * removed with byte-preserving query surgery — every other parameter keeps
58
+ * its original order and encoding.
59
+ */
60
+ function worldUrlOf(url) {
61
+ const raw = stripLabelFromUrl(url, HUB_MOUNT_PATH);
62
+ const queryAt = raw.indexOf('?');
63
+ if (queryAt === -1)
64
+ return raw;
65
+ const kept = raw.slice(queryAt + 1).split('&').filter((pair) => {
66
+ if (pair === '')
67
+ return false;
68
+ const eq = pair.indexOf('=');
69
+ return (eq === -1 ? pair : pair.slice(0, eq)) !== 'agent';
70
+ });
71
+ return kept.length === 0 ? raw.slice(0, queryAt) : `${raw.slice(0, queryAt)}?${kept.join('&')}`;
72
+ }
73
+ /** Reason phrases for the raw-socket upgrade rejections this family writes. */
74
+ const REASON_PHRASE = { 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found' };
75
+ /** Reject an upgrade on the raw socket (no ws ownership transfer). */
76
+ function rejectUpgrade(socket, status, body, contentType) {
77
+ const writable = socket;
78
+ writable.write?.(`HTTP/1.1 ${status} ${REASON_PHRASE[status] ?? 'Rejected'}\r\n`
79
+ + 'Connection: close\r\n'
80
+ + `Content-Type: ${contentType}\r\n`
81
+ + `Content-Length: ${String(Buffer.byteLength(body, 'utf8'))}\r\n\r\n`
82
+ + body);
83
+ writable.destroy?.();
84
+ }
85
+ /**
86
+ * Run the boolean gate against an upgrade: the authorizer's false path writes
87
+ * to a `res` that does not exist on an upgrade, so the answer is captured and
88
+ * replayed on the raw socket verbatim. False means the upgrade was answered.
89
+ */
90
+ function gateUpgrade(options, req, socket) {
91
+ let status = 401;
92
+ let body;
93
+ const admitted = options.authorize(req, {
94
+ writeHead: (answered) => { status = answered; },
95
+ end: (chunk) => { if (chunk !== undefined && chunk !== null)
96
+ body = String(chunk); },
97
+ });
98
+ if (admitted)
99
+ return true;
100
+ rejectUpgrade(socket, status, body ?? '', 'text/plain; charset=utf-8');
101
+ return false;
102
+ }
103
+ /** One shared 404 answer shape for family-level demux misses. */
104
+ function answer404(res, body) {
105
+ writeJson(res, 404, JSON.stringify(body));
106
+ }
107
+ /**
108
+ * Register the family on ctx0's webServer.
109
+ * @param options - see {@link HubRouteOptions}.
110
+ * @returns the disposer unregistering both routes.
111
+ */
112
+ export function registerHubRoute(options) {
113
+ const handleRequest = async (req, res) => {
114
+ // ctx0's gate first (world-mount order): false ⇒ already answered.
115
+ if (!options.authorize(req, res))
116
+ return;
117
+ // Then the SAME browser-trust fence the sibling carrier family runs
118
+ // (the kernel's first statement there): before ANY family answer — the
119
+ // demux 404s below are for trusted traffic only.
120
+ const rejection = options.requestRejection?.(req);
121
+ if (rejection !== undefined) {
122
+ writeJson(res, rejection, JSON.stringify({ error: rejection === 401 ? 'unauthorized' : 'forbidden' }));
123
+ return;
124
+ }
125
+ const agent = agentOf(req);
126
+ if (agent === undefined || agent === 'native') {
127
+ answer404(res, {
128
+ error: 'foreign-only',
129
+ message: 'the /hub family serves ?agent=<label> foreign traffic only; native belongs to /api',
130
+ });
131
+ return;
132
+ }
133
+ const gateway = options.gatewayOf(agent);
134
+ if (gateway === undefined) {
135
+ answer404(res, { error: 'unknown-agent', label: agent });
136
+ return;
137
+ }
138
+ // Consume the family plumbing: the kernel forwards a native-shaped URL
139
+ // (the kernel carries the same fence face as the carrier's own kernel).
140
+ req.url = worldUrlOf(req.url);
141
+ await forwardApiCall({ gateway, label: agent, mountPath: HUB_MOUNT_PATH, requestRejection: options.requestRejection }, req, res);
142
+ };
143
+ const handleUpgrade = (req, socket, head) => {
144
+ if (!gateUpgrade(options, req, socket))
145
+ return;
146
+ // Same fence as the sibling carrier's mux: raw 401/403, no ws handover
147
+ // for untrusted Host/Origin.
148
+ const rejection = options.requestRejection?.(req);
149
+ if (rejection !== undefined) {
150
+ rejectUpgrade(socket, rejection, JSON.stringify({ error: rejection === 401 ? 'unauthorized' : 'forbidden' }), 'application/json');
151
+ return;
152
+ }
153
+ const agent = agentOf(req);
154
+ if (agent === undefined || agent === 'native') {
155
+ rejectUpgrade(socket, 404, JSON.stringify({ error: 'foreign-only' }), 'application/json');
156
+ return;
157
+ }
158
+ const gateway = options.gatewayOf(agent);
159
+ if (gateway === undefined) {
160
+ rejectUpgrade(socket, 404, JSON.stringify({ error: 'unknown-agent', label: agent }), 'application/json');
161
+ return;
162
+ }
163
+ // One mux per accepted connection: the binding resolves at upgrade time,
164
+ // so a respawned world is addressed by its CURRENT gateway. The mux roots
165
+ // a heartbeat interval and a per-connection WebSocketServer; on socket
166
+ // close it must be CLOSED (world-mux clears the heartbeat only in close),
167
+ // or every reconnect leaks the timer plus the mux graph forever.
168
+ const mux = worldMuxOf(gateway);
169
+ socket.once?.('close', () => {
170
+ void mux.close().catch(() => undefined);
171
+ });
172
+ void mux.handleUpgrade(req, socket, head);
173
+ };
174
+ const disposers = [
175
+ options.real.register({ kind: 'prefix', path: HUB_MOUNT_PATH, handler: handleRequest }),
176
+ options.real.registerUpgrade({ path: HUB_MUX_PATH, handler: handleUpgrade }),
177
+ ];
178
+ return () => {
179
+ for (const dispose of disposers.splice(0).reverse()) {
180
+ try {
181
+ dispose();
182
+ }
183
+ catch {
184
+ // teardown must not throw
185
+ }
186
+ }
187
+ };
188
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @pgmi-builds/agent-hub — host half public surface (Agent Worlds line).
2
+ * @pgmi-builds/superd-hub — host half public surface (Super D line).
3
3
  *
4
4
  * The line owns the switch and the gateway (per super-dsh/AGENTS.md):
5
5
  * the world inventory control plane + the foreign agent roster + the world
@@ -8,22 +8,30 @@
8
8
  * DL7 (2026-09-16): the app-level selector value is GONE. World ownership is
9
9
  * per-request: the addressed world is the mount path of the request
10
10
  * (`/<label>/...`). `/api/*` stays ctx0/native and untouched.
11
+ *
12
+ * Path separation T-D (2026-09-26): the world-page faces left the surface —
13
+ * the client mount shim, the mount HTML pass (`rewriteIndexHtml`) and the
14
+ * retired `mountWorld` no-op are gone (git history keeps them), and worlds
15
+ * compose as pure data planes (no webui rows).
11
16
  */
12
17
  export { handleAgentRuntime, registerSelectorRpc, AGENT_RUNTIME_RPC_PATH, registryTargets, type RuntimeTargetSource } from './rpc.js';
13
18
  export { registerForeignTarget, unregisterForeignTarget, getTarget, listRuntimeKeys, type ForeignTarget, type GatewayFace } from './targets.js';
14
19
  export { registerAgent, unregisterAgent, setReady, listAgents, onRosterChanged, type RosterEntry } from './roster.js';
15
20
  export { spawnWorld, type SpawnWorldOptions } from './spawn-world.js';
16
- export { provisionWorldProfile, type ProvisionWorldOptions } from './world-provision.js';
17
- export { claimLabel, labelOf, releaseLabel, listLabels, type LabelClaim, type ClaimOptions } from './labels.js';
21
+ export { provisionWorldProfile, resolveInstallAnchor, type ProvisionWorldOptions } from './world-provision.js';
18
22
  export { WorldWebServer, stripLabelFromUrl, type WorldWebRoute, type WorldWebUpgradeRoute, type RealWebServerFace } from './world-web-server.js';
19
23
  export { parseClientRequest, serverResponse, serverError, serverResult, wireErrorOf, EnvelopeError, type ClientRequestEnvelope, type WireError } from './envelope.js';
20
24
  export { WorldMuxServer, parseClientMessage, defaultFailure, type MuxOpen, type MuxFailure, type WorldMuxOptions } from './world-mux.js';
21
- export { mountWorld, rewriteIndexHtml, type CarrierGatewayFace, type MountWorldOptions, type MountedWorld } from './carrier.js';
22
- export { renderClientShim } from './client-shim.js';
25
+ export { forwardApiCall, forwardWorldFetch, worldMuxOf, writeJson, type CarrierGatewayFace, type ForwardOptions } from './carrier.js';
26
+ export { registerHubRoute, HUB_MOUNT_PATH, HUB_MUX_PATH, type HubRouteOptions } from './hub-route.js';
27
+ export { renderFusionTransport, deriveFusionFlag, FUSION_FLAG_ENV, FUSION_FLAG_GLOBAL, type FusionConfig } from './fusion-transport.js';
28
+ export { createSwitchController, parseLandingTarget, namespacedKey, storageNamespace, seatClickDecision, NATIVE_AGENT, SESSIONS_CURRENT_KEY, SWITCH_FALLBACK_MS, type SwitchHost, type SwitchController, type SwitchOptions, type SeatClickInput, type SeatClickDecision } from './switch-core.js';
23
29
  export { agentLinks, agentRosterRow, AGENT_ROSTER_GLOBAL, type AgentLink } from './agent-roster.js';
24
30
  export { createOwnershipIndex, type OwnershipIndex, type OwnershipRow } from './ownership.js';
25
31
  export { registerHostMount, takeHostMount, listHostMounts, noteWorldServer, worldServerOf, type HostMount } from './world-host.js';
26
- export { worldMountPatches, WORLD_MOUNT_PLUGIN, WORLD_MOUNT_ROW_ID, type WorldPatchLayer } from './world-mount.js';
32
+ export { worldMountPatches, superdTrustedHosts, WORLD_MOUNT_PLUGIN, WORLD_MOUNT_ROW_ID, WORLD_HUB_PLUGIN, WORLD_HUB_ROW_ID, type WorldPatchLayer, } from './world-mount.js';
33
+ export { buildTrustScript, deriveDefaultPageAuthorities, type BootScriptRow, type TrustConfig, } from './web-trust.js';
34
+ export { withSystemHead, type HeadAwareSessionFace, type HeadPlaceholder, type WithSystemHead } from './system-head.js';
27
35
  export { default as worldApply, name as worldPluginName, type Config as WorldEntryConfig } from './world-entry.js';
28
36
  export { default as joinApply, name as worldJoinPluginName, type Config as WorldJoinConfig } from './world-join.js';
29
37
  export { default } from './gateway.js';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @pgmi-builds/agent-hub — host half public surface (Agent Worlds line).
2
+ * @pgmi-builds/superd-hub — host half public surface (Super D line).
3
3
  *
4
4
  * The line owns the switch and the gateway (per super-dsh/AGENTS.md):
5
5
  * the world inventory control plane + the foreign agent roster + the world
@@ -8,22 +8,30 @@
8
8
  * DL7 (2026-09-16): the app-level selector value is GONE. World ownership is
9
9
  * per-request: the addressed world is the mount path of the request
10
10
  * (`/<label>/...`). `/api/*` stays ctx0/native and untouched.
11
+ *
12
+ * Path separation T-D (2026-09-26): the world-page faces left the surface —
13
+ * the client mount shim, the mount HTML pass (`rewriteIndexHtml`) and the
14
+ * retired `mountWorld` no-op are gone (git history keeps them), and worlds
15
+ * compose as pure data planes (no webui rows).
11
16
  */
12
17
  export { handleAgentRuntime, registerSelectorRpc, AGENT_RUNTIME_RPC_PATH, registryTargets } from './rpc.js';
13
18
  export { registerForeignTarget, unregisterForeignTarget, getTarget, listRuntimeKeys } from './targets.js';
14
19
  export { registerAgent, unregisterAgent, setReady, listAgents, onRosterChanged } from './roster.js';
15
20
  export { spawnWorld } from './spawn-world.js';
16
- export { provisionWorldProfile } from './world-provision.js';
17
- export { claimLabel, labelOf, releaseLabel, listLabels } from './labels.js';
21
+ export { provisionWorldProfile, resolveInstallAnchor } from './world-provision.js';
18
22
  export { WorldWebServer, stripLabelFromUrl } from './world-web-server.js';
19
23
  export { parseClientRequest, serverResponse, serverError, serverResult, wireErrorOf, EnvelopeError } from './envelope.js';
20
24
  export { WorldMuxServer, parseClientMessage, defaultFailure } from './world-mux.js';
21
- export { mountWorld, rewriteIndexHtml } from './carrier.js';
22
- export { renderClientShim } from './client-shim.js';
25
+ export { forwardApiCall, forwardWorldFetch, worldMuxOf, writeJson } from './carrier.js';
26
+ export { registerHubRoute, HUB_MOUNT_PATH, HUB_MUX_PATH } from './hub-route.js';
27
+ export { renderFusionTransport, deriveFusionFlag, FUSION_FLAG_ENV, FUSION_FLAG_GLOBAL } from './fusion-transport.js';
28
+ export { createSwitchController, parseLandingTarget, namespacedKey, storageNamespace, seatClickDecision, NATIVE_AGENT, SESSIONS_CURRENT_KEY, SWITCH_FALLBACK_MS } from './switch-core.js';
23
29
  export { agentLinks, agentRosterRow, AGENT_ROSTER_GLOBAL } from './agent-roster.js';
24
30
  export { createOwnershipIndex } from './ownership.js';
25
31
  export { registerHostMount, takeHostMount, listHostMounts, noteWorldServer, worldServerOf } from './world-host.js';
26
- export { worldMountPatches, WORLD_MOUNT_PLUGIN, WORLD_MOUNT_ROW_ID } from './world-mount.js';
32
+ export { worldMountPatches, superdTrustedHosts, WORLD_MOUNT_PLUGIN, WORLD_MOUNT_ROW_ID, WORLD_HUB_PLUGIN, WORLD_HUB_ROW_ID, } from './world-mount.js';
33
+ export { buildTrustScript, deriveDefaultPageAuthorities, } from './web-trust.js';
34
+ export { withSystemHead } from './system-head.js';
27
35
  export { default as worldApply, name as worldPluginName } from './world-entry.js';
28
36
  export { default as joinApply, name as worldJoinPluginName } from './world-join.js';
29
37
  export { default } from './gateway.js';
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * No config file: an agent is in the roster exactly while its adapter
5
5
  * plugin is loaded and registered. Adapter plugins self-register on
6
- * activation (key/label/ready) and the RuntimeSeat chip consumes the
7
- * snapshot. Roster entries are inventory only: they carry NO selection —
6
+ * activation (key/label/ready) and the Hub UI consumes the snapshot.
7
+ * Roster entries are inventory only: they carry NO selection —
8
8
  * the addressed world is the request's mount path (`DL7`, 2026-09-16).
9
9
  * one authority.
10
10
  */
@@ -47,6 +47,6 @@ export function registerSelectorRpc(ctx, source) {
47
47
  methods: ['GET', 'POST'],
48
48
  requestBody: 'buffered',
49
49
  fetch: (request) => handleAgentRuntime(request, source),
50
- }), 'agent-hub: /api/agent-runtime route');
50
+ }), 'superd-hub: /api/agent-runtime route');
51
51
  });
52
52
  }
@@ -9,12 +9,27 @@
9
9
  * test/boot-prepare-ordering.test.mjs. Data detachment at this stage comes
10
10
  * from the adapter side (foreign app-home redirection, spec §七).
11
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).
12
+ * Profile resolution (2026-09-25 upstream alignment —
13
+ * docs/specs/world-resolution-upstream): loadProfile(name, profileName,
14
+ * installAnchor, home) reads profiles under `<home>/profiles/<profileName>`
15
+ * and writes nothing to disk. Before boot imports a single row, spawnWorld
16
+ * publishes THIS world into the HOST's live resolution table (the successor
17
+ * contract's sanctioned surface: an added linkedRoot covering the world
18
+ * tree + added table entries for the world's own `@pgmi-builds` packages +
19
+ * a generated peerDependencies block in the world profile manifest for
20
+ * routeLinked's peer gate). The world config's bare row names then resolve
21
+ * through the same interception the launcher uses; the per-world bare-URL
22
+ * farms and bareModuleBaseUrl are gone.
16
23
  */
17
24
  import { boot } from '@deepseek-ai/dsh-app-boot';
25
+ /**
26
+ * The host (ctx0) surface spawnWorld needs: cordis `ctx.get`, which locates
27
+ * the launcher's `pluginPackages` table and `profileContext` facts. Structural
28
+ * on purpose — adapters pass their own minimal host context.
29
+ */
30
+ export interface WorldHostSurface {
31
+ get(name: string): unknown;
32
+ }
18
33
  export interface SpawnWorldOptions {
19
34
  /** App name for the spawned root (also its fiber label). */
20
35
  appName: string;
@@ -40,7 +55,13 @@ export interface SpawnWorldOptions {
40
55
  dataHome?: string;
41
56
  /** Extra patch layers appended after the profile's own (e.g. an adapter's insert row). */
42
57
  extraPatches?: Parameters<typeof boot>[2];
43
- /** Override the bare-name resolution base (defaults to the install anchor's node_modules). */
44
- bareModuleBaseUrl?: string;
58
+ /**
59
+ * The host (ctx0) context. When it carries the launcher's `pluginPackages`
60
+ * table, spawnWorld republishes a widened successor so this world's tree
61
+ * resolves through the host table exactly like a launcher profile (spec
62
+ * §2.1). Table-less harnesses (unit tests, embedders) omit it and keep
63
+ * native resolution.
64
+ */
65
+ host?: WorldHostSurface;
45
66
  }
46
67
  export declare function spawnWorld(opts: SpawnWorldOptions): Promise<import("@deepseek-ai/cordis").Context>;
@@ -0,0 +1,279 @@
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 (2026-09-25 upstream alignment —
13
+ * docs/specs/world-resolution-upstream): loadProfile(name, profileName,
14
+ * installAnchor, home) reads profiles under `<home>/profiles/<profileName>`
15
+ * and writes nothing to disk. Before boot imports a single row, spawnWorld
16
+ * publishes THIS world into the HOST's live resolution table (the successor
17
+ * contract's sanctioned surface: an added linkedRoot covering the world
18
+ * tree + added table entries for the world's own `@pgmi-builds` packages +
19
+ * a generated peerDependencies block in the world profile manifest for
20
+ * routeLinked's peer gate). The world config's bare row names then resolve
21
+ * through the same interception the launcher uses; the per-world bare-URL
22
+ * farms and bareModuleBaseUrl are gone.
23
+ */
24
+ import { loadProfile, boot, composeEntries, createRuntimeResolution } from '@deepseek-ai/dsh-app-boot';
25
+ import { provideCmdline } from '@deepseek-ai/dsh-cmdline';
26
+ import { dump } from 'js-yaml';
27
+ import { entryListSchema } from '@deepseek-ai/cordis-plugin-include';
28
+ import { existsSync, writeFileSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
29
+ import { join } from 'node:path';
30
+ import { superdTrustedHosts } from './world-mount.js';
31
+ /**
32
+ * Republish the host's live resolution with THIS world added as a linked
33
+ * root (spec §2.1, contract-legal form): `profilesDir`/`profileDir` are
34
+ * frozen per process ("a successor cannot change its profile scope"), so
35
+ * the world tree joins the table the way the successor contract sanctions —
36
+ * an added linkedRoot covering `<worldHome>`, added entries for the world's
37
+ * own `@pgmi-builds` packages (routeLinked resolves names from the table),
38
+ * and a generated `peerDependencies` block in the world profile manifest
39
+ * (routeLinked's peer gate reads the importer's ancestor manifests; the
40
+ * peers are the composed rows' bare names — deterministic, regenerated on
41
+ * every spawn, so upstream row additions self-heal on the next boot).
42
+ *
43
+ * Faithful reconstruction — reload the launcher's profile from
44
+ * `profileContext` facts and recompute: the service exposes no `current`,
45
+ * and a plugin add between host boot and now is picked up for free (each
46
+ * spawn re-publishes; worlds are spawn-time facts and never recompose, so a
47
+ * later host-side replace can only affect the next spawn).
48
+ */
49
+ async function publishWorldInHostResolutionTurn(packages, launcher, opts, worldProfileDir, peerNames) {
50
+ const profile = loadProfile('dsh', launcher.name, launcher.installAnchor, launcher.home);
51
+ const base = await createRuntimeResolution({ installAnchor: launcher.installAnchor, profile, home: launcher.home });
52
+ // routeLinked's peer gate: the gate reads ancestor manifests of the
53
+ // IMPORTER — and ctx.baseUrl is a DIRECTORY URL with a trailing slash, so
54
+ // the resolver's dirname() lands on `<worldHome>/profiles`, one level
55
+ // above the profile itself. Peers therefore go into BOTH manifests (the
56
+ // world profile's own and the profiles-level directory), regenerated
57
+ // idempotently on every spawn — upstream row additions self-heal next boot.
58
+ const peers = Object.fromEntries([...peerNames].sort().map((name) => [name, '*']));
59
+ for (const manifestDir of [worldProfileDir, join(opts.home, 'profiles')]) {
60
+ const manifestPath = join(manifestDir, 'package.json');
61
+ const manifest = existsSync(manifestPath)
62
+ ? JSON.parse(readFileSync(manifestPath, 'utf8'))
63
+ : { name: `superd-${opts.appName}-profiles`, private: true };
64
+ manifest.peerDependencies = peers;
65
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
66
+ }
67
+ // The world's own packages become table entries (targets = the realpaths of
68
+ // the profile-local links provisionWorldProfile wrote). declarer MUST be a
69
+ // manifest PATH: the ESM interception rewrites the resolve parent to it
70
+ // (pathToFileURL(route.entry.declarer)).
71
+ const declarer = join(worldProfileDir, 'package.json');
72
+ const worldScope = join(worldProfileDir, 'node_modules', '@pgmi-builds');
73
+ const baseNames = new Set(base.entries.map((entry) => entry.name));
74
+ for (const name of readdirSync(worldScope, { withFileTypes: true })) {
75
+ if (!name.isDirectory() && !name.isSymbolicLink())
76
+ continue;
77
+ const pkgName = `@pgmi-builds/${name.name}`;
78
+ // A profile-declared package already covers the name (a consumer who
79
+ // properly installed our subpackages) — the base entry wins, never assert
80
+ // a divergent mapping over it.
81
+ if (baseNames.has(pkgName))
82
+ continue;
83
+ const packageDir = realpathSync(join(worldScope, name.name));
84
+ const pkgManifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
85
+ // FIRST-WRITE-WINS: shared names (every world publishes superd-hub) must
86
+ // keep the first publisher's declarer — overwriting it with another
87
+ // world's manifest path trips "replacing X requires a process restart"
88
+ // (dev3 0.1.3-u evidence). Any manifest path satisfies the ESM redirect;
89
+ // the peer gate reads the IMPORTER's ancestors, not the declarer.
90
+ if (!publishedEntries.has(pkgName)) {
91
+ publishedEntries.set(pkgName, { name: pkgName, packageDir, version: pkgManifest.version, declarer, scope: 'profile' });
92
+ }
93
+ }
94
+ const rootName = `superd-world-${opts.appName}`;
95
+ if (!publishedRoots.has(rootName)) {
96
+ publishedRoots.set(rootName, { name: rootName, realPath: opts.home });
97
+ }
98
+ packages.replace({
99
+ ...base,
100
+ entries: [...base.entries, ...publishedEntries.values()],
101
+ linkedRoots: [...base.linkedRoots, ...publishedRoots.values()],
102
+ });
103
+ }
104
+ /** Bare package name of a module specifier (`@scope/pkg/sub` → `@scope/pkg`). */
105
+ function barePackageNameOf(specifier) {
106
+ if (!specifier || specifier[0] === '.' || specifier[0] === '/' || specifier[0] === '#' || specifier.includes(':'))
107
+ return undefined;
108
+ const first = specifier.indexOf('/');
109
+ if (specifier[0] !== '@')
110
+ return first < 0 ? specifier : specifier.slice(0, first);
111
+ if (first < 0)
112
+ return;
113
+ const second = specifier.indexOf('/', first + 1);
114
+ return second < 0 ? specifier : specifier.slice(0, second);
115
+ }
116
+ /**
117
+ * Cross-spawn accumulation (spec §2.1, dev3 0.1.3-t evidence): the service
118
+ * exposes no `current`, and every reconstruction starts from a fresh base —
119
+ * a successor that DROPS a prior world's additions trips the contract
120
+ * ("replacing X requires a process restart") and every world after the
121
+ * first falls back to native resolution. Each spawn therefore merges its
122
+ * additions into this process-wide accumulator, and successors always carry
123
+ * the union. Values are deterministic per world (stable worldHome and pack
124
+ * paths), so re-spawns re-assert identical mappings — contract-stable.
125
+ */
126
+ const publishedEntries = new Map();
127
+ const publishedRoots = new Map();
128
+ /**
129
+ * Serializes table publishing: the six world spawns run concurrently, and
130
+ * interleaved reconstruct→replace cycles assert divergent successors against
131
+ * a mid-flight current (dev3 0.1.3-u evidence). One publish at a time; a
132
+ * failed turn never blocks the next (the caller's fail-soft handles it).
133
+ */
134
+ let publishQueue = Promise.resolve();
135
+ async function publishWorldInHostResolution(host, opts, worldProfileDir, peerNames) {
136
+ const packages = host.get('pluginPackages');
137
+ const launcher = host.get('profileContext');
138
+ if (packages === undefined || launcher?.installAnchor === undefined || launcher.home === undefined || launcher.name === undefined)
139
+ return;
140
+ const turn = publishQueue.catch(() => { }).then(() => publishWorldInHostResolutionTurn(packages, launcher, opts, worldProfileDir, peerNames));
141
+ publishQueue = turn;
142
+ await turn;
143
+ }
144
+ export async function spawnWorld(opts) {
145
+ const profile = loadProfile(opts.appName, opts.profileName, opts.installAnchor, opts.home);
146
+ // THE FILE IS THE COMPOSITION (0.1.7 alignment): compose every layer
147
+ // (bundle patches in profile order + profile patch + spawn extras like
148
+ // worldMountPatches) ONCE here and write the FINAL row list into the
149
+ // include file. Boot mounts it with NO patch stream. Rationale: the
150
+ // include's runtime patch application provably dropped the spawn-time
151
+ // worldMountPatches in nested boots (webserver re-enabled on 3080, world
152
+ // rows gone — 2026-09-24 alignment evidence), while the composition itself
153
+ // is deterministic and dumpable with the loader's own dialect (`!!js`
154
+ // preserved via entryListSchema).
155
+ const composed = composeEntries(profile.layers.map((l) => l.patches).concat([profile.patches, ...(opts.extraPatches ?? [])]), () => { });
156
+ writeFileSync(join(profile.dir, 'cordis.yml'), dump(composed, { schema: entryListSchema, noRefs: true, lineWidth: -1 }) + '\n');
157
+ const patches = [];
158
+ if (process.env.SUPERD_TRACE === '1') {
159
+ // Alignment diagnostics: what the world tree will actually compose.
160
+ try {
161
+ const rows = composed;
162
+ const pick = (id) => rows.find((r) => r.id === id);
163
+ for (const id of ['webserver', 'hmr', 'web-runtime', 'client-hmr', 'modules', 'superd-world-mount', 'superd-hub', 'agent-preset-registry']) {
164
+ const row = pick(id);
165
+ process.stderr.write(`[spawn-world:${opts.appName}] ${id}: ${row === undefined ? 'ABSENT' : JSON.stringify({ disabled: row.disabled, inject: 'inject' in row ? row.inject : undefined, name: row.name })}\n`);
166
+ }
167
+ process.stderr.write(`[spawn-world:${opts.appName}] composed rows: ${rows.length}\n`);
168
+ process.stderr.write(`[spawn-world:${opts.appName}] patches tail: ${JSON.stringify(patches.slice(-3).map((p) => p.id ?? (Array.isArray(p.insert) ? `insert(${p.insert.length})` : '?')))}\n`);
169
+ }
170
+ catch (cause) {
171
+ process.stderr.write(`[spawn-world:${opts.appName}] trace compose failed: ${String(cause)}\n`);
172
+ }
173
+ }
174
+ // Host table first (spec §2.1): publish this world as a linked root of the
175
+ // live resolution BEFORE boot() mounts the root include — the world config's
176
+ // bare row names then resolve through the same interception the launcher
177
+ // uses. Fail-soft: a table-less or hostile host degrades to native
178
+ // resolution (the world fails loudly on its first missing import, never the
179
+ // host).
180
+ if (opts.host !== undefined) {
181
+ try {
182
+ const peerNames = new Set();
183
+ for (const row of composed) {
184
+ for (const specifier of [
185
+ ...(typeof row.name === 'string' ? [row.name] : []),
186
+ ...(Array.isArray(row.inject) ? row.inject.filter((s) => typeof s === 'string') : []),
187
+ ]) {
188
+ const bare = barePackageNameOf(specifier);
189
+ if (bare !== undefined)
190
+ peerNames.add(bare);
191
+ }
192
+ }
193
+ await publishWorldInHostResolution(opts.host, opts, profile.dir, peerNames);
194
+ }
195
+ catch (cause) {
196
+ process.stderr.write(`[spawn-world:${opts.appName}] host table publishing failed (${String(cause)}) — falling back to native resolution\n`);
197
+ }
198
+ }
199
+ // Embedding-host facts via the PUBLIC cmdline API (risk-2 probe: boot owns
200
+ // dshHomePath, so the prepare slot is for OUR provides — cmdlineArgs is not
201
+ // boot-provided and upstream documents the empty-args embedding path).
202
+ // Worlds take no flags and never open a browser; exit = dispose own fiber.
203
+ // LAN authorities for the world's trust fence (deploy-level, env-driven):
204
+ // the world webserver must accept the Host the browser actually sends.
205
+ // (Same derivation worldMountPatches carries into the mount row's neutral
206
+ // webRuntime facts — one source, `superdTrustedHosts`.)
207
+ const trusted = superdTrustedHosts(process.env.SUPERD_TRUSTED_HOSTS)
208
+ .flatMap((h) => ['--trusted-host', h]);
209
+ let fiber;
210
+ // Launcher parity (0.1.7): a profileContext-carrying tree runs the profile
211
+ // HMR row, whose init REQUIRES the appReady service (otherwise "Profile HMR
212
+ // requires application readiness"). The launcher owns the readiness signal
213
+ // and commits it after boot settles — worlds do the same inline.
214
+ let appReadyCommitted = false;
215
+ const appReadyListeners = new Set();
216
+ const appReady = {
217
+ service: {
218
+ onReady(listener) {
219
+ if (appReadyCommitted) {
220
+ listener();
221
+ return () => { };
222
+ }
223
+ appReadyListeners.add(listener);
224
+ return () => { appReadyListeners.delete(listener); };
225
+ },
226
+ },
227
+ commit() {
228
+ if (appReadyCommitted)
229
+ return;
230
+ appReadyCommitted = true;
231
+ for (const listener of [...appReadyListeners])
232
+ listener();
233
+ appReadyListeners.clear();
234
+ },
235
+ };
236
+ const ctxW = await boot(opts.appName, join(profile.dir, 'cordis.yml'), patches, (ctx) => {
237
+ provideCmdline(ctx, {
238
+ args: ['--no-open', ...trusted],
239
+ exit: () => { void fiber?.dispose(); },
240
+ ready: appReady.service,
241
+ });
242
+ // This tree reads its own home from here on: sessions, settings.yaml,
243
+ // workspace store — all under the world's nested root.
244
+ //
245
+ // `set` (not `provide`): boot already registered `dshHomePath` on this
246
+ // root, and cordis refuses a second `provide` in the same scope
247
+ // ("service … has been registered at <root>"). `set` is the documented
248
+ // "overwrite a provided service's value" path for the owning fiber, which
249
+ // the prepare slot is — and ADR 0008 constraint 2 prescribes exactly this
250
+ // in-tree override (process.env is a shared resolver and must not change).
251
+ if (opts.dataHome !== undefined) {
252
+ const dataHome = opts.dataHome;
253
+ ctx.set('dshHomePath', (...segments) => join(dataHome, ...segments));
254
+ }
255
+ // 0.1.7: profile-mounted services (settings …) inject `profileContext`,
256
+ // which only runProfile's prepare slot used to provide. Worlds boot
257
+ // directly, so provide the equivalent here — without it every world's
258
+ // provider row pends on `waiting for service: settings`. This also arms
259
+ // the row-admission compatibility preflight inside world trees (it no-ops
260
+ // without a profileContext, which would leave world rows unjudged).
261
+ ctx.provide('profileContext', {
262
+ name: opts.appName,
263
+ dir: profile.dir,
264
+ patchPath: profile.patchPath,
265
+ installAnchor: opts.installAnchor,
266
+ cwd: process.cwd(),
267
+ home: opts.home,
268
+ startedBundles: profile.layers.map((l) => l.packageName),
269
+ overlays: profile.patches,
270
+ telemetryDisabledEnv: process.env.DSH_TELEMETRY_DISABLED,
271
+ });
272
+ });
273
+ fiber = ctxW.fiber;
274
+ // Launcher parity: commit readiness only when the world tree actually
275
+ // settled with a Loader (runProfile's same condition).
276
+ if (ctxW.get('loader') !== undefined)
277
+ appReady.commit();
278
+ return ctxW;
279
+ }