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.
- package/README.md +6 -7
- package/agent-agy/dist/world-plugin.js +18 -33
- package/agent-codex/dist/world-plugin.js +18 -33
- package/agent-hermes/dist/world-plugin.js +18 -34
- package/agent-omp/dist/world-plugin.js +16 -32
- package/agent-pi/dist/world-plugin.js +18 -33
- package/client/index.js +2086 -0
- package/cordis.patch.yml +21 -7
- package/package.json +13 -3
- package/superd-hub/dist/agent-roster.d.ts +3 -1
- package/superd-hub/dist/agent-roster.js +4 -2
- package/superd-hub/dist/carrier.d.ts +41 -28
- package/superd-hub/dist/carrier.js +148 -202
- package/superd-hub/dist/fusion-transport.d.ts +150 -0
- package/superd-hub/dist/fusion-transport.js +369 -0
- package/superd-hub/dist/gateway.d.ts +7 -2
- package/superd-hub/dist/gateway.js +28 -2
- package/superd-hub/dist/hub-route.d.ts +74 -0
- package/superd-hub/dist/hub-route.js +188 -0
- package/superd-hub/dist/index.d.ts +10 -4
- package/superd-hub/dist/index.js +10 -4
- package/superd-hub/dist/roster.d.ts +2 -2
- package/superd-hub/dist/spawn-world.js +5 -3
- package/superd-hub/dist/switch-core.d.ts +630 -0
- package/superd-hub/dist/switch-core.js +734 -0
- package/superd-hub/dist/web-trust.d.ts +5 -5
- package/superd-hub/dist/web-trust.js +5 -5
- package/superd-hub/dist/world-entry.d.ts +20 -13
- package/superd-hub/dist/world-entry.js +15 -19
- package/superd-hub/dist/world-host.d.ts +15 -20
- package/superd-hub/dist/world-host.js +25 -4
- package/superd-hub/dist/world-join.js +14 -29
- package/superd-hub/dist/world-mount.d.ts +48 -26
- package/superd-hub/dist/world-mount.js +59 -27
- package/superd-hub/dist/world-provision.js +11 -4
- package/superd-hub/dist/world-web-server.d.ts +57 -106
- package/superd-hub/dist/world-web-server.js +61 -216
- package/superd-hub/lib/client/index.js +2086 -0
- 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
|
-
*
|
|
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
|
-
*
|
|
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;
|