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,150 @@
1
+ /**
2
+ * Fusion transport (native UI fusion, plan Task 3) — the ctx0 page-level head
3
+ * script plus its injection component.
4
+ *
5
+ * The script installs, BEFORE any application bundle materializes:
6
+ * - the page-level state atom `__SUPERD_UI_STATE__`
7
+ * ({ get activeAgent(): 'native'|string, setAgent(label), subscribe(fn) }),
8
+ * initial `'native'`, never persisted (no localStorage);
9
+ * - five CALL-TIME patches (the atom is read at request time, never baked
10
+ * into a closure — after setAgent the same request expression retargets):
11
+ * 1. `globalThis.fetch` — while a FOREIGN agent is active, a same-origin
12
+ * request whose path starts with `/api` is re-rooted under `/hub`
13
+ * and gains exactly one `agent=<label>` param (a pre-existing agent
14
+ * key is replaced, never duplicated — the server demuxes repeated
15
+ * params to the FIRST value; other query bytes + hash preserved);
16
+ * 2. `WebSocket` / `EventSource` Proxy constructors + `XMLHttpRequest.open`
17
+ * — the same rule (the mux WS lands on `/hub/api/remote.mux`);
18
+ * 3. `__DSH_FILE_UPLOAD__ = { fetch }` — uploads ride patch 1 (the
19
+ * file-upload service reads the hook at construction, so the hook
20
+ * must exist before app boot);
21
+ * 4. `HTMLAnchorElement.prototype.click` — ONLY download anchors with a
22
+ * same-origin `/api` href rewrite (the narrow client-shim r2 rule);
23
+ * the rewrite is transient — saved around the native click and
24
+ * restored after, so an agent switch re-targets the same anchor;
25
+ * 5. `Storage.prototype` get/set/removeItem — namespace read at call
26
+ * time: native = no prefix, foreign = `'<label>:'`.
27
+ * - one control-plane exemption: `/api/agent-runtime` is NEVER rewritten —
28
+ * the roster always talks to ctx0, so the user can always switch back.
29
+ * Native mode is zero-interference: every patched face passes its exact
30
+ * arguments through untouched.
31
+ *
32
+ * The component's gated listener ALSO bakes `__SUPERD_UI_FUSION__ = true`
33
+ * into the ctx0 index (Task 4): the client half's fusion-branch
34
+ * discriminator, emitted by the same gate as the script so the halves never
35
+ * disagree (world pages never carry it and keep real hyperlinks — spec §5).
36
+ *
37
+ * Quoting discipline: the generated script contains NO double-quoted string —
38
+ * index-pass rewrites every DOUBLE-quoted root-absolute literal in served
39
+ * HTML, so all literals stay single-quoted (the client-shim precedent). The
40
+ * install is idempotent (per-leg guards) and best-effort (every leg is
41
+ * individually guarded; a failure disables that leg only).
42
+ *
43
+ * Injection (the web-trust emission path, ordered AFTER it — spec §3.4 hard
44
+ * constraint): the row is a `webserver/index-inject` head script pushed by the
45
+ * `superd-fusion-transport` component, composed through the composite row
46
+ * `super-dsh/fusion-transport` positioned after `super-dsh/web-trust`. Both
47
+ * components defer their listener registration behind `inject(['webServer'])`,
48
+ * so cordis's FIFO effect order guarantees trust pushes first and fusion
49
+ * pushes last — the non-merging `if(!window.__DSH_TRANSPORT__)` verdict in the
50
+ * trust script is always evaluated before anything could own that global.
51
+ * Row gate: the row config `enabled`, ON by default — the composite patch
52
+ * composing this row is the switch. `enabled: false` (or `disabled: true`)
53
+ * leaves the component dormant: no listener, no script, no flag global.
54
+ *
55
+ * @module @pgmi-builds/superd-hub/fusion-transport
56
+ */
57
+ import type { Context } from '@deepseek-ai/cordis';
58
+ import z from '@deepseek-ai/schemastery';
59
+ /**
60
+ * Render the page-level fusion transport as a classic script (head injection).
61
+ * Pure and constant: the active label is state, never a baked literal, so the
62
+ * script takes no arguments. All string literals are single-quoted (see the
63
+ * module doc block).
64
+ * @returns JavaScript source.
65
+ */
66
+ export declare function renderFusionTransport(): string;
67
+ /**
68
+ * The page global the ctx0 index carries when this row is on: the client
69
+ * half's fusion-branch discriminator (the Hub UI switch surfaces read its
70
+ * presence to intercept agent clicks into the switch orchestration instead
71
+ * of letting the hyperlinks navigate — the sole switch surface since the T-B
72
+ * RuntimeSeat retirement; world pages never carry it, so they keep the real
73
+ * navigation half — spec §5 双半边). Baked by the SAME listener as the
74
+ * transport script, so the two halves can never disagree.
75
+ */
76
+ export declare const FUSION_FLAG_GLOBAL = "__SUPERD_UI_FUSION__";
77
+ /**
78
+ * The row's SEMANTICS after the path-separation Part 2 closeout (T-C/T-D
79
+ * landed; this block is the T-E closeout record) — native-ui-only, there is
80
+ * no fallback route anywhere:
81
+ *
82
+ * - ON: the ctx0 page fuses. The transport script arms the in-place switch
83
+ * and `/hub` is the sole surface for every runtime. The per-label URL
84
+ * family (`/<label>/` ctx0 pages, T-C) and the world web UIs (T-D) are
85
+ * RETIRED from the product, not gated by this row — there is nothing
86
+ * navigational behind them to fall back to.
87
+ * - OFF: NO foreign entry exists AT ALL. The served product is the bare
88
+ * native UI plus the Hub panel as an information surface: the roster
89
+ * still lists the runtimes (the control plane is not gated), but the
90
+ * switch affordances render inert with the degradation hint
91
+ * (`switchAffordance` in switch-core) instead of dead controls — a row
92
+ * has no /<label>/ page to reach and the Hub UI ships no hrefs to reach
93
+ * one with.
94
+ *
95
+ * The switch is ROW COMPOSITION, exactly like every other row here: the
96
+ * composite patch composes this row, so the fused surface is what the product
97
+ * ships. A deployment that does not want it turns the row off where cordis
98
+ * already owns that decision — a profile layer restating the row with
99
+ * `config: { enabled: false }` (整行重述: same id, same name) or
100
+ * `disabled: true`, or dropping `super-dsh/fusion-transport` from the
101
+ * composition entirely. There is deliberately no env-derived default: a
102
+ * release gate living in an environment variable is deployment state that no
103
+ * patch layer can see, read, or diff.
104
+ */
105
+ export interface FusionConfig {
106
+ /**
107
+ * Whether this row injects the transport script and the flag global on
108
+ * ctx0's index. Default on — the row being composed is the switch.
109
+ */
110
+ enabled?: boolean;
111
+ }
112
+ /** Row config: ON unless a patch layer restates the row with `enabled: false`. */
113
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
114
+ enabled: z<boolean, boolean, "defined">;
115
+ }>>, Schemastery.ObjectT<NoInfer<{
116
+ enabled: z<boolean, boolean, "defined">;
117
+ }>>, "plain">;
118
+ /** Cordis plugin name. */
119
+ export declare const name = "superd-fusion-transport";
120
+ /**
121
+ * No static injects: `webServer` is composed conditionally inside
122
+ * {@link apply}, so the row loads (dormant) in compositions without one.
123
+ */
124
+ export declare const inject: string[];
125
+ /**
126
+ * Mount the fusion injection: one `webserver/index-inject` listener pushing
127
+ * the rendered head script row — the same emission path as the web-trust
128
+ * component, composed AFTER it (the composite row order is load order, and
129
+ * both components register their listeners behind `inject(['webServer'])`, so
130
+ * cordis's FIFO effect order pushes the fusion row last; spec §3.4) — plus
131
+ * the {@link FUSION_FLAG_GLOBAL} baked global row (Task 4): one gate, both
132
+ * halves.
133
+ *
134
+ * Off → the component registers nothing: no listener, no row, no script,
135
+ * no flag global.
136
+ * @param ctx - host plugin context.
137
+ * @param config - the row config (`enabled`, default on).
138
+ */
139
+ export declare function apply(ctx: Context, config: FusionConfig | undefined): void;
140
+ declare const _default: {
141
+ name: string;
142
+ inject: string[];
143
+ Config: z<Schemastery.ObjectS<NoInfer<{
144
+ enabled: z<boolean, boolean, "defined">;
145
+ }>>, Schemastery.ObjectT<NoInfer<{
146
+ enabled: z<boolean, boolean, "defined">;
147
+ }>>, "plain">;
148
+ apply: typeof apply;
149
+ };
150
+ export default _default;
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Fusion transport (native UI fusion, plan Task 3) — the ctx0 page-level head
3
+ * script plus its injection component.
4
+ *
5
+ * The script installs, BEFORE any application bundle materializes:
6
+ * - the page-level state atom `__SUPERD_UI_STATE__`
7
+ * ({ get activeAgent(): 'native'|string, setAgent(label), subscribe(fn) }),
8
+ * initial `'native'`, never persisted (no localStorage);
9
+ * - five CALL-TIME patches (the atom is read at request time, never baked
10
+ * into a closure — after setAgent the same request expression retargets):
11
+ * 1. `globalThis.fetch` — while a FOREIGN agent is active, a same-origin
12
+ * request whose path starts with `/api` is re-rooted under `/hub`
13
+ * and gains exactly one `agent=<label>` param (a pre-existing agent
14
+ * key is replaced, never duplicated — the server demuxes repeated
15
+ * params to the FIRST value; other query bytes + hash preserved);
16
+ * 2. `WebSocket` / `EventSource` Proxy constructors + `XMLHttpRequest.open`
17
+ * — the same rule (the mux WS lands on `/hub/api/remote.mux`);
18
+ * 3. `__DSH_FILE_UPLOAD__ = { fetch }` — uploads ride patch 1 (the
19
+ * file-upload service reads the hook at construction, so the hook
20
+ * must exist before app boot);
21
+ * 4. `HTMLAnchorElement.prototype.click` — ONLY download anchors with a
22
+ * same-origin `/api` href rewrite (the narrow client-shim r2 rule);
23
+ * the rewrite is transient — saved around the native click and
24
+ * restored after, so an agent switch re-targets the same anchor;
25
+ * 5. `Storage.prototype` get/set/removeItem — namespace read at call
26
+ * time: native = no prefix, foreign = `'<label>:'`.
27
+ * - one control-plane exemption: `/api/agent-runtime` is NEVER rewritten —
28
+ * the roster always talks to ctx0, so the user can always switch back.
29
+ * Native mode is zero-interference: every patched face passes its exact
30
+ * arguments through untouched.
31
+ *
32
+ * The component's gated listener ALSO bakes `__SUPERD_UI_FUSION__ = true`
33
+ * into the ctx0 index (Task 4): the client half's fusion-branch
34
+ * discriminator, emitted by the same gate as the script so the halves never
35
+ * disagree (world pages never carry it and keep real hyperlinks — spec §5).
36
+ *
37
+ * Quoting discipline: the generated script contains NO double-quoted string —
38
+ * index-pass rewrites every DOUBLE-quoted root-absolute literal in served
39
+ * HTML, so all literals stay single-quoted (the client-shim precedent). The
40
+ * install is idempotent (per-leg guards) and best-effort (every leg is
41
+ * individually guarded; a failure disables that leg only).
42
+ *
43
+ * Injection (the web-trust emission path, ordered AFTER it — spec §3.4 hard
44
+ * constraint): the row is a `webserver/index-inject` head script pushed by the
45
+ * `superd-fusion-transport` component, composed through the composite row
46
+ * `super-dsh/fusion-transport` positioned after `super-dsh/web-trust`. Both
47
+ * components defer their listener registration behind `inject(['webServer'])`,
48
+ * so cordis's FIFO effect order guarantees trust pushes first and fusion
49
+ * pushes last — the non-merging `if(!window.__DSH_TRANSPORT__)` verdict in the
50
+ * trust script is always evaluated before anything could own that global.
51
+ * Row gate: the row config `enabled`, ON by default — the composite patch
52
+ * composing this row is the switch. `enabled: false` (or `disabled: true`)
53
+ * leaves the component dormant: no listener, no script, no flag global.
54
+ *
55
+ * @module @pgmi-builds/superd-hub/fusion-transport
56
+ */
57
+ import z from '@deepseek-ai/schemastery';
58
+ /**
59
+ * Render the page-level fusion transport as a classic script (head injection).
60
+ * Pure and constant: the active label is state, never a baked literal, so the
61
+ * script takes no arguments. All string literals are single-quoted (see the
62
+ * module doc block).
63
+ * @returns JavaScript source.
64
+ */
65
+ export function renderFusionTransport() {
66
+ return `(() => {
67
+ try {
68
+ const g = globalThis
69
+
70
+ // ---- the __SUPERD_UI_STATE__ atom (created once; re-runs keep live state) ----
71
+ if (g.__SUPERD_UI_STATE__ === undefined || g.__SUPERD_UI_STATE__ === null) {
72
+ let current = 'native'
73
+ const subscribers = []
74
+ g.__SUPERD_UI_STATE__ = {
75
+ get activeAgent() { return current },
76
+ setAgent(label) {
77
+ if (typeof label !== 'string' || label === '') return
78
+ if (label === current) return
79
+ current = label
80
+ for (const fn of subscribers.slice()) {
81
+ try { fn(current) } catch { /* a broken subscriber never breaks the atom */ }
82
+ }
83
+ },
84
+ subscribe(fn) {
85
+ if (typeof fn !== 'function') return () => {}
86
+ subscribers.push(fn)
87
+ return () => {
88
+ const at = subscribers.indexOf(fn)
89
+ if (at !== -1) subscribers.splice(at, 1)
90
+ }
91
+ },
92
+ }
93
+ }
94
+ const state = g.__SUPERD_UI_STATE__
95
+
96
+ // ---- the one rewrite rule: '/api...' becomes '/hub/api...' + '?agent=<label>' ----
97
+ const HUB = '/hub'
98
+ const CONTROL = '/api/agent-runtime'
99
+ const origin = () => {
100
+ const loc = g.location
101
+ if (loc !== undefined && typeof loc.origin === 'string' && loc.origin !== 'null') return loc.origin
102
+ return 'http://dsh.internal'
103
+ }
104
+ // Same authority as the page (scheme-agnostic: ws/wss/http/https share host).
105
+ const sameAuthority = (url) => {
106
+ try {
107
+ return url.host === new URL(origin()).host
108
+ } catch {
109
+ return false
110
+ }
111
+ }
112
+ const isApiPath = (path) => path === '/api' || path.startsWith('/api/')
113
+ // Control-plane exemption: the roster control plane always hits ctx0.
114
+ const isExempt = (path) => path === CONTROL || path.startsWith(CONTROL + '/')
115
+ // Key compare matches the server demux shape: searchParams.get('agent')
116
+ // looks up PERCENT-DECODED keys, so '%61gent=x' demuxes as agent=x and the
117
+ // surgery must strip those spellings too (+' ' wins the '+' spelling).
118
+ const decodedKey = (k) => {
119
+ try { return decodeURIComponent(k.replace(/\\+/g, ' ')) } catch { return k }
120
+ }
121
+ const isAgentKey = (k) => k === 'agent' || decodedKey(k) === 'agent'
122
+ // Pure string surgery on (path, search, hash): the '/hub' prefix grows on
123
+ // the path, the hash survives byte-for-byte, and EXACTLY ONE agent param
124
+ // carries the active label. A pre-existing agent key is stripped before the
125
+ // fresh one lands — appending blindly would leave two agent params and the
126
+ // server demuxes to the FIRST value (searchParams.get), i.e. the wrong
127
+ // world with no trace. Non-agent query pairs keep their exact bytes and
128
+ // order; a bare '?' (no pairs) collapses instead of doubling the question
129
+ // mark, which would swallow the demux key.
130
+ const retarget = (path, search, hash) => {
131
+ if (isExempt(path) || !isApiPath(path)) return null
132
+ let kept = ''
133
+ if (search.length > 1) {
134
+ const survivors = []
135
+ for (const pair of (search.charAt(0) === '?' ? search.slice(1) : search).split('&')) {
136
+ if (pair === '') continue
137
+ const eq = pair.indexOf('=')
138
+ if (!isAgentKey(eq === -1 ? pair : pair.slice(0, eq))) survivors.push(pair)
139
+ }
140
+ kept = survivors.join('&')
141
+ }
142
+ return HUB + path + (kept === '' ? '?' : '?' + kept + '&') + 'agent=' + encodeURIComponent(String(state.activeAgent)) + hash
143
+ }
144
+ const rewriteUrlString = (s) => {
145
+ if (typeof s !== 'string' || s === '') return s
146
+ if (s.startsWith('/')) {
147
+ if (s.startsWith('//')) return s // protocol-relative: another authority
148
+ const hashAt = s.indexOf('#')
149
+ const hash = hashAt === -1 ? '' : s.slice(hashAt)
150
+ const rest = hashAt === -1 ? s : s.slice(0, hashAt)
151
+ const queryAt = rest.indexOf('?')
152
+ const search = queryAt === -1 ? '' : rest.slice(queryAt)
153
+ const path = queryAt === -1 ? rest : rest.slice(0, queryAt)
154
+ const next = retarget(path, search, hash)
155
+ return next === null ? s : next
156
+ }
157
+ try {
158
+ const url = new URL(s, origin())
159
+ if (!sameAuthority(url)) return s
160
+ const next = retarget(url.pathname, url.search, url.hash)
161
+ return next === null ? s : url.origin + next
162
+ } catch {
163
+ return s
164
+ }
165
+ }
166
+ const rewriteApiUrl = (input) => {
167
+ if (typeof input === 'string') return rewriteUrlString(input)
168
+ if (input !== null && typeof input === 'object' && typeof input.href === 'string') {
169
+ const next = rewriteUrlString(input.href)
170
+ return next === input.href ? input : next
171
+ }
172
+ return input
173
+ }
174
+ // Foreign means a non-native STRING label; anything else behaves native.
175
+ const foreign = () => typeof state.activeAgent === 'string' && state.activeAgent !== 'native'
176
+ // Never-throw wrapper: a failed rewrite falls back to the exact input.
177
+ const rewriteSafe = (input) => {
178
+ try {
179
+ return foreign() ? rewriteApiUrl(input) : input
180
+ } catch {
181
+ return input
182
+ }
183
+ }
184
+
185
+ // ---- patches 1 + 3: globalThis.fetch, and the file-upload hook riding it ----
186
+ if (g.__SUPERD_UI_FUSION_FETCH__ !== true) {
187
+ g.__SUPERD_UI_FUSION_FETCH__ = true
188
+ try {
189
+ const nativeFetch = g.fetch
190
+ if (typeof nativeFetch === 'function') {
191
+ const shimFetch = (input, init) => {
192
+ if (!foreign()) return nativeFetch(input, init)
193
+ let target = input
194
+ try {
195
+ if (typeof Request === 'function' && input instanceof Request) {
196
+ const url = rewriteApiUrl(input.url)
197
+ target = url === input.url ? input : new Request(url, input)
198
+ } else {
199
+ target = rewriteApiUrl(input)
200
+ }
201
+ } catch {
202
+ target = input
203
+ }
204
+ return nativeFetch(target, init)
205
+ }
206
+ g.fetch = shimFetch
207
+ // Read by the file-upload service at construction time: present means
208
+ // the page fetch carries uploads (no blob worker), and the hook IS the
209
+ // rewriting fetch, so uploads follow the active agent like patch 1.
210
+ g.__DSH_FILE_UPLOAD__ = { fetch: shimFetch }
211
+ }
212
+ } catch { /* best-effort: this leg dies alone */ }
213
+ }
214
+
215
+ // ---- patch 2: WebSocket / EventSource Proxy constructors + XMLHttpRequest.open ----
216
+ // Construct through a Proxy, never a hand-rolled wrapper: statics, prototype
217
+ // and instanceof must survive (the mux client compares readyState constants).
218
+ if (g.__SUPERD_UI_FUSION_SOCKETS__ !== true) {
219
+ g.__SUPERD_UI_FUSION_SOCKETS__ = true
220
+ try {
221
+ const NativeWebSocket = g.WebSocket
222
+ if (typeof NativeWebSocket === 'function') {
223
+ g.WebSocket = new Proxy(NativeWebSocket, {
224
+ construct: (Target, args) => {
225
+ const url = rewriteSafe(args[0])
226
+ if (url !== args[0]) return new Target(url, ...args.slice(1))
227
+ return Reflect.construct(Target, args)
228
+ },
229
+ })
230
+ }
231
+ } catch { /* best-effort */ }
232
+ try {
233
+ const NativeEventSource = g.EventSource
234
+ if (typeof NativeEventSource === 'function') {
235
+ g.EventSource = new Proxy(NativeEventSource, {
236
+ construct: (Target, args) => {
237
+ const url = rewriteSafe(args[0])
238
+ if (url !== args[0]) return new Target(url, ...args.slice(1))
239
+ return Reflect.construct(Target, args)
240
+ },
241
+ })
242
+ }
243
+ } catch { /* best-effort */ }
244
+ try {
245
+ const NativeXhr = g.XMLHttpRequest
246
+ if (typeof NativeXhr === 'function' && NativeXhr.prototype !== undefined && typeof NativeXhr.prototype.open === 'function') {
247
+ const nativeOpen = NativeXhr.prototype.open
248
+ NativeXhr.prototype.open = function (method, url) {
249
+ const target = rewriteSafe(url)
250
+ if (target === url) return nativeOpen.apply(this, arguments)
251
+ return nativeOpen.apply(this, [method, target].concat(Array.prototype.slice.call(arguments, 2)))
252
+ }
253
+ }
254
+ } catch { /* best-effort */ }
255
+ }
256
+
257
+ // ---- patch 4: download anchors (the narrow rule, client-shim r2 shape) ----
258
+ // ONLY anchors carrying a download attribute AND a same-origin /api href
259
+ // rewrite; every other anchor (navigation links included) passes through.
260
+ try {
261
+ if (g.HTMLAnchorElement !== undefined && g.HTMLAnchorElement.prototype !== undefined
262
+ && typeof g.HTMLAnchorElement.prototype.click === 'function'
263
+ && g.__SUPERD_UI_FUSION_ANCHOR__ !== true) {
264
+ g.__SUPERD_UI_FUSION_ANCHOR__ = true
265
+ const nativeAnchorClick = g.HTMLAnchorElement.prototype.click
266
+ g.HTMLAnchorElement.prototype.click = function (...args) {
267
+ let original = null
268
+ try {
269
+ if (foreign() && this.hasAttribute('download')) {
270
+ const current = this.getAttribute('href')
271
+ if (typeof current === 'string' && current !== '') {
272
+ const next = rewriteSafe(current)
273
+ if (typeof next === 'string' && next !== current) {
274
+ this.setAttribute('href', next)
275
+ original = current
276
+ }
277
+ }
278
+ }
279
+ } catch { /* best-effort: never break the click itself */ }
280
+ try {
281
+ return nativeAnchorClick.apply(this, args)
282
+ } finally {
283
+ // The rewrite is TRANSIENT: restored right after the native click,
284
+ // so the rewritten /hub/api href never outlives it. A persisted
285
+ // rewrite would survive an agent switch and the next click would
286
+ // browse the stale /hub/api form — no /api match, no re-rewrite,
287
+ // wrong-agent download.
288
+ if (original !== null) {
289
+ try { this.setAttribute('href', original) } catch { /* best-effort */ }
290
+ }
291
+ }
292
+ }
293
+ }
294
+ } catch { /* best-effort */ }
295
+
296
+ // ---- patch 5: storage namespace, derived at CALL time ----
297
+ // Native mode prefixes nothing (bare keys, byte-identical behavior); a
298
+ // foreign agent reads and writes only its own '<label>:' tree.
299
+ try {
300
+ const StorageProto = g.Storage && g.Storage.prototype
301
+ if (StorageProto !== undefined && g.localStorage !== undefined
302
+ && g.__SUPERD_UI_FUSION_STORAGE__ !== true
303
+ && typeof StorageProto.getItem === 'function'
304
+ && typeof StorageProto.setItem === 'function'
305
+ && typeof StorageProto.removeItem === 'function') {
306
+ g.__SUPERD_UI_FUSION_STORAGE__ = true
307
+ const nativeGetItem = StorageProto.getItem
308
+ const nativeSetItem = StorageProto.setItem
309
+ const nativeRemoveItem = StorageProto.removeItem
310
+ const ns = () => {
311
+ const active = state.activeAgent
312
+ return typeof active === 'string' && active !== 'native' ? active + ':' : ''
313
+ }
314
+ StorageProto.getItem = function (name) { return nativeGetItem.call(this, ns() + name) }
315
+ StorageProto.setItem = function (name, value) { return nativeSetItem.call(this, ns() + name, value) }
316
+ StorageProto.removeItem = function (name) { return nativeRemoveItem.call(this, ns() + name) }
317
+ }
318
+ } catch { /* storage namespace is best-effort: never break the page */ }
319
+ } catch { /* the whole install is best-effort: a partial page is still a page */ }
320
+ })()`;
321
+ }
322
+ /**
323
+ * The page global the ctx0 index carries when this row is on: the client
324
+ * half's fusion-branch discriminator (the Hub UI switch surfaces read its
325
+ * presence to intercept agent clicks into the switch orchestration instead
326
+ * of letting the hyperlinks navigate — the sole switch surface since the T-B
327
+ * RuntimeSeat retirement; world pages never carry it, so they keep the real
328
+ * navigation half — spec §5 双半边). Baked by the SAME listener as the
329
+ * transport script, so the two halves can never disagree.
330
+ */
331
+ export const FUSION_FLAG_GLOBAL = '__SUPERD_UI_FUSION__';
332
+ /** Row config: ON unless a patch layer restates the row with `enabled: false`. */
333
+ export const Config = z.object({
334
+ enabled: z.boolean().default(true),
335
+ });
336
+ /** Cordis plugin name. */
337
+ export const name = 'superd-fusion-transport';
338
+ /**
339
+ * No static injects: `webServer` is composed conditionally inside
340
+ * {@link apply}, so the row loads (dormant) in compositions without one.
341
+ */
342
+ export const inject = [];
343
+ /**
344
+ * Mount the fusion injection: one `webserver/index-inject` listener pushing
345
+ * the rendered head script row — the same emission path as the web-trust
346
+ * component, composed AFTER it (the composite row order is load order, and
347
+ * both components register their listeners behind `inject(['webServer'])`, so
348
+ * cordis's FIFO effect order pushes the fusion row last; spec §3.4) — plus
349
+ * the {@link FUSION_FLAG_GLOBAL} baked global row (Task 4): one gate, both
350
+ * halves.
351
+ *
352
+ * Off → the component registers nothing: no listener, no row, no script,
353
+ * no flag global.
354
+ * @param ctx - host plugin context.
355
+ * @param config - the row config (`enabled`, default on).
356
+ */
357
+ export function apply(ctx, config) {
358
+ // Composed row = on. Only an explicit `enabled: false` turns it off, so a
359
+ // composition that omits the config key cannot silently disable the surface.
360
+ if (config?.enabled === false)
361
+ return;
362
+ ctx.inject(['webServer'], () => {
363
+ ctx.on('webserver/index-inject', (table) => {
364
+ table.push({ kind: 'script', placement: 'head', text: renderFusionTransport() });
365
+ table.push({ kind: 'global', name: FUSION_FLAG_GLOBAL, value: true });
366
+ });
367
+ });
368
+ }
369
+ export default { name, inject, Config, apply };
@@ -19,10 +19,15 @@ export declare const name = "superd-hub";
19
19
  /**
20
20
  * The plugin form (row contract): applied when the row mounts.
21
21
  *
22
- * Two CTX0-owned contributions, neither carrying any selection value:
22
+ * Three CTX0-owned contributions, none carrying any selection value:
23
23
  * - the world inventory control plane (read-only);
24
24
  * - the agent roster row baked into ctx0's own index, so the selector lists
25
- * every mount path the hub currently serves.
25
+ * every mount path the hub currently serves;
26
+ * - the `/hub` route family (native UI fusion Task 2): the single foreign
27
+ * seam on ctx0's listener, registered once the listening stack is up.
28
+ * World trees compose this same row, but their `webServer` is the VIRTUAL
29
+ * per-world translation surface — the `/hub` family belongs to ctx0's
30
+ * real listener only (one family per process, demuxed by `?agent=`).
26
31
  */
