super-dsh 0.1.11 → 0.1.12
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 +54 -6
- package/agent-agy/dist/agent.d.ts +4 -0
- package/agent-agy/dist/agent.js +41 -7
- package/agent-agy/dist/agy-cli-client.d.ts +12 -0
- package/agent-agy/dist/agy-cli-client.js +81 -13
- package/agent-agy/dist/index.js +13 -7
- package/agent-agy/package.json +5 -1
- package/agent-claude/dist/agent.js +67 -4
- package/agent-claude/dist/claude-client.js +46 -1
- package/agent-claude/dist/index.js +3 -0
- package/agent-claude/dist/sdk-runtime.js +2 -2
- package/agent-codex/dist/agent.js +33 -8
- package/agent-codex/dist/app-server-client.js +26 -3
- package/agent-codex/dist/app-server-protocol.js +6 -2
- package/agent-codex/dist/app-server-wire.js +23 -6
- package/agent-codex/dist/codex-native-config.js +6 -1
- package/agent-codex/dist/codex-runtime-fetch.js +293 -0
- package/agent-codex/dist/codex-runtime.js +12 -12
- package/agent-codex/dist/index.js +10 -4
- package/agent-codex/package.json +8 -3
- package/agent-codex-sdk/dist/sdk-runtime.js +1 -1
- package/agent-hermes/dist/hermes-client.js +44 -5
- package/agent-hermes/dist/index.js +10 -5
- package/agent-omp/dist/agent.js +36 -5
- package/agent-omp/dist/index.js +29 -6
- package/agent-omp/dist/lazy-rpc.js +11 -2
- package/agent-omp/dist/rpc-client.js +6 -1
- package/agent-pi/dist/agent.js +36 -9
- package/agent-pi/dist/index.js +6 -2
- package/agent-pi/dist/pi-client.js +13 -3
- package/cordis.patch.yml +10 -9
- package/package.json +6 -4
- package/superd-hub/dist/a2a-row.js +20 -3
- package/superd-hub/dist/connection-trust.d.ts +8 -0
- package/superd-hub/dist/connection-trust.js +16 -0
- package/superd-hub/dist/foreign-agy.d.ts +3 -0
- package/superd-hub/dist/foreign-agy.js +110 -0
- package/superd-hub/dist/foreign-claude.d.ts +4 -0
- package/superd-hub/dist/foreign-claude.js +160 -0
- package/superd-hub/dist/foreign-extension.d.ts +10 -0
- package/superd-hub/dist/foreign-extension.js +27 -0
- package/superd-hub/dist/foreign-hub.d.ts +27 -0
- package/superd-hub/dist/foreign-hub.js +243 -0
- package/superd-hub/dist/foreign-native.d.ts +4 -0
- package/superd-hub/dist/foreign-native.js +115 -0
- package/superd-hub/dist/web-trust.d.ts +14 -42
- package/superd-hub/dist/web-trust.js +18 -43
- package/superd-hub/package.json +14 -2
- package/agent-codex/README.md +0 -191
- package/agent-codex-sdk/README.md +0 -79
- package/agent-hermes/README.md +0 -145
- package/agent-omp/README.md +0 -94
- package/agent-omp-sdk/README.md +0 -103
- package/agent-pi/README.md +0 -67
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Antigravity native plugin: tool catalog via stdio, delivery via the owned CLI. */
|
|
2
|
+
import { mkdir, readFile, readdir, writeFile, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
const owner = 'super-dsh:agy-hub-plugin:v1\n';
|
|
6
|
+
// t and earlier test installers appended a second LF; recognize that exact
|
|
7
|
+
// owned legacy form while rewriting the marker canonically below.
|
|
8
|
+
const isOwner = (marker) => marker === owner || marker === owner + '\n';
|
|
9
|
+
const jobs = new Map();
|
|
10
|
+
async function serialized(home, work) {
|
|
11
|
+
const key = resolve(home);
|
|
12
|
+
const current = (jobs.get(key) ?? Promise.resolve()).catch(() => { }).then(work);
|
|
13
|
+
jobs.set(key, current);
|
|
14
|
+
try {
|
|
15
|
+
return await current;
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
if (jobs.get(key) === current)
|
|
19
|
+
jobs.delete(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function atomic(path, content) {
|
|
23
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
24
|
+
try {
|
|
25
|
+
await writeFile(temporary, content, { flag: 'wx', mode: 0o600 });
|
|
26
|
+
await rename(temporary, path);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await rm(temporary, { force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Only MCP's synchronous catalog/call surface is used. Incoming A2A delivery
|
|
33
|
+
// never uses MCP notifications: the Hub already owns the live CLI stdin.
|
|
34
|
+
const serverModule = String.raw `const {readFileSync}=require('node:fs');
|
|
35
|
+
const {join}=require('node:path');
|
|
36
|
+
const {createInterface}=require('node:readline');
|
|
37
|
+
const publicConfig=()=>JSON.parse(readFileSync(join(__dirname,'connection.json'),'utf8'));
|
|
38
|
+
async function request(m){
|
|
39
|
+
if(m.method==='initialize')return {protocolVersion:m.params?.protocolVersion??'2024-11-05',capabilities:{tools:{}},serverInfo:{name:'superd-hub',version:'1.0.0'}};
|
|
40
|
+
if(m.method==='ping')return {};
|
|
41
|
+
if(m.method==='tools/list')return {tools:[publicConfig().definition]};
|
|
42
|
+
if(m.method==='tools/call'){
|
|
43
|
+
if(m.params?.name!=='superd-hub')throw Object.assign(new Error('Unknown tool'),{code:-32602});
|
|
44
|
+
try {
|
|
45
|
+
const privateSession=process.env.SUPERD_HUB_SESSION;
|
|
46
|
+
const config=privateSession?JSON.parse(privateSession):publicConfig();
|
|
47
|
+
const url=new URL(config.url);
|
|
48
|
+
if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.pathname!=='/tool')throw new Error('Hub requires a same-machine loopback URL');
|
|
49
|
+
const headers={'content-type':'application/json'};
|
|
50
|
+
if(privateSession)headers.authorization='Bearer '+config.token;
|
|
51
|
+
const response=await fetch(url,{method:'POST',headers,body:JSON.stringify(m.params.arguments??{}),signal:AbortSignal.timeout(120000)});
|
|
52
|
+
const envelope=await response.json();
|
|
53
|
+
return {content:[{type:'text',text:JSON.stringify(envelope.error?{error:envelope.error}:envelope.result)}],isError:!response.ok||!!envelope.error};
|
|
54
|
+
}catch(error){return {content:[{type:'text',text:'[E_HUB_UNAVAILABLE] '+error.message+'. Open or resume through Super D.'}],isError:true};}
|
|
55
|
+
}
|
|
56
|
+
throw Object.assign(new Error('Method not found'),{code:-32601});
|
|
57
|
+
}
|
|
58
|
+
for await (const line of createInterface({input:process.stdin,crlfDelay:Infinity})){
|
|
59
|
+
let m;try{m=JSON.parse(line)}catch{process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:null,error:{code:-32700,message:'Parse error'}})+'\n');continue;}
|
|
60
|
+
if(m.id===undefined)continue;
|
|
61
|
+
try{const result=await request(m);process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:m.id,result})+'\n');}
|
|
62
|
+
catch(error){process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:m.id,error:{code:error.code??-32603,message:error.message}})+'\n');}
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
// CJS permits the installed helper to run without a package dependency or loader.
|
|
66
|
+
const server = serverModule.replace('for await (const line', ';(async()=>{for await (const line') + '})().catch(()=>process.exitCode=1);\n';
|
|
67
|
+
export async function refreshAgyNativeEndpoint(endpoint, nativeHome) {
|
|
68
|
+
await serialized(nativeHome, async () => {
|
|
69
|
+
const directory = join(nativeHome, 'plugins', 'superd-hub');
|
|
70
|
+
if (!isOwner(await readFile(join(directory, '.superd-owner'), 'utf8').catch(() => '')))
|
|
71
|
+
return;
|
|
72
|
+
const config = JSON.parse(await readFile(join(directory, 'connection.json'), 'utf8'));
|
|
73
|
+
const url = await endpoint();
|
|
74
|
+
if (url !== undefined)
|
|
75
|
+
await atomic(join(directory, 'connection.json'), JSON.stringify({ url, definition: config.definition }) + '\n');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function prepareAgyNativePlugin(binding, nativeHome, stagingDirectory, install) {
|
|
79
|
+
const transport = await binding.transport();
|
|
80
|
+
if (transport === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
return serialized(nativeHome, async () => {
|
|
83
|
+
const directory = join(nativeHome, 'plugins', 'superd-hub');
|
|
84
|
+
const entries = await readdir(directory).catch((error) => { if (error.code !== 'ENOENT')
|
|
85
|
+
throw error; return []; });
|
|
86
|
+
if (entries.length > 0 && !isOwner(await readFile(join(directory, '.superd-owner'), 'utf8').catch(() => '')))
|
|
87
|
+
throw new Error(`Refusing to overwrite unowned native plugin directory: ${directory}`);
|
|
88
|
+
await mkdir(stagingDirectory, { recursive: true, mode: 0o700 });
|
|
89
|
+
const files = {
|
|
90
|
+
'.superd-owner': owner,
|
|
91
|
+
'plugin.json': JSON.stringify({ name: 'superd-hub', description: 'Substrate Hub facility for Super D conversations' }),
|
|
92
|
+
'connection.json': JSON.stringify({ url: transport.url, definition: transport.definition }),
|
|
93
|
+
'server.cjs': server,
|
|
94
|
+
'mcp_config.json': JSON.stringify({ mcpServers: { 'superd-hub': { command: process.execPath, args: [join(directory, 'server.cjs')] } } }),
|
|
95
|
+
};
|
|
96
|
+
const payload = (name, content) => name === '.superd-owner' ? content : content + '\n';
|
|
97
|
+
for (const [name, content] of Object.entries(files))
|
|
98
|
+
await atomic(join(stagingDirectory, name), payload(name, content));
|
|
99
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
100
|
+
for (const [name, content] of Object.entries(files))
|
|
101
|
+
await atomic(join(directory, name), payload(name, content));
|
|
102
|
+
await install(stagingDirectory);
|
|
103
|
+
// The official installer may skip non-customization helper files; ensure
|
|
104
|
+
// our own staged payload is present, with no capability written publicly.
|
|
105
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
106
|
+
for (const [name, content] of Object.entries(files))
|
|
107
|
+
await atomic(join(directory, name), payload(name, content));
|
|
108
|
+
return JSON.stringify(transport);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ForeignHubBinding } from './foreign-hub.js';
|
|
2
|
+
export declare function refreshClaudeNativeEndpoint(endpoint: () => Promise<string | undefined>, nativeHome: string): Promise<void>;
|
|
3
|
+
/** Incrementally enable our owned native plugin; never persist a session capability. */
|
|
4
|
+
export declare function prepareClaudeNativePlugin(binding: Pick<ForeignHubBinding, 'transport'>, nativeHome: string): Promise<string | undefined>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** Claude's native Mods API: one tool and an addressed, background inbox. */
|
|
2
|
+
import { mkdir, readFile, readdir, writeFile, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve, delimiter } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
const owner = 'super-dsh:claude-hub-mod:v1\n';
|
|
6
|
+
const preparing = new Map();
|
|
7
|
+
async function serialized(home, work) {
|
|
8
|
+
const key = resolve(home);
|
|
9
|
+
const current = (preparing.get(key) ?? Promise.resolve()).catch(() => { }).then(work);
|
|
10
|
+
preparing.set(key, current);
|
|
11
|
+
try {
|
|
12
|
+
return await current;
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
if (preparing.get(key) === current)
|
|
16
|
+
preparing.delete(key);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function atomicWrite(path, content) {
|
|
20
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
21
|
+
try {
|
|
22
|
+
await writeFile(temporary, content, { mode: 0o600, flag: 'wx' });
|
|
23
|
+
await rename(temporary, path);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await rm(temporary, { force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Runs inside Claude's Mod sandbox. No Node imports, MCP process, prompt
|
|
30
|
+
// injection or turn hooks. Native unaddressed sessions register the tool but
|
|
31
|
+
// do not poll; the post office itself reports E_HUB_ADDRESS_REQUIRED.
|
|
32
|
+
const moduleSource = String.raw `async function connection($) {
|
|
33
|
+
const privateConfig = await $.env.get("SUPERD_HUB_SESSION");
|
|
34
|
+
const config = JSON.parse(privateConfig || await $.fs.read($.plugin.root + "/connection.json"));
|
|
35
|
+
if (!config.url.startsWith("http://127.0.0.1:")) throw new Error("Super D Hub requires same-machine loopback HTTP");
|
|
36
|
+
return { config, addressed: Boolean(privateConfig) };
|
|
37
|
+
}
|
|
38
|
+
function headers(config, addressed) {
|
|
39
|
+
return { "Content-Type": "application/json", ...(addressed ? { Authorization: "Bearer " + config.token } : {}) };
|
|
40
|
+
}
|
|
41
|
+
export function register(on) {
|
|
42
|
+
let timer;
|
|
43
|
+
let receiving = false;
|
|
44
|
+
let submitted;
|
|
45
|
+
const queued = new Map();
|
|
46
|
+
on("session.start", async ($, e, next) => {
|
|
47
|
+
timer?.cancel();
|
|
48
|
+
const { config, addressed } = await connection($);
|
|
49
|
+
await $.tool.register(config.definition);
|
|
50
|
+
if (addressed) timer = $.clock.every(250, async () => {
|
|
51
|
+
if (receiving) return;
|
|
52
|
+
receiving = true;
|
|
53
|
+
try {
|
|
54
|
+
for (const [id, pending] of queued) if (pending.aborted) { queued.delete(id); submitted = undefined; }
|
|
55
|
+
const url = config.url.replace(/\/tool$/, "/inbox");
|
|
56
|
+
const response = await $.http.fetch(url, { method: "GET", headers: headers(config, true) });
|
|
57
|
+
if (response.status === 401 || response.status === 403) { timer?.cancel(); return; }
|
|
58
|
+
if (!response.ok) return;
|
|
59
|
+
const message = JSON.parse(response.text).message;
|
|
60
|
+
if (!message) return;
|
|
61
|
+
if (submitted !== message.id) {
|
|
62
|
+
const marker = "[superd-hub:inbox message=" + message.id + "]";
|
|
63
|
+
queued.set(message.id, { marker, url, config, confirmed: false, aborted: false });
|
|
64
|
+
const outcome = await $.prompt.submit({ text: marker + "\n" + message.text });
|
|
65
|
+
if (outcome && "drop" in outcome) { queued.delete(message.id); return; }
|
|
66
|
+
submitted = message.id;
|
|
67
|
+
}
|
|
68
|
+
// An unconfirmed turn is cancelled before its model runs and remains
|
|
69
|
+
// in the inbox to retry; never ACK it or send a late start receipt.
|
|
70
|
+
if (!queued.get(message.id)?.confirmed) return;
|
|
71
|
+
// Ack only after the runtime has accepted the plugin's prompt. If the
|
|
72
|
+
// ack fails, retry it without submitting the same prompt twice.
|
|
73
|
+
const ack = await $.http.fetch(url, { method: "POST", headers: headers(config, true), body: JSON.stringify({ ack: message.id }) });
|
|
74
|
+
if (ack.ok) { submitted = undefined; queued.delete(message.id); }
|
|
75
|
+
} catch (_) {
|
|
76
|
+
// A temporary offline post office leaves the message queued there.
|
|
77
|
+
} finally { receiving = false; }
|
|
78
|
+
});
|
|
79
|
+
return next(e);
|
|
80
|
+
});
|
|
81
|
+
// This receipt binds the Hub's source message to its actual native turn;
|
|
82
|
+
// a different user/plugin turn cannot claim it by happening to run first.
|
|
83
|
+
on("turn.start", async ($, e, next) => {
|
|
84
|
+
for (const [id, pending] of queued) {
|
|
85
|
+
if (!e.text.includes(pending.marker)) continue;
|
|
86
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
87
|
+
try {
|
|
88
|
+
const response = await $.http.fetch(pending.url, { method: "POST", headers: headers(pending.config, true), body: JSON.stringify({ started: id }) });
|
|
89
|
+
if (response.ok) { pending.confirmed = true; break; }
|
|
90
|
+
if (response.status === 401 || response.status === 403) break;
|
|
91
|
+
} catch (_) {}
|
|
92
|
+
}
|
|
93
|
+
if (!pending.confirmed) {
|
|
94
|
+
pending.aborted = true;
|
|
95
|
+
await $.turn.abort({ turnId: e.turnId });
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
return next(e);
|
|
100
|
+
});
|
|
101
|
+
on("tool.call", { tool: "mcp__superd-hub__superd-hub" }, async ($, e) => {
|
|
102
|
+
try {
|
|
103
|
+
const { config, addressed } = await connection($);
|
|
104
|
+
const { tool, tool_use_id, agentId, consent, ...args } = e;
|
|
105
|
+
const response = await $.http.fetch(config.url, { method: "POST", headers: headers(config, addressed), body: JSON.stringify(args) });
|
|
106
|
+
const envelope = JSON.parse(response.text);
|
|
107
|
+
return { result: JSON.stringify(envelope.error ? { error: envelope.error } : envelope.result) };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return { result: JSON.stringify({ error: "[E_HUB_UNAVAILABLE] " + String(error) + ". Open or resume this conversation through Super D." }) };
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
export async function refreshClaudeNativeEndpoint(endpoint, nativeHome) {
|
|
115
|
+
await serialized(nativeHome, async () => {
|
|
116
|
+
const directory = join(nativeHome, 'plugins', 'superd-hub');
|
|
117
|
+
if (await readFile(join(directory, '.superd-owner'), 'utf8').catch(() => '') !== owner)
|
|
118
|
+
return;
|
|
119
|
+
const config = JSON.parse(await readFile(join(directory, 'connection.json'), 'utf8'));
|
|
120
|
+
const url = await endpoint();
|
|
121
|
+
if (url !== undefined)
|
|
122
|
+
await atomicWrite(join(directory, 'connection.json'), JSON.stringify({ url, definition: config.definition }) + '\n');
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** Incrementally enable our owned native plugin; never persist a session capability. */
|
|
126
|
+
export async function prepareClaudeNativePlugin(binding, nativeHome) {
|
|
127
|
+
const transport = await binding.transport();
|
|
128
|
+
if (transport === undefined)
|
|
129
|
+
return undefined;
|
|
130
|
+
return serialized(nativeHome, async () => {
|
|
131
|
+
const directory = join(nativeHome, 'plugins', 'superd-hub');
|
|
132
|
+
const entries = await readdir(directory).catch(error => {
|
|
133
|
+
if (error.code !== 'ENOENT')
|
|
134
|
+
throw error;
|
|
135
|
+
return [];
|
|
136
|
+
});
|
|
137
|
+
if (entries.length > 0 && await readFile(join(directory, '.superd-owner'), 'utf8').catch(() => '') !== owner) {
|
|
138
|
+
throw new Error(`Refusing to overwrite unowned native plugin directory: ${directory}`);
|
|
139
|
+
}
|
|
140
|
+
const settingsPath = join(nativeHome, 'settings.json');
|
|
141
|
+
const settings = JSON.parse(await readFile(settingsPath, 'utf8').catch(error => {
|
|
142
|
+
if (error.code !== 'ENOENT')
|
|
143
|
+
throw error;
|
|
144
|
+
return '{}';
|
|
145
|
+
}));
|
|
146
|
+
const paths = (settings.env?.['CLAUDE_CODE_PLUGIN_DIRS'] ?? '').split(delimiter).filter(Boolean);
|
|
147
|
+
if (!paths.includes(directory))
|
|
148
|
+
paths.push(directory);
|
|
149
|
+
await mkdir(join(directory, '.claude-plugin'), { recursive: true, mode: 0o700 });
|
|
150
|
+
await mkdir(join(directory, 'hooks'), { recursive: true, mode: 0o700 });
|
|
151
|
+
await atomicWrite(join(directory, '.superd-owner'), owner);
|
|
152
|
+
await atomicWrite(join(directory, 'connection.json'), JSON.stringify({ url: transport.url, definition: transport.definition }) + '\n');
|
|
153
|
+
await atomicWrite(join(directory, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'superd-hub', version: '1.0.0', description: 'Substrate Hub facility for Super D conversations' }) + '\n');
|
|
154
|
+
await atomicWrite(join(directory, 'hooks', 'hooks.json'), JSON.stringify({ modules: ['./register.js'] }) + '\n');
|
|
155
|
+
await atomicWrite(join(directory, 'hooks', 'register.js'), moduleSource);
|
|
156
|
+
settings.env = { ...settings.env, CLAUDE_CODE_PLUGIN_DIRS: paths.join(delimiter) };
|
|
157
|
+
await atomicWrite(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
158
|
+
return JSON.stringify(transport);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Shared Pi / OMP native extension. No runtime imports or ambient home changes. */
|
|
2
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
export interface ForeignExtensionConfig {
|
|
4
|
+
url: string;
|
|
5
|
+
token: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
parameters: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export declare function createForeignExtension(config: ForeignExtensionConfig): (api: ExtensionAPI) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function createForeignExtension(config) {
|
|
2
|
+
return (api) => {
|
|
3
|
+
api.registerTool({
|
|
4
|
+
name: config.name,
|
|
5
|
+
label: 'Super D Hub',
|
|
6
|
+
description: config.description,
|
|
7
|
+
promptSnippet: 'Use the Super D Hub for agent messaging, runtime status, and remote execution.',
|
|
8
|
+
parameters: config.parameters,
|
|
9
|
+
async execute(_callId, args, signal) {
|
|
10
|
+
const response = await fetch(config.url, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { authorization: `Bearer ${config.token}`, 'content-type': 'application/json' },
|
|
13
|
+
body: JSON.stringify(args),
|
|
14
|
+
...(signal === undefined ? {} : { signal }),
|
|
15
|
+
}).catch((error) => {
|
|
16
|
+
if (signal?.aborted)
|
|
17
|
+
throw error;
|
|
18
|
+
throw new Error('[E_HUB_UNAVAILABLE] Super D Hub connection is unavailable. Restart Super D, then resume this conversation to renew its session binding.');
|
|
19
|
+
});
|
|
20
|
+
const body = await response.json();
|
|
21
|
+
if (!response.ok)
|
|
22
|
+
throw new Error(body.error ?? `Super D Hub returned HTTP ${response.status}`);
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify(body.result) }], details: body.result };
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { A2aAgentHandle } from './a2a-delivery.js';
|
|
2
|
+
import { type HubToolDeps } from './hub-tool.js';
|
|
3
|
+
export interface ForeignToolDefinition {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface ForeignHubBinding {
|
|
9
|
+
definition(): ForeignToolDefinition | undefined;
|
|
10
|
+
execute(args: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
11
|
+
transport(): Promise<ForeignHubTransport | undefined>;
|
|
12
|
+
enqueue(text: string): Promise<string>;
|
|
13
|
+
onTurnStarted(listener: (id: string) => void): () => void;
|
|
14
|
+
extensionPath(): Promise<string | undefined>;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
}
|
|
17
|
+
export interface ForeignHubTransport {
|
|
18
|
+
url: string;
|
|
19
|
+
token: string;
|
|
20
|
+
definition: ForeignToolDefinition;
|
|
21
|
+
}
|
|
22
|
+
export declare function foreignAgentHandle(home: string, sessionId: string): A2aAgentHandle | undefined;
|
|
23
|
+
/** Register exactly the assembly used by the DSH-native Hub tool. */
|
|
24
|
+
export declare function registerForeignHubHost(home: string, deps: Omit<HubToolDeps, 'initiatorOf'>): () => Promise<void>;
|
|
25
|
+
/** Public native-plugin endpoint; publishing it never grants a caller identity. */
|
|
26
|
+
export declare function foreignHubEndpoint(home: string): Promise<string | undefined>;
|
|
27
|
+
export declare function bindForeignHub(runtime: string, sessionId: string, worldHome: string, getAgent: () => A2aAgentHandle | undefined): ForeignHubBinding;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/** Session-bound facility bridge. Host-owned transport; drafts perform no I/O. */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { rmSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { createSuperdHubTool } from './hub-tool.js';
|
|
8
|
+
import { localA2aInitiator } from './a2a-host.js';
|
|
9
|
+
const hosts = new Map();
|
|
10
|
+
// Delivery is a separate in-memory registration: passive resume must be
|
|
11
|
+
// reachable before runtime/tool materialization, without minting capabilities.
|
|
12
|
+
const deliveryBindings = new Map();
|
|
13
|
+
export function foreignAgentHandle(home, sessionId) {
|
|
14
|
+
const key = resolve(home);
|
|
15
|
+
if (!hosts.has(key))
|
|
16
|
+
return undefined;
|
|
17
|
+
for (const session of deliveryBindings.values()) {
|
|
18
|
+
if (session.home === key && session.sessionId === sessionId)
|
|
19
|
+
return session.getAgent();
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Register exactly the assembly used by the DSH-native Hub tool. */
|
|
24
|
+
export function registerForeignHubHost(home, deps) {
|
|
25
|
+
const key = resolve(home);
|
|
26
|
+
const host = { home: key, deps, sessions: new Map(), closed: false };
|
|
27
|
+
hosts.set(key, host);
|
|
28
|
+
return async () => {
|
|
29
|
+
host.closed = true;
|
|
30
|
+
host.sessions.clear();
|
|
31
|
+
if (hosts.get(key) === host)
|
|
32
|
+
hosts.delete(key);
|
|
33
|
+
if (host.server !== undefined) {
|
|
34
|
+
host.server.closeAllConnections();
|
|
35
|
+
await new Promise((done) => host.server.close(() => done()));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function listen(host) {
|
|
40
|
+
if (host.closed)
|
|
41
|
+
throw new Error('[E_HUB_CLOSED] Super D Hub binding is closed');
|
|
42
|
+
if (host.listening !== undefined)
|
|
43
|
+
return host.listening;
|
|
44
|
+
const server = createServer(async (request, response) => {
|
|
45
|
+
const send = (status, body) => {
|
|
46
|
+
response.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
47
|
+
response.end(JSON.stringify(body));
|
|
48
|
+
};
|
|
49
|
+
const token = request.headers.authorization?.replace(/^Bearer /, '');
|
|
50
|
+
if (token === undefined) {
|
|
51
|
+
send(403, { error: '[E_HUB_ADDRESS_REQUIRED] This native session has no Substrate Hub address. Open or resume the conversation through Super D; a plugin alone does not register a mailbox.' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const bound = token === undefined ? undefined : host.sessions.get(token);
|
|
55
|
+
if (host.closed || bound === undefined) {
|
|
56
|
+
send(401, { error: '[E_HUB_BINDING] Unknown or expired Super D Hub session binding' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (request.method === 'GET' && request.url === '/inbox') {
|
|
60
|
+
send(200, { message: bound.inbox[0] ?? null });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (request.method !== 'POST' || !['/tool', '/inbox'].includes(request.url ?? '')) {
|
|
64
|
+
send(405, { error: 'Use POST /tool, GET /inbox or POST /inbox' });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
response.on('close', () => { if (!response.writableEnded)
|
|
69
|
+
controller.abort(); });
|
|
70
|
+
try {
|
|
71
|
+
let body = '';
|
|
72
|
+
for await (const chunk of request) {
|
|
73
|
+
body += chunk.toString();
|
|
74
|
+
if (Buffer.byteLength(body) > 65_536) {
|
|
75
|
+
send(413, { error: 'Super D Hub arguments exceed 64 KiB' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const args = JSON.parse(body);
|
|
80
|
+
if (request.url === '/inbox') {
|
|
81
|
+
if (typeof args?.started === 'string') {
|
|
82
|
+
if (!bound.unstarted.has(args.started) && !bound.started.has(args.started)) {
|
|
83
|
+
send(409, { error: '[E_HUB_INBOX_START] Unknown message' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (bound.unstarted.has(args.started)) {
|
|
87
|
+
bound.onTurnStarted(args.started);
|
|
88
|
+
bound.unstarted.delete(args.started);
|
|
89
|
+
bound.started.add(args.started);
|
|
90
|
+
if (bound.started.size > 128)
|
|
91
|
+
bound.started.delete(bound.started.values().next().value);
|
|
92
|
+
}
|
|
93
|
+
send(200, { started: true });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (typeof args?.ack !== 'string' || args.ack !== bound.inbox[0]?.id) {
|
|
97
|
+
send(409, { error: '[E_HUB_INBOX_ACK] Message is not the current inbox item' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
bound.inbox.shift();
|
|
101
|
+
send(200, { acknowledged: true });
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
send(200, { result: await bound.execute(args, controller.signal) });
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (!response.writableEnded)
|
|
108
|
+
send(400, { error: error instanceof Error ? error.message : String(error) });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
host.server = server;
|
|
112
|
+
host.listening = new Promise((accept, reject) => {
|
|
113
|
+
server.once('error', reject);
|
|
114
|
+
server.listen(0, '127.0.0.1', () => {
|
|
115
|
+
const address = server.address();
|
|
116
|
+
if (address === null || typeof address === 'string') {
|
|
117
|
+
reject(new Error('Super D Hub loopback listener has no address'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
server.unref();
|
|
121
|
+
accept(`http://127.0.0.1:${address.port}/tool`);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
return host.listening;
|
|
125
|
+
}
|
|
126
|
+
/** Public native-plugin endpoint; publishing it never grants a caller identity. */
|
|
127
|
+
export async function foreignHubEndpoint(home) {
|
|
128
|
+
const host = hosts.get(resolve(home));
|
|
129
|
+
return host === undefined || host.closed ? undefined : listen(host);
|
|
130
|
+
}
|
|
131
|
+
export function bindForeignHub(runtime, sessionId, worldHome, getAgent) {
|
|
132
|
+
const home = resolve(worldHome);
|
|
133
|
+
const hostHome = dirname(home).endsWith('/agents') ? dirname(dirname(home)) : home;
|
|
134
|
+
const deliveryKey = Symbol('foreign-hub-delivery');
|
|
135
|
+
deliveryBindings.set(deliveryKey, { home: hostHome, sessionId, getAgent });
|
|
136
|
+
let attached;
|
|
137
|
+
let tool;
|
|
138
|
+
let token;
|
|
139
|
+
let path;
|
|
140
|
+
let preparing;
|
|
141
|
+
let disposed = false;
|
|
142
|
+
const inbox = [];
|
|
143
|
+
const unstarted = new Set();
|
|
144
|
+
const started = new Set();
|
|
145
|
+
const turnListeners = new Set();
|
|
146
|
+
const attach = () => {
|
|
147
|
+
if (disposed)
|
|
148
|
+
throw new Error('[E_HUB_BINDING] Super D Hub session binding is closed');
|
|
149
|
+
const host = hosts.get(hostHome);
|
|
150
|
+
if (host === undefined || host.closed)
|
|
151
|
+
return undefined;
|
|
152
|
+
if (attached !== host) {
|
|
153
|
+
if (attached !== undefined && token !== undefined)
|
|
154
|
+
attached.sessions.delete(token);
|
|
155
|
+
attached = host;
|
|
156
|
+
token = randomUUID();
|
|
157
|
+
tool = createSuperdHubTool({ ...host.deps, initiatorOf: () => localA2aInitiator(host.home, runtime, sessionId) });
|
|
158
|
+
host.sessions.set(token, { runtime, sessionId, getAgent, execute: binding.execute, inbox, unstarted, started, onTurnStarted: id => { for (const listener of turnListeners)
|
|
159
|
+
listener(id); } });
|
|
160
|
+
}
|
|
161
|
+
return tool;
|
|
162
|
+
};
|
|
163
|
+
const binding = {
|
|
164
|
+
definition() {
|
|
165
|
+
const current = attach();
|
|
166
|
+
return current === undefined ? undefined : { name: current.name, description: current.description, inputSchema: current.parameters };
|
|
167
|
+
},
|
|
168
|
+
async execute(args, signal = new AbortController().signal) {
|
|
169
|
+
const current = attach();
|
|
170
|
+
if (current === undefined)
|
|
171
|
+
throw new Error('[E_HUB_UNAVAILABLE] Super D Hub is not running for this session; reconnect through Super D');
|
|
172
|
+
// A host tool context, with identity supplied by the binding rather than model arguments.
|
|
173
|
+
const exec = {
|
|
174
|
+
callId: randomUUID(), rootCallId: randomUUID(), name: current.name, arguments: args,
|
|
175
|
+
agent: { id: sessionId }, signal, token: Symbol('foreign-hub-call'),
|
|
176
|
+
deferContext() { }, concludeTurn() { },
|
|
177
|
+
};
|
|
178
|
+
return current.execute(args, exec);
|
|
179
|
+
},
|
|
180
|
+
async transport() {
|
|
181
|
+
const definition = binding.definition();
|
|
182
|
+
if (definition === undefined)
|
|
183
|
+
return undefined;
|
|
184
|
+
const host = attached;
|
|
185
|
+
const capability = token;
|
|
186
|
+
const url = await listen(host);
|
|
187
|
+
if (disposed)
|
|
188
|
+
throw new Error('[E_HUB_BINDING] Super D Hub session binding is closed');
|
|
189
|
+
return { url, token: capability, definition };
|
|
190
|
+
},
|
|
191
|
+
async enqueue(text) {
|
|
192
|
+
if (attach() === undefined)
|
|
193
|
+
throw new Error('[E_HUB_UNAVAILABLE] Super D Hub is not running for this session');
|
|
194
|
+
if (Buffer.byteLength(text) > 65_536 || inbox.length >= 128 || unstarted.size >= 128)
|
|
195
|
+
throw new Error('[E_HUB_INBOX_FULL] Hub inbox is full or the message exceeds 64 KiB');
|
|
196
|
+
const id = randomUUID();
|
|
197
|
+
unstarted.add(id);
|
|
198
|
+
inbox.push({ id, text });
|
|
199
|
+
return id;
|
|
200
|
+
},
|
|
201
|
+
onTurnStarted(listener) { turnListeners.add(listener); return () => turnListeners.delete(listener); },
|
|
202
|
+
async extensionPath() {
|
|
203
|
+
if (preparing !== undefined)
|
|
204
|
+
return preparing;
|
|
205
|
+
const definition = binding.definition();
|
|
206
|
+
if (definition === undefined)
|
|
207
|
+
return undefined;
|
|
208
|
+
const host = attached;
|
|
209
|
+
const capability = token;
|
|
210
|
+
preparing = (async () => {
|
|
211
|
+
const url = await listen(host);
|
|
212
|
+
if (disposed)
|
|
213
|
+
throw new Error('[E_HUB_BINDING] Super D Hub session binding is closed');
|
|
214
|
+
const directory = join(home, 'hub-extensions');
|
|
215
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
216
|
+
const wrapper = join(directory, `${capability}.mjs`);
|
|
217
|
+
const config = { url, token: capability, name: definition.name, description: definition.description, parameters: definition.inputSchema };
|
|
218
|
+
const entry = new URL('./foreign-extension.js', import.meta.url).href;
|
|
219
|
+
await writeFile(wrapper, `import { createForeignExtension } from ${JSON.stringify(entry)};\nexport default createForeignExtension(${JSON.stringify(config)});\n`, { mode: 0o600 });
|
|
220
|
+
if (disposed) {
|
|
221
|
+
rmSync(wrapper, { force: true });
|
|
222
|
+
throw new Error('[E_HUB_BINDING] Super D Hub session binding is closed');
|
|
223
|
+
}
|
|
224
|
+
path = wrapper;
|
|
225
|
+
return wrapper;
|
|
226
|
+
})();
|
|
227
|
+
return preparing;
|
|
228
|
+
},
|
|
229
|
+
dispose() {
|
|
230
|
+
disposed = true;
|
|
231
|
+
inbox.length = 0;
|
|
232
|
+
unstarted.clear();
|
|
233
|
+
started.clear();
|
|
234
|
+
turnListeners.clear();
|
|
235
|
+
deliveryBindings.delete(deliveryKey);
|
|
236
|
+
if (attached !== undefined && token !== undefined)
|
|
237
|
+
attached.sessions.delete(token);
|
|
238
|
+
if (path !== undefined)
|
|
239
|
+
rmSync(path, { force: true });
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
return binding;
|
|
243
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ForeignHubBinding } from './foreign-hub.js';
|
|
2
|
+
/** Called by the Hub host at mount, independently of any agent/tool use. */
|
|
3
|
+
export declare function refreshHermesNativeEndpoint(endpoint: () => Promise<string | undefined>, nativeHome: string): Promise<void>;
|
|
4
|
+
export declare function prepareHermesNativePlugin(binding: Pick<ForeignHubBinding, 'transport'>, nativeHome: string, enable: (key: string) => Promise<unknown>): Promise<string | undefined>;
|