super-dsh 0.1.4 → 0.1.5-a

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 (39) hide show
  1. package/README.md +6 -7
  2. package/agent-agy/dist/world-plugin.js +18 -33
  3. package/agent-codex/dist/world-plugin.js +18 -33
  4. package/agent-hermes/dist/world-plugin.js +18 -34
  5. package/agent-omp/dist/world-plugin.js +16 -32
  6. package/agent-pi/dist/world-plugin.js +18 -33
  7. package/client/index.js +2086 -0
  8. package/cordis.patch.yml +21 -7
  9. package/package.json +13 -3
  10. package/superd-hub/dist/agent-roster.d.ts +3 -1
  11. package/superd-hub/dist/agent-roster.js +4 -2
  12. package/superd-hub/dist/carrier.d.ts +41 -28
  13. package/superd-hub/dist/carrier.js +148 -202
  14. package/superd-hub/dist/fusion-transport.d.ts +150 -0
  15. package/superd-hub/dist/fusion-transport.js +369 -0
  16. package/superd-hub/dist/gateway.d.ts +7 -2
  17. package/superd-hub/dist/gateway.js +28 -2
  18. package/superd-hub/dist/hub-route.d.ts +74 -0
  19. package/superd-hub/dist/hub-route.js +188 -0
  20. package/superd-hub/dist/index.d.ts +10 -4
  21. package/superd-hub/dist/index.js +10 -4
  22. package/superd-hub/dist/roster.d.ts +2 -2
  23. package/superd-hub/dist/spawn-world.js +5 -3
  24. package/superd-hub/dist/switch-core.d.ts +630 -0
  25. package/superd-hub/dist/switch-core.js +734 -0
  26. package/superd-hub/dist/web-trust.d.ts +5 -5
  27. package/superd-hub/dist/web-trust.js +5 -5
  28. package/superd-hub/dist/world-entry.d.ts +20 -13
  29. package/superd-hub/dist/world-entry.js +15 -19
  30. package/superd-hub/dist/world-host.d.ts +15 -20
  31. package/superd-hub/dist/world-host.js +25 -4
  32. package/superd-hub/dist/world-join.js +14 -29
  33. package/superd-hub/dist/world-mount.d.ts +48 -26
  34. package/superd-hub/dist/world-mount.js +59 -27
  35. package/superd-hub/dist/world-provision.js +11 -4
  36. package/superd-hub/dist/world-web-server.d.ts +57 -106
  37. package/superd-hub/dist/world-web-server.js +61 -216
  38. package/superd-hub/lib/client/index.js +2086 -0
  39. package/superd-hub/package.json +12 -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
+ }
@@ -8,22 +8,28 @@
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
21
  export { provisionWorldProfile, resolveInstallAnchor, type ProvisionWorldOptions } from './world-provision.js';
17
- export { claimLabel, labelOf, releaseLabel, listLabels, type LabelClaim, type ClaimOptions } from './labels.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, 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, WORLD_WEB_TRUST_PLUGIN, WORLD_WEB_TRUST_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';
27
33
  export { buildTrustScript, deriveDefaultPageAuthorities, type BootScriptRow, type TrustConfig, } from './web-trust.js';
28
34
  export { withSystemHead, type HeadAwareSessionFace, type HeadPlaceholder, type WithSystemHead } from './system-head.js';
29
35
  export { default as worldApply, name as worldPluginName, type Config as WorldEntryConfig } from './world-entry.js';
@@ -8,22 +8,28 @@
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
21
  export { provisionWorldProfile, resolveInstallAnchor } from './world-provision.js';
17
- export { claimLabel, labelOf, releaseLabel, listLabels } from './labels.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, 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, WORLD_WEB_TRUST_PLUGIN, WORLD_WEB_TRUST_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';
27
33
  export { buildTrustScript, deriveDefaultPageAuthorities, } from './web-trust.js';
28
34
  export { withSystemHead } from './system-head.js';
29
35
  export { default as worldApply, name as worldPluginName } from './world-entry.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). Roster entries are inventory only: they
7
- * 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
  */
@@ -27,6 +27,7 @@ import { dump } from 'js-yaml';
27
27
  import { entryListSchema } from '@deepseek-ai/cordis-plugin-include';
28
28
  import { existsSync, writeFileSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
29
29
  import { join } from 'node:path';
30
+ import { superdTrustedHosts } from './world-mount.js';
30
31
  /**
31
32
  * Republish the host's live resolution with THIS world added as a linked
32
33
  * root (spec §2.1, contract-legal form): `profilesDir`/`profileDir` are
@@ -159,7 +160,7 @@ export async function spawnWorld(opts) {
159
160
  try {
160
161
  const rows = composed;
161
162
  const pick = (id) => rows.find((r) => r.id === id);
162
- for (const id of ['webserver', 'hmr', 'modules', 'superd-world-mount', 'superd-hub', 'agent-preset-registry']) {
163
+ for (const id of ['webserver', 'hmr', 'web-runtime', 'client-hmr', 'modules', 'superd-world-mount', 'superd-hub', 'agent-preset-registry']) {
163
164
  const row = pick(id);
164
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`);
165
166
  }
@@ -201,8 +202,9 @@ export async function spawnWorld(opts) {
201
202
  // Worlds take no flags and never open a browser; exit = dispose own fiber.
202
203
  // LAN authorities for the world's trust fence (deploy-level, env-driven):
203
204
  // the world webserver must accept the Host the browser actually sends.
204
- const trusted = (process.env.SUPERD_TRUSTED_HOSTS ?? '')
205
- .split(/[\s,]+/).filter(Boolean)
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)
206
208
  .flatMap((h) => ['--trusted-host', h]);
207
209
  let fiber;
208
210
  // Launcher parity (0.1.7): a profileContext-carrying tree runs the profile