27
32
  export declare function apply(ctx: Context): void;
28
33
  export default apply;
@@ -1,18 +1,44 @@
1
1
  import { registerSelectorRpc, registryTargets } from './rpc.js';
2
2
  import { agentRosterRow } from './agent-roster.js';
3
+ import { getTarget } from './targets.js';
4
+ import { registerHubRoute } from './hub-route.js';
5
+ import { WorldWebServer } from './world-web-server.js';
3
6
  /** Stable Cordis plugin name. */
4
7
  export const name = 'superd-hub';
5
8
  /**
6
9
  * The plugin form (row contract): applied when the row mounts.
7
10
  *
8
- * Two CTX0-owned contributions, neither carrying any selection value:
11
+ * Three CTX0-owned contributions, none carrying any selection value:
9
12
  * - the world inventory control plane (read-only);
10
13
  * - the agent roster row baked into ctx0's own index, so the selector lists
11
- * every mount path the hub currently serves.
14
+ * every mount path the hub currently serves;
15
+ * - the `/hub` route family (native UI fusion Task 2): the single foreign
16
+ * seam on ctx0's listener, registered once the listening stack is up.
17
+ * World trees compose this same row, but their `webServer` is the VIRTUAL
18
+ * per-world translation surface — the `/hub` family belongs to ctx0's
19
+ * real listener only (one family per process, demuxed by `?agent=`).
12
20
  */
13
21
  export function apply(ctx) {
14
22
  registerSelectorRpc(ctx, registryTargets);
15
23
  const on = ctx.on;
16
24
  on?.call(ctx, 'webserver/index-inject', (table) => { table.push(agentRosterRow()); });
25
+ ctx.inject(['webServer', 'connection'], (host) => {
26
+ const real = host.get('webServer');
27
+ if (real instanceof WorldWebServer)
28
+ return;
29
+ const connection = host.get('connection');
30
+ // The gate is ctx0's authorize source (connection.authorizeIndex); the
31
+ // addressed world resolves per request through the live foreign-target
32
+ // table — never a shared selection. The requestRejection face is the
33
+ // ctx0 browser-trust fence: auth first, then Host/Origin — the family is
34
+ // never weaker than the neighbors carrying identical traffic.
35
+ const dispose = registerHubRoute({
36
+ real: real,
37
+ authorize: (req, res) => connection.authorizeIndex(req, res),
38
+ requestRejection: (req) => connection.requestRejection(req),
39
+ gatewayOf: (label) => getTarget(label)?.gateway,
40
+ });
41
+ ctx.effect(() => dispose, 'superd-hub: /hub route family');
42
+ });
17
43
  }
18
44
  export default apply;
@@ -0,0 +1,74 @@
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 { type CarrierGatewayFace } from './carrier.js';
43
+ import { type RealWebServerFace } from './world-web-server.js';
44
+ /** The mount prefix the family owns. */
45
+ export declare const HUB_MOUNT_PATH = "/hub";
46
+ /** The WebSocket upgrade path (upstream matches upgrades by pathname, so `?agent=` rides along). */
47
+ export declare const HUB_MUX_PATH = "/hub/api/remote.mux";
48
+ /** Options for registering the family on one (real) webServer. */
49
+ export interface HubRouteOptions {
50
+ /** The ctx0 webServer the family registers on. */
51
+ real: RealWebServerFace;
52
+ /**
53
+ * ctx0's index authorization (the world-mount gate source): false means the
54
+ * authorizer already answered (401, or the token→cookie 303) and the family
55
+ * must not touch any gateway.
56
+ */
57
+ authorize(req: unknown, res: unknown): boolean;
58
+ /**
59
+ * ctx0's browser-trust fence — the SAME `isTrustedApiRequest` face the
60
+ * sibling carrier mounts run: returns an HTTP status to reject with, or
61
+ * undefined for trusted Host/Origin. Runs after {@link authorize} (auth
62
+ * first, the carrier's own ordering precedent) and before ANY family
63
+ * answer, so even the demux 404s never speak to untrusted traffic.
64
+ */
65
+ requestRejection?(req: unknown): number | undefined;
66
+ /** The live gateway of a registered label, or undefined (unknown / not yet spawned). */
67
+ gatewayOf(label: string): CarrierGatewayFace | undefined;
68
+ }
69
+ /**
70
+ * Register the family on ctx0's webServer.
71
+ * @param options - see {@link HubRouteOptions}.
72
+ * @returns the disposer unregistering both routes.
73
+ */
74
+ export declare function registerHubRoute(options: HubRouteOptions): () => void;