super-dsh 0.3.0-e → 0.3.0-g

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 (41) hide show
  1. package/README.md +13 -55
  2. package/client/index.js +141 -10
  3. package/cordis.patch.yml +6 -3
  4. package/package.json +12 -5
  5. package/remote/file-worker.js +341 -0
  6. package/remote/plugin.d.ts +523 -0
  7. package/remote/plugin.js +4979 -0
  8. package/remote/provenance.json +64 -0
  9. package/superd-hub/dist/a2a-row.d.ts +18 -2
  10. package/superd-hub/dist/a2a-row.js +11 -69
  11. package/superd-hub/dist/bundle-setup.d.ts +16 -6
  12. package/superd-hub/dist/bundle-setup.js +84 -148
  13. package/superd-hub/dist/client/companion-api.d.ts +5 -0
  14. package/superd-hub/dist/client/companion-api.js +30 -0
  15. package/superd-hub/dist/companion-contract.d.ts +9 -0
  16. package/superd-hub/dist/companion-contract.js +1 -0
  17. package/superd-hub/dist/companions.d.ts +30 -0
  18. package/superd-hub/dist/companions.js +209 -0
  19. package/superd-hub/dist/exec-pty-framing.d.ts +3 -1
  20. package/superd-hub/dist/exec-pty-framing.js +12 -4
  21. package/superd-hub/dist/exec-pty-session.d.ts +2 -0
  22. package/superd-hub/dist/exec-pty-session.js +7 -13
  23. package/superd-hub/dist/exec.d.ts +5 -2
  24. package/superd-hub/dist/exec.js +34 -54
  25. package/superd-hub/dist/fusion-transport.js +1 -0
  26. package/superd-hub/dist/gateway.js +24 -11
  27. package/superd-hub/dist/hub-tool.d.ts +3 -0
  28. package/superd-hub/dist/hub-tool.js +13 -5
  29. package/superd-hub/dist/remote-file-rpc.d.ts +20 -0
  30. package/superd-hub/dist/remote-file-rpc.js +574 -0
  31. package/superd-hub/dist/remote-job-client.d.ts +3 -0
  32. package/superd-hub/dist/remote-job-client.js +197 -0
  33. package/superd-hub/dist/remote-job-rpc.d.ts +21 -0
  34. package/superd-hub/dist/remote-job-rpc.js +155 -0
  35. package/superd-hub/dist/remote-provider.d.ts +13 -0
  36. package/superd-hub/dist/remote-provider.js +151 -0
  37. package/superd-hub/dist/remote-row.d.ts +6 -0
  38. package/superd-hub/dist/remote-row.js +13 -0
  39. package/superd-hub/lib/client/index.js +141 -10
  40. package/superd-hub/package.json +1 -2
  41. package/superd-hub/scripts/register-companion.mjs +0 -100
@@ -0,0 +1,197 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ByteTailRing, readUtf8Tail } from '../../remote/plugin.js';
3
+ import { REMOTE_JOB_PATH, TASK_LEASE_MS, TASK_RENEW_STEP_MS, TASK_CLEANUP_MS, TASK_CONTROL_TIMEOUT_MS, TASK_LEASE_PROTOCOL } from './remote-job-rpc.js';
4
+ /** Event-driven output; the only periodic operation renews this task's bounded resource lease. */
5
+ export function startRemoteProcess(origin, cookie, input, owner) {
6
+ const id = randomUUID(), started = performance.now();
7
+ const rings = { stdout: new ByteTailRing(), stderr: new ByteTailRing() };
8
+ const offsets = { stdout: 0, stderr: 0 };
9
+ const stats = {};
10
+ const streamAbort = new AbortController();
11
+ let diagnostic, cancelled = false, finished = false, stopping = false;
12
+ let lease, cleanupDeadline = 0, nextRenewAt = 0;
13
+ let renewTimer, renewal;
14
+ let stopWork, result;
15
+ let prepared, acknowledge;
16
+ const preparation = new Promise(resolve => { prepared = resolve; });
17
+ const cleaned = new Promise(resolve => { acknowledge = resolve; });
18
+ let acknowledged = false, startDispatched = false, outputComplete = false;
19
+ const confirm = () => { acknowledged = true; acknowledge(); };
20
+ const note = (error) => { const text = error instanceof Error ? error.message : String(error); diagnostic = diagnostic ? `${diagnostic}; ${text}` : text; };
21
+ const headers = { 'content-type': 'application/json', cookie };
22
+ // Header/control time bounds stop new ownership extension; they never prove process cleanup.
23
+ const request = async (body, controller, consumeJson, timeoutMs = TASK_CONTROL_TIMEOUT_MS) => {
24
+ const timer = setTimeout(() => controller.abort(new Error('remote task control timeout')), timeoutMs);
25
+ try {
26
+ const response = await fetch(`${origin}${REMOTE_JOB_PATH}`, { method: 'POST', headers, body: JSON.stringify(body), signal: controller.signal });
27
+ if (!response.ok)
28
+ throw new Error(`remote task: ${response.status} ${await response.text()}`);
29
+ return consumeJson ? await response.json() : response;
30
+ }
31
+ finally {
32
+ clearTimeout(timer);
33
+ }
34
+ };
35
+ const control = (op, extra = {}) => request({ op, owner, id, ...extra }, new AbortController(), true, op === 'cancel' ? TASK_CLEANUP_MS + TASK_CONTROL_TIMEOUT_MS : TASK_CONTROL_TIMEOUT_MS);
36
+ const parseLease = (value) => {
37
+ const l = value?.lease;
38
+ if (!l || l.protocol !== TASK_LEASE_PROTOCOL || l.leaseMs !== TASK_LEASE_MS || l.renewStepMs !== TASK_RENEW_STEP_MS || l.cleanupMs !== TASK_CLEANUP_MS || typeof l.token !== 'string')
39
+ throw new Error('remote task: bounded lease protocol unsupported');
40
+ return l;
41
+ };
42
+ // Full cancel snapshots and incremental stream frames share absolute byte coordinates.
43
+ // Append only the unseen suffix; stale/overlapping frames cannot rewind or duplicate output.
44
+ const mergeOutput = (channel, read) => {
45
+ if (read.nextOffset < offsets[channel])
46
+ return;
47
+ const delta = readUtf8Tail(Buffer.from(read.text), read.nextOffset, offsets[channel]);
48
+ if (delta.lossy)
49
+ rings[channel] = new ByteTailRing();
50
+ rings[channel].push(Buffer.from(delta.text));
51
+ offsets[channel] = delta.nextOffset;
52
+ stats[channel] = read.stats;
53
+ if (read.diagnostic)
54
+ diagnostic = read.diagnostic;
55
+ };
56
+ const stopRenewing = () => { stopping = true; clearTimeout(renewTimer); };
57
+ const waitForCleanup = async () => {
58
+ if (acknowledged || !lease)
59
+ return;
60
+ // Monotonic upper bound: prepare receipt + initial lease + every dispatched renewal step
61
+ // + canonical group cleanup. In-flight renewals were accounted BEFORE sending, so loss is safe.
62
+ let timer;
63
+ await Promise.race([cleaned, new Promise(resolve => { timer = setTimeout(resolve, Math.max(0, cleanupDeadline - performance.now())); })]);
64
+ clearTimeout(timer);
65
+ if (!acknowledged)
66
+ note('remote task lease expired; receiver cleanup bound elapsed');
67
+ };
68
+ const stop = () => {
69
+ stopRenewing();
70
+ return stopWork ??= (async () => {
71
+ await preparation;
72
+ if (!lease || acknowledged)
73
+ return;
74
+ try {
75
+ const response = await control('cancel');
76
+ if (response.done !== true)
77
+ throw new Error('remote task: invalid cleanup acknowledgement');
78
+ if (response.snapshot) {
79
+ mergeOutput('stdout', response.snapshot.stdout);
80
+ mergeOutput('stderr', response.snapshot.stderr);
81
+ result = response.snapshot.result;
82
+ outputComplete = true;
83
+ }
84
+ else if (response.notStarted)
85
+ outputComplete = true;
86
+ confirm();
87
+ }
88
+ catch (error) {
89
+ note(error);
90
+ }
91
+ // Closing the reader helps a reachable receiver observe disconnection. The lease/ack barrier,
92
+ // NOT this abort, determines completion even if the receiver has not observed that close.
93
+ streamAbort.abort();
94
+ await waitForCleanup();
95
+ })();
96
+ };
97
+ const scheduleRenewal = () => {
98
+ if (stopping || finished)
99
+ return;
100
+ renewTimer = setTimeout(() => {
101
+ if (stopping || finished || !lease)
102
+ return;
103
+ cleanupDeadline += TASK_RENEW_STEP_MS; // include possibly-applied renewal even when its response is lost
104
+ const token = lease.token;
105
+ renewal = (async () => {
106
+ try {
107
+ lease = parseLease(await control('renew', { token }));
108
+ }
109
+ catch (error) {
110
+ if (!stopping && !acknowledged) {
111
+ note(error);
112
+ void stop();
113
+ }
114
+ }
115
+ finally {
116
+ nextRenewAt += TASK_RENEW_STEP_MS;
117
+ scheduleRenewal();
118
+ }
119
+ })();
120
+ }, Math.max(0, nextRenewAt - performance.now()));
121
+ };
122
+ const done = (async () => {
123
+ try {
124
+ lease = parseLease(await control('prepare'));
125
+ cleanupDeadline = performance.now() + TASK_LEASE_MS + TASK_CLEANUP_MS;
126
+ nextRenewAt = performance.now() + TASK_RENEW_STEP_MS;
127
+ prepared();
128
+ if (cancelled) {
129
+ await stop();
130
+ return { exit: null, timedOut: false, cancelled: true, durationMs: Math.round(performance.now() - started) };
131
+ }
132
+ // No start may be admitted without the bounded token received above. Delayed starts cannot
133
+ // outlive that token or resurrect a cancelled/expired record.
134
+ startDispatched = true;
135
+ const response = await request({ op: 'start', owner, id, token: lease.token, input: { cmd: input.cmd, stdin: input.stdin, timeoutSec: input.timeout } }, streamAbort, false);
136
+ scheduleRenewal();
137
+ if (!response.body)
138
+ throw new Error('remote task: missing output stream');
139
+ const reader = response.body.getReader(), decoder = new TextDecoder();
140
+ let pending = '';
141
+ try {
142
+ for (;;) {
143
+ const chunk = await reader.read();
144
+ if (chunk.done)
145
+ break;
146
+ pending += decoder.decode(chunk.value, { stream: true });
147
+ let newline;
148
+ while ((newline = pending.indexOf('\n')) >= 0) {
149
+ const frame = JSON.parse(pending.slice(0, newline));
150
+ pending = pending.slice(newline + 1);
151
+ mergeOutput('stdout', frame.stdout);
152
+ mergeOutput('stderr', frame.stderr);
153
+ if (frame.done) {
154
+ result = frame.done;
155
+ outputComplete = true;
156
+ stopRenewing();
157
+ confirm();
158
+ }
159
+ }
160
+ }
161
+ }
162
+ finally {
163
+ await reader.cancel().catch(() => { });
164
+ reader.releaseLock();
165
+ }
166
+ if (!result)
167
+ throw new Error('remote task stream ended before cleanup acknowledgement');
168
+ await stopWork;
169
+ return result;
170
+ }
171
+ catch (error) {
172
+ if (!stopping)
173
+ note(error);
174
+ prepared(); // A failed prepare never spawned anything; any successful prepare is bounded above.
175
+ await stop();
176
+ return result ?? { exit: null, timedOut: false, cancelled, durationMs: Math.round(performance.now() - started) };
177
+ }
178
+ finally {
179
+ finished = true;
180
+ stopRenewing();
181
+ prepared();
182
+ await renewal; // no in-flight transport resource or lease extension survives done
183
+ if (startDispatched && !outputComplete)
184
+ note('remote task final output unavailable; returned output and counters may be incomplete');
185
+ }
186
+ })();
187
+ return {
188
+ cancel() { if (cancelled || finished)
189
+ return; cancelled = true; void stop(); }, done,
190
+ read(channel, fromByte) {
191
+ const read = readUtf8Tail(rings[channel].tail(), offsets[channel], fromByte);
192
+ const incomplete = finished && startDispatched && !outputComplete;
193
+ const metric = incomplete ? undefined : stats[channel];
194
+ return { ...read, lossy: read.lossy || incomplete, ...(metric ? { stats: { ...metric, returned: Buffer.byteLength(read.text) } } : {}), ...(channel === 'stderr' && diagnostic ? { diagnostic } : {}) };
195
+ },
196
+ };
197
+ }
@@ -0,0 +1,21 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type RemoteProcess } from '../../remote/plugin.js';
3
+ export declare const REMOTE_JOB_PATH = "/api/superd/exec/task";
4
+ export declare const TASK_LEASE_MS = 6000;
5
+ export declare const TASK_RENEW_STEP_MS = 2000;
6
+ export declare const TASK_CLEANUP_MS: number;
7
+ export declare const TASK_CONTROL_TIMEOUT_MS = 1500;
8
+ export declare const TASK_LEASE_PROTOCOL = 1;
9
+ type Result = Awaited<RemoteProcess['done']>;
10
+ export interface RemoteTaskSnapshot {
11
+ stdout: ReturnType<RemoteProcess['read']>;
12
+ stderr: ReturnType<RemoteProcess['read']>;
13
+ result: Result;
14
+ }
15
+ export declare function createRemoteJobExecutor(): {
16
+ handle(request: Request): Promise<Response>;
17
+ dispose(): Promise<void>;
18
+ };
19
+ /** Official connection fetch bridge supplies disconnect AbortSignal and auth/trust fence. */
20
+ export declare function registerRemoteJobRpc(ctx: Context): void;
21
+ export {};
@@ -0,0 +1,155 @@
1
+ /** Owned execution resources, not a model job registry. Identity/lifecycle remain in native ctx.jobs. */
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { startOneShot, PROCESS_CLEANUP_BOUND_MS } from '../../remote/plugin.js';
4
+ import { parseOneShotExecBody } from './exec.js';
5
+ export const REMOTE_JOB_PATH = '/api/superd/exec/task';
6
+ // Receiver lease deadlines use monotonic time. Renewal adds a fixed step to the previous deadline,
7
+ // never to request arrival: even a delayed in-flight request has a client-accountable upper bound.
8
+ export const TASK_LEASE_MS = 6000;
9
+ export const TASK_RENEW_STEP_MS = 2000;
10
+ // Canonical process cleanup: TERM, 2000ms grace, KILL, 100ms settle. Include scheduling margin.
11
+ export const TASK_CLEANUP_MS = PROCESS_CLEANUP_BOUND_MS + 200;
12
+ export const TASK_CONTROL_TIMEOUT_MS = 1500;
13
+ export const TASK_LEASE_PROTOCOL = 1;
14
+ const json = (status, body) => Response.json(body, { status });
15
+ export function createRemoteJobExecutor() {
16
+ const active = new Map();
17
+ let disposed = false;
18
+ const stop = async (key, lease, remove = true) => {
19
+ lease.stopping = true;
20
+ if (remove)
21
+ clearTimeout(lease.timer);
22
+ lease.process?.cancel('remote task ownership released');
23
+ await lease.process?.done;
24
+ if (remove && active.get(key) === lease)
25
+ active.delete(key);
26
+ };
27
+ const arm = (key, lease) => {
28
+ clearTimeout(lease.timer);
29
+ lease.timer = setTimeout(() => { void stop(key, lease); }, Math.max(0, lease.expires - performance.now()));
30
+ };
31
+ const metadata = (lease) => ({ protocol: TASK_LEASE_PROTOCOL, token: lease.token, leaseMs: TASK_LEASE_MS, renewStepMs: TASK_RENEW_STEP_MS, cleanupMs: TASK_CLEANUP_MS });
32
+ return {
33
+ async handle(request) {
34
+ if (disposed)
35
+ return json(503, { error: 'remote executor disposed' });
36
+ if (request.method !== 'POST')
37
+ return json(405, { error: 'use POST' });
38
+ let body;
39
+ try {
40
+ body = await request.json();
41
+ }
42
+ catch {
43
+ return json(400, { error: 'invalid JSON' });
44
+ }
45
+ // Buffered parsing yields. Disposal closes admission permanently before it awaits owned processes.
46
+ if (disposed)
47
+ return json(503, { error: 'remote executor disposed' });
48
+ if (!body || typeof body.owner !== 'string' || typeof body.id !== 'string' || !/^[\w-]{1,128}$/.test(body.owner) || !/^[\w-]{1,128}$/.test(body.id))
49
+ return json(400, { error: 'owner and task id required' });
50
+ const credential = createHash('sha256').update(request.headers.get('cookie') ?? '').digest('hex');
51
+ const key = `${credential}:${body.owner}:${body.id}`;
52
+ if (body.op === 'cancel') {
53
+ const lease = active.get(key);
54
+ if (!lease)
55
+ return json(200, { done: true, outputUnavailable: true }); // no foreign resource existence disclosed
56
+ await stop(key, lease);
57
+ const process = lease.process;
58
+ if (!process)
59
+ return json(200, { done: true, notStarted: true });
60
+ const snapshot = { result: await process.done, stdout: process.read('stdout', 0), stderr: process.read('stderr', 0) };
61
+ return json(200, { done: true, snapshot });
62
+ }
63
+ if (body.op === 'prepare') {
64
+ if (active.has(key))
65
+ return json(409, { error: 'task already prepared' });
66
+ if (request.signal.aborted)
67
+ return json(499, { error: 'request disconnected before preparation' });
68
+ const lease = { token: randomUUID(), expires: performance.now() + TASK_LEASE_MS, stopping: false };
69
+ active.set(key, lease);
70
+ arm(key, lease);
71
+ // No subprocess exists yet. The client must receive this bounded token before it can send start.
72
+ return json(200, { lease: metadata(lease) });
73
+ }
74
+ const lease = active.get(key);
75
+ if (body.op === 'read') {
76
+ if (!lease?.process)
77
+ return json(404, { error: 'unknown owned task' });
78
+ const out = body.stdout ?? 0, err = body.stderr ?? 0;
79
+ if (![out, err].every(n => Number.isSafeInteger(n) && n >= 0))
80
+ return json(400, { error: 'invalid byte cursor' });
81
+ return json(200, { stdout: lease.process.read('stdout', out), stderr: lease.process.read('stderr', err) });
82
+ }
83
+ if (body.op !== 'start' && body.op !== 'renew')
84
+ return json(400, { error: 'unknown task operation' });
85
+ if (!lease || lease.stopping || lease.token !== body.token || performance.now() >= lease.expires)
86
+ return json(409, { error: 'task lease absent, expired or superseded; nothing started or renewed' });
87
+ if (body.op === 'renew') {
88
+ if (!lease.process)
89
+ return json(409, { error: 'task has not started' });
90
+ lease.expires = Math.min(lease.expires + TASK_RENEW_STEP_MS, performance.now() + TASK_LEASE_MS);
91
+ lease.token = randomUUID();
92
+ arm(key, lease);
93
+ return json(200, { lease: metadata(lease) });
94
+ }
95
+ if (lease.process)
96
+ return json(409, { error: 'task already started' });
97
+ const parsed = parseOneShotExecBody(body.input);
98
+ if (!parsed.ok)
99
+ return json(400, { error: parsed.error });
100
+ if (request.signal.aborted) {
101
+ await stop(key, lease);
102
+ return json(499, { error: 'request disconnected before start' });
103
+ }
104
+ let wake;
105
+ let changed = true, result, streamClosed = false;
106
+ const notify = () => { changed = true; wake?.(); wake = undefined; };
107
+ const input = parsed.value;
108
+ const process = startOneShot(['bash', '-lc', input.cmd], { unit: 'bytes', stdin: input.stdin, cwd: input.cwd, timeoutSec: input.timeoutSec, onOutput: notify });
109
+ lease.process = process;
110
+ const cancel = () => { void stop(key, lease, false); };
111
+ request.signal.addEventListener('abort', cancel, { once: true });
112
+ const detach = () => request.signal.removeEventListener('abort', cancel);
113
+ // Keep the terminal ring available through the existing bounded lease window.
114
+ // Stream delivery is not end-to-end receipt: a proxy may still hold the final frame.
115
+ void process.done.then(value => { result = value; lease.stopping = true; detach(); notify(); });
116
+ let out = 0, err = 0;
117
+ const stream = new ReadableStream({
118
+ async pull(controller) {
119
+ if (streamClosed)
120
+ return;
121
+ if (!changed && !result)
122
+ await new Promise(resolve => { wake = resolve; });
123
+ if (streamClosed)
124
+ return;
125
+ changed = false;
126
+ const stdout = process.read('stdout', out), stderr = process.read('stderr', err);
127
+ out = stdout.nextOffset;
128
+ err = stderr.nextOffset;
129
+ controller.enqueue(Buffer.from(JSON.stringify({ stdout, stderr, ...(result ? { done: result } : {}) }) + '\n'));
130
+ if (result) {
131
+ streamClosed = true;
132
+ controller.close();
133
+ }
134
+ },
135
+ async cancel() { streamClosed = true; notify(); await stop(key, lease, false); detach(); },
136
+ }, { highWaterMark: 0 });
137
+ return new Response(stream, { headers: { 'content-type': 'application/x-ndjson', 'cache-control': 'no-store' } });
138
+ },
139
+ async dispose() {
140
+ disposed = true;
141
+ await Promise.all([...active].map(([key, lease]) => stop(key, lease)));
142
+ },
143
+ };
144
+ }
145
+ /** Official connection fetch bridge supplies disconnect AbortSignal and auth/trust fence. */
146
+ export function registerRemoteJobRpc(ctx) {
147
+ ctx.inject(['connection'], connectionCtx => {
148
+ const connection = Reflect.get(connectionCtx, 'connection');
149
+ if (!connection?.fetch)
150
+ return;
151
+ const executor = createRemoteJobExecutor();
152
+ connectionCtx.effect(() => () => executor.dispose(), 'remote task executor cleanup');
153
+ connectionCtx.effect(() => connection.fetch.register({ path: REMOTE_JOB_PATH, methods: ['POST'], requestBody: 'buffered', fetch: request => executor.handle(request) }), 'remote task authenticated route');
154
+ });
155
+ }
@@ -0,0 +1,13 @@
1
+ /** Hub transport implementation. HTTP/WS/pool mechanics stay in the existing exec modules. */
2
+ import { type HubRemoteTransport } from '../../remote/plugin.js';
3
+ import type { HubToolExecFace } from './hub-tool.js';
4
+ import type { A2aLogFn } from './a2a-log.js';
5
+ /** One shared initiator implementation for legacy tool and the optional remote service. */
6
+ export declare function createHubExec(home: string, log?: A2aLogFn): {
7
+ face: HubToolExecFace;
8
+ releaseOwner(owner: string): Promise<void>;
9
+ dispose(): Promise<void>;
10
+ };
11
+ export declare function createHubRemoteTransport(home: string, log?: A2aLogFn): HubRemoteTransport & {
12
+ dispose(): Promise<void>;
13
+ };
@@ -0,0 +1,151 @@
1
+ import { createRemoteFileEndpoint } from './remote-file-rpc.js';
2
+ import { startRemoteProcess } from './remote-job-client.js';
3
+ import { ensureSelf, listRemotes } from './machines.js';
4
+ import { cookieOf } from './mesh-link.js';
5
+ import { runOneShotExec, postOneShotExec, parseOneShotExecBody } from './exec.js';
6
+ import { PtyPool } from './exec-pty-session.js';
7
+ import { LocalScriptChannel, WsPtyChannel } from './exec-pty-channel.js';
8
+ import { PTY_EXEC_PATH } from './exec-pty-rpc.js';
9
+ /** One shared initiator implementation for legacy tool and the optional remote service. */
10
+ export function createHubExec(home, log) {
11
+ const owners = new Map();
12
+ let closed = false;
13
+ const assertOpen = () => { if (closed)
14
+ throw new Error('Hub transport unavailable — provider has been disposed'); };
15
+ const remote = (machine) => {
16
+ assertOpen();
17
+ const row = listRemotes(home).find(r => r.id === machine);
18
+ if (!row)
19
+ throw new Error(`unknown machine "${machine}" — call roster for remote machines`);
20
+ if (!row.enabled || row.execEnabled === false)
21
+ throw new Error(`exec is disabled for machine "${machine}" on this registry row (enabled/execEnabled=false)`);
22
+ return row;
23
+ };
24
+ const face = {
25
+ selfM4: () => ensureSelf(home, 'local').id,
26
+ run: input => { assertOpen(); return runOneShotExec(input, log); },
27
+ post: async (machine, input) => {
28
+ const row = remote(machine);
29
+ log?.('info', 'exec.remote', { machine, cmd: input.cmd, timeoutSec: input.timeoutSec ?? 120 });
30
+ const result = await postOneShotExec(row.origin, input, { cookie: cookieOf(row.id, home) });
31
+ log?.('info', 'exec.remote.done', { machine, exit: result.exit ?? 'none', durationMs: result.durationMs, outcome: result.outcome });
32
+ return result;
33
+ },
34
+ pty: async (owner, machine, cmd, opts = {}) => {
35
+ assertOpen();
36
+ // Validate on EVERY dispatch, including a warm shell. Pairing may have changed.
37
+ const row = machine === face.selfM4() ? undefined : remote(machine);
38
+ let pool = owners.get(owner);
39
+ if (!pool)
40
+ owners.set(owner, pool = new PtyPool({ idleTtlSec: 600 }));
41
+ const key = `${machine}|${row?.origin ?? 'local'}`;
42
+ const session = pool.getOrCreate(key, () => row
43
+ ? new WsPtyChannel(`${row.origin.replace(/^http/, 'ws')}${PTY_EXEC_PATH}`, { cookie: cookieOf(row.id, home) })
44
+ : new LocalScriptChannel());
45
+ const result = await session.dispatch(cmd, opts);
46
+ log?.('info', 'exec.pty.run', { machine, cmd: cmd.slice(0, 120), exit: result.exit ?? 'none', durationMs: result.durationMs });
47
+ return result;
48
+ },
49
+ };
50
+ return {
51
+ face,
52
+ async releaseOwner(owner) {
53
+ const pool = owners.get(owner);
54
+ if (pool) {
55
+ await pool.disposeAll();
56
+ if (owners.get(owner) === pool)
57
+ owners.delete(owner);
58
+ }
59
+ },
60
+ async dispose() { closed = true; await Promise.all([...owners.values()].map(p => p.disposeAll())); owners.clear(); },
61
+ };
62
+ }
63
+ export function createHubRemoteTransport(home, log) {
64
+ const exec = createHubExec(home, log);
65
+ let closed = false;
66
+ const released = new Set();
67
+ const fileAbort = new AbortController();
68
+ const files = new Set();
69
+ const processes = new Map();
70
+ const pending = new Map();
71
+ const requireRemote = (machine) => {
72
+ if (!machine || machine === exec.face.selfM4())
73
+ throw new Error('Select a remote machine from the Hub roster; local execution is not available through remote');
74
+ };
75
+ const track = async (owner, work) => {
76
+ let set = pending.get(owner);
77
+ if (!set)
78
+ pending.set(owner, set = new Set());
79
+ set.add(work);
80
+ try {
81
+ return await work;
82
+ }
83
+ finally {
84
+ set.delete(work);
85
+ if (!set.size)
86
+ pending.delete(owner);
87
+ }
88
+ };
89
+ return {
90
+ protocol: 1,
91
+ listTargets: () => listRemotes(home).filter(r => r.id !== exec.face.selfM4()).map(r => ({ id: r.id, label: r.name, execEnabled: r.enabled && r.execEnabled !== false })),
92
+ async start(machine, input, owner) {
93
+ requireRemote(machine);
94
+ const parsed = parseOneShotExecBody({ cmd: input.cmd, ...(input.stdin === undefined ? {} : { stdin: input.stdin }), ...(input.timeout === undefined ? {} : { timeoutSec: input.timeout }) });
95
+ if (!parsed.ok)
96
+ throw new Error(parsed.error);
97
+ if (closed || released.has(owner))
98
+ throw new Error('Hub remote owner disposed');
99
+ const row = listRemotes(home).find(r => r.id === machine);
100
+ if (!row)
101
+ throw new Error(`unknown machine "${machine}"`);
102
+ if (!row.enabled || row.execEnabled === false)
103
+ throw new Error(`exec is disabled for machine "${machine}"`);
104
+ const process = startRemoteProcess(row.origin, cookieOf(row.id, home) ?? '', input, owner);
105
+ let set = processes.get(owner);
106
+ if (!set)
107
+ processes.set(owner, set = new Set());
108
+ set.add(process);
109
+ void process.done.then(() => { set.delete(process); if (!set.size)
110
+ processes.delete(owner); });
111
+ return process;
112
+ },
113
+ async pty(machine, cmd, owner, options) {
114
+ requireRemote(machine);
115
+ if (closed || released.has(owner))
116
+ throw new Error('Hub remote owner disposed');
117
+ const { output, ...result } = await track(owner, exec.face.pty(owner, machine, cmd, { timeoutSec: options.timeout, stdin: options.stdin }));
118
+ return { mode: 'pty', target: `hub:${machine}`, ...result, stdout: output };
119
+ },
120
+ fileEndpoint(machine) {
121
+ requireRemote(machine);
122
+ return createRemoteFileEndpoint(() => {
123
+ if (closed)
124
+ throw new Error('Hub remote provider disposed');
125
+ const row = listRemotes(home).find(r => r.id === machine);
126
+ if (!row)
127
+ throw new Error(`unknown machine "${machine}"`);
128
+ if (!row.enabled || row.execEnabled === false)
129
+ throw new Error(`exec is disabled for machine "${machine}"`);
130
+ return { origin: row.origin, cookie: cookieOf(row.id, home) ?? '' };
131
+ }, fileAbort.signal, done => { files.add(done); void done.finally(() => files.delete(done)); });
132
+ },
133
+ async releaseOwner(owner) {
134
+ released.add(owner);
135
+ const owned = [...processes.get(owner) ?? []];
136
+ for (const process of owned)
137
+ process.cancel('owner released');
138
+ await Promise.all([exec.releaseOwner(owner), ...owned.map(p => p.done), ...pending.get(owner) ?? []]);
139
+ },
140
+ async dispose() {
141
+ closed = true;
142
+ fileAbort.abort(new Error('Hub file provider disposed'));
143
+ await Promise.allSettled([...files]);
144
+ const owned = [...processes.values()].flatMap(s => [...s]);
145
+ for (const process of owned)
146
+ process.cancel('provider disposed');
147
+ await exec.dispose();
148
+ await Promise.allSettled([...owned.map(p => p.done), ...[...pending.values()].flatMap(s => [...s])]);
149
+ },
150
+ };
151
+ }
@@ -0,0 +1,6 @@
1
+ export { createRemoteJobExecutor } from './remote-job-rpc.js';
2
+ /** Hub-body capability, deliberately independent of model tool rows. */
3
+ import type { Context } from '@deepseek-ai/cordis';
4
+ export declare function installHubRemoteTransport(ctx: Context, options: {
5
+ home: string;
6
+ }): void;
@@ -0,0 +1,13 @@
1
+ import { registerRemoteJobRpc } from './remote-job-rpc.js';
2
+ export { createRemoteJobExecutor } from './remote-job-rpc.js';
3
+ import { createHubRemoteTransport } from './remote-provider.js';
4
+ import { registerExecRpc } from './mesh-rpc.js';
5
+ import { consoleA2aLog } from './a2a-log.js';
6
+ export function installHubRemoteTransport(ctx, options) {
7
+ const log = consoleA2aLog();
8
+ const transport = createHubRemoteTransport(options.home, log);
9
+ ctx.effect(() => () => transport.dispose(), 'superd-hub: remote transport cleanup');
10
+ ctx.provide('superdRemoteTransport', transport);
11
+ registerExecRpc(ctx, log);
12
+ registerRemoteJobRpc(ctx);
13
+ }