super-dsh 0.1.4 → 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 (55) 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 +20 -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 +148 -0
  15. package/superd-hub/dist/fusion-transport.js +397 -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
  40. package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
  41. package/agent-agy/dist/agent-preset-projection.js +0 -33
  42. package/agent-claude/dist/agent-preset-projection.js +0 -30
  43. package/agent-codex/dist/agent-preset-projection.js +0 -37
  44. package/agent-codex/dist/app-home.js +0 -65
  45. package/agent-codex/dist/spike.js +0 -46
  46. package/agent-hermes/dist/agent-preset-projection.js +0 -34
  47. package/agent-omp/dist/agent-preset-projection.js +0 -36
  48. package/agent-omp/dist/app-home.js +0 -86
  49. package/agent-pi/dist/agent-preset-projection.js +0 -29
  50. package/superd-hub/dist/client-shim.d.ts +0 -28
  51. package/superd-hub/dist/client-shim.js +0 -251
  52. package/superd-hub/dist/index-pass.d.ts +0 -22
  53. package/superd-hub/dist/index-pass.js +0 -47
  54. package/superd-hub/dist/labels.d.ts +0 -45
  55. package/superd-hub/dist/labels.js +0 -83
@@ -0,0 +1,148 @@
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
+ * Flag gate (minimal, Task 5 formalizes the config face): row config
52
+ * `enabled`, defaulting OFF from the `SUPERD_UI_FUSION` env — while the flag
53
+ * is off the component stays dormant and injects no row at all.
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
+ /** The env name the minimal Task-3 gate reads; Task 5 builds the config face on it. */
68
+ export declare const FUSION_FLAG_ENV = "SUPERD_UI_FUSION";
69
+ /**
70
+ * The page global the ctx0 index carries when the gate is on: the client
71
+ * half's fusion-branch discriminator (the Hub UI switch surfaces read its
72
+ * presence to intercept agent clicks into the switch orchestration instead
73
+ * of letting the hyperlinks navigate — the sole switch surface since the T-B
74
+ * RuntimeSeat retirement; world pages never carry it, so they keep the real
75
+ * navigation half — spec §5 双半边). Baked by the SAME gated listener as the
76
+ * transport script, so the two halves can never disagree.
77
+ */
78
+ export declare const FUSION_FLAG_GLOBAL = "__SUPERD_UI_FUSION__";
79
+ /**
80
+ * The flag's SEMANTICS after the path-separation Part 2 closeout (T-C/T-D
81
+ * landed; this block is the T-E closeout record) — native-ui-only, there is
82
+ * no fallback route anywhere:
83
+ *
84
+ * - ON: the ctx0 page fuses. The transport script arms the in-place switch
85
+ * and `/hub` is the sole surface for every runtime. The per-label URL
86
+ * family (`/<label>/` ctx0 pages, T-C) and the world web UIs (T-D) are
87
+ * RETIRED from the product, not gated by this flag — there is nothing
88
+ * navigational behind them to fall back to.
89
+ * - OFF: NO foreign entry exists AT ALL. The served product is the bare
90
+ * native UI plus the Hub panel as an information surface: the roster
91
+ * still lists the runtimes (the control plane is not gated), but the
92
+ * switch affordances render inert with the degradation hint
93
+ * (`switchAffordance` in switch-core) instead of dead controls — a row
94
+ * has no /<label>/ page to reach and the Hub UI ships no hrefs to reach
95
+ * one with.
96
+ *
97
+ * The shipped default stays OFF (truthy env opt-in). Moving the default ON is
98
+ * the user's 放行 (acceptance/release) round — a two-gate decision per repo
99
+ * §〇.1, never a code-side choice.
100
+ *
101
+ * Derivation: truthy spellings only (`1` / `true` / `yes` / `on`,
102
+ * case-insensitive, trimmed). Anything else — unset included — is OFF.
103
+ */
104
+ export declare function deriveFusionFlag(env: string | undefined): boolean;
105
+ /** Row config schema face. */
106
+ export interface FusionConfig {
107
+ /** Master gate: inject the transport script on ctx0's index. Default off. */
108
+ enabled?: boolean;
109
+ }
110
+ /** Row config (schema-level default derives from the {@link FUSION_FLAG_ENV} env). */
111
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
112
+ enabled: z<boolean, boolean, "defined">;
113
+ }>>, Schemastery.ObjectT<NoInfer<{
114
+ enabled: z<boolean, boolean, "defined">;
115
+ }>>, "plain">;
116
+ /** Cordis plugin name. */
117
+ export declare const name = "superd-fusion-transport";
118
+ /**
119
+ * No static injects: `webServer` is composed conditionally inside
120
+ * {@link apply}, so the row loads (dormant) in compositions without one.
121
+ */
122
+ export declare const inject: string[];
123
+ /**
124
+ * Mount the fusion injection: one `webserver/index-inject` listener pushing
125
+ * the rendered head script row — the same emission path as the web-trust
126
+ * component, composed AFTER it (the composite row order is load order, and
127
+ * both components register their listeners behind `inject(['webServer'])`, so
128
+ * cordis's FIFO effect order pushes the fusion row last; spec §3.4) — plus
129
+ * the {@link FUSION_FLAG_GLOBAL} baked global row (Task 4): one gate, both
130
+ * halves.
131
+ *
132
+ * Flag off → the component registers nothing: no listener, no row, no script,
133
+ * no flag global.
134
+ * @param ctx - host plugin context.
135
+ * @param config - the row config.
136
+ */
137
+ export declare function apply(ctx: Context, config: FusionConfig | undefined): void;
138
+ declare const _default: {
139
+ name: string;
140
+ inject: string[];
141
+ Config: z<Schemastery.ObjectS<NoInfer<{
142
+ enabled: z<boolean, boolean, "defined">;
143
+ }>>, Schemastery.ObjectT<NoInfer<{
144
+ enabled: z<boolean, boolean, "defined">;
145
+ }>>, "plain">;
146
+ apply: typeof apply;
147
+ };
148
+ export default _default;
@@ -0,0 +1,397 @@
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
+ * Flag gate (minimal, Task 5 formalizes the config face): row config
52
+ * `enabled`, defaulting OFF from the `SUPERD_UI_FUSION` env — while the flag
53
+ * is off the component stays dormant and injects no row at all.
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
+ /** The env name the minimal Task-3 gate reads; Task 5 builds the config face on it. */
323
+ export const FUSION_FLAG_ENV = 'SUPERD_UI_FUSION';
324
+ /**
325
+ * The page global the ctx0 index carries when the gate is on: the client
326
+ * half's fusion-branch discriminator (the Hub UI switch surfaces read its
327
+ * presence to intercept agent clicks into the switch orchestration instead
328
+ * of letting the hyperlinks navigate — the sole switch surface since the T-B
329
+ * RuntimeSeat retirement; world pages never carry it, so they keep the real
330
+ * navigation half — spec §5 双半边). Baked by the SAME gated listener as the
331
+ * transport script, so the two halves can never disagree.
332
+ */
333
+ export const FUSION_FLAG_GLOBAL = '__SUPERD_UI_FUSION__';
334
+ /**
335
+ * The flag's SEMANTICS after the path-separation Part 2 closeout (T-C/T-D
336
+ * landed; this block is the T-E closeout record) — native-ui-only, there is
337
+ * no fallback route anywhere:
338
+ *
339
+ * - ON: the ctx0 page fuses. The transport script arms the in-place switch
340
+ * and `/hub` is the sole surface for every runtime. The per-label URL
341
+ * family (`/<label>/` ctx0 pages, T-C) and the world web UIs (T-D) are
342
+ * RETIRED from the product, not gated by this flag — there is nothing
343
+ * navigational behind them to fall back to.
344
+ * - OFF: NO foreign entry exists AT ALL. The served product is the bare
345
+ * native UI plus the Hub panel as an information surface: the roster
346
+ * still lists the runtimes (the control plane is not gated), but the
347
+ * switch affordances render inert with the degradation hint
348
+ * (`switchAffordance` in switch-core) instead of dead controls — a row
349
+ * has no /<label>/ page to reach and the Hub UI ships no hrefs to reach
350
+ * one with.
351
+ *
352
+ * The shipped default stays OFF (truthy env opt-in). Moving the default ON is
353
+ * the user's 放行 (acceptance/release) round — a two-gate decision per repo
354
+ * §〇.1, never a code-side choice.
355
+ *
356
+ * Derivation: truthy spellings only (`1` / `true` / `yes` / `on`,
357
+ * case-insensitive, trimmed). Anything else — unset included — is OFF.
358
+ */
359
+ export function deriveFusionFlag(env) {
360
+ return ['1', 'true', 'yes', 'on'].includes((env ?? '').trim().toLowerCase());
361
+ }
362
+ /** Row config (schema-level default derives from the {@link FUSION_FLAG_ENV} env). */
363
+ export const Config = z.object({
364
+ enabled: z.boolean().default(deriveFusionFlag(process.env[FUSION_FLAG_ENV])),
365
+ });
366
+ /** Cordis plugin name. */
367
+ export const name = 'superd-fusion-transport';
368
+ /**
369
+ * No static injects: `webServer` is composed conditionally inside
370
+ * {@link apply}, so the row loads (dormant) in compositions without one.
371
+ */
372
+ export const inject = [];
373
+ /**
374
+ * Mount the fusion injection: one `webserver/index-inject` listener pushing
375
+ * the rendered head script row — the same emission path as the web-trust
376
+ * component, composed AFTER it (the composite row order is load order, and
377
+ * both components register their listeners behind `inject(['webServer'])`, so
378
+ * cordis's FIFO effect order pushes the fusion row last; spec §3.4) — plus
379
+ * the {@link FUSION_FLAG_GLOBAL} baked global row (Task 4): one gate, both
380
+ * halves.
381
+ *
382
+ * Flag off → the component registers nothing: no listener, no row, no script,
383
+ * no flag global.
384
+ * @param ctx - host plugin context.
385
+ * @param config - the row config.
386
+ */
387
+ export function apply(ctx, config) {
388
+ if (config?.enabled !== true)
389
+ return;
390
+ ctx.inject(['webServer'], () => {
391
+ ctx.on('webserver/index-inject', (table) => {
392
+ table.push({ kind: 'script', placement: 'head', text: renderFusionTransport() });
393
+ table.push({ kind: 'global', name: FUSION_FLAG_GLOBAL, value: true });
394
+ });
395
+ });
396
+ }
397
+ 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;