super-dsh 0.1.6 → 0.1.10-e
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 +25 -3
- package/agent-agy/cordis.patch.yml +7 -4
- package/agent-agy/dist/agent.d.ts +12 -11
- package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
- package/agent-agy/dist/agy-auth-gateway.js +233 -0
- package/agent-agy/dist/agy-auth.d.ts +153 -0
- package/agent-agy/dist/agy-auth.js +179 -0
- package/agent-agy/dist/agy-cli-client.d.ts +39 -9
- package/agent-agy/dist/agy-cli-client.js +82 -18
- package/agent-agy/dist/agy-store.d.ts +8 -0
- package/agent-agy/dist/index.d.ts +46 -16
- package/agent-agy/dist/index.js +82 -56
- package/agent-agy/dist/models.d.ts +70 -0
- package/agent-agy/dist/models.js +102 -3
- package/agent-agy/package.json +7 -5
- package/agent-agy-sdk/cordis.patch.yml +58 -0
- package/agent-agy-sdk/dist/adapter.d.ts +15 -0
- package/agent-agy-sdk/dist/adapter.js +42 -0
- package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
- package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
- package/agent-agy-sdk/dist/agent.d.ts +138 -0
- package/agent-agy-sdk/dist/agent.js +494 -0
- package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
- package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
- package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
- package/agent-agy-sdk/dist/agy-sessions.js +107 -0
- package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
- package/agent-agy-sdk/dist/agy-store.js +83 -0
- package/agent-agy-sdk/dist/inbox.d.ts +26 -0
- package/agent-agy-sdk/dist/inbox.js +53 -0
- package/agent-agy-sdk/dist/index.d.ts +93 -0
- package/agent-agy-sdk/dist/index.js +476 -0
- package/agent-agy-sdk/dist/models.d.ts +41 -0
- package/agent-agy-sdk/dist/models.js +74 -0
- package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
- package/agent-agy-sdk/dist/sdk-venv.js +173 -0
- package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
- package/agent-agy-sdk/dist/world-plugin.js +82 -0
- package/agent-agy-sdk/package.json +64 -0
- package/agent-claude/dist/adapter.js +106 -31
- package/agent-claude/dist/agent.js +154 -17
- package/agent-claude/dist/claude-client.js +84 -43
- package/agent-claude/dist/claude-launch.js +101 -0
- package/agent-claude/dist/claude-native-config.js +169 -0
- package/agent-claude/dist/claude-provider-gateway.js +340 -0
- package/agent-claude/dist/claude-providers.js +391 -0
- package/agent-claude/dist/claude-route.js +152 -0
- package/agent-claude/dist/index.js +214 -37
- package/agent-claude/dist/models.js +14 -0
- package/agent-claude/dist/sdk-runtime.js +320 -0
- package/agent-claude/package.json +6 -4
- package/agent-codex/README.md +141 -36
- package/agent-codex/cordis.patch.yml +14 -10
- package/agent-codex/dist/adapter.js +64 -28
- package/agent-codex/dist/agent.js +255 -81
- package/agent-codex/dist/app-server-approvals.js +165 -0
- package/agent-codex/dist/app-server-client.js +723 -0
- package/agent-codex/dist/app-server-events.js +432 -0
- package/agent-codex/dist/app-server-protocol.js +95 -0
- package/agent-codex/dist/app-server-wire.js +299 -0
- package/agent-codex/dist/codex-launch.js +78 -0
- package/agent-codex/dist/codex-native-config.js +392 -0
- package/agent-codex/dist/codex-provider-gateway.js +402 -0
- package/agent-codex/dist/codex-providers.js +376 -0
- package/agent-codex/dist/codex-route.js +86 -0
- package/agent-codex/dist/index.js +256 -115
- package/agent-codex/dist/permission.js +75 -1
- package/agent-codex/package.json +1 -2
- package/agent-codex-sdk/README.md +79 -0
- package/agent-codex-sdk/cordis.patch.yml +81 -0
- package/agent-codex-sdk/dist/adapter.js +91 -0
- package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
- package/agent-codex-sdk/dist/agent.js +1085 -0
- package/agent-codex-sdk/dist/codex-native-config.js +392 -0
- package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
- package/agent-codex-sdk/dist/codex-providers.js +376 -0
- package/agent-codex-sdk/dist/codex-route.js +86 -0
- package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
- package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
- package/agent-codex-sdk/dist/codex-store.js +180 -0
- package/agent-codex-sdk/dist/inbox.js +53 -0
- package/agent-codex-sdk/dist/index.js +739 -0
- package/agent-codex-sdk/dist/permission.js +71 -0
- package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
- package/agent-codex-sdk/dist/session-map.js +167 -0
- package/agent-codex-sdk/dist/world-plugin.js +93 -0
- package/agent-codex-sdk/package.json +79 -0
- package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
- package/agent-omp/README.md +72 -55
- package/agent-omp/dist/agent.js +36 -11
- package/agent-omp/dist/index.js +92 -52
- package/agent-omp/dist/lazy-rpc.js +11 -4
- package/agent-omp/dist/omp-cli.js +114 -97
- package/agent-omp/dist/omp-store.js +12 -4
- package/agent-omp/dist/permission.js +70 -5
- package/agent-omp/dist/rpc-client.js +409 -0
- package/agent-omp/dist/world-plugin.js +36 -3
- package/agent-omp/package.json +2 -6
- package/agent-omp-sdk/README.md +103 -0
- package/agent-omp-sdk/cordis.patch.yml +108 -0
- package/agent-omp-sdk/dist/adapter.d.ts +21 -0
- package/agent-omp-sdk/dist/adapter.js +36 -0
- package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
- package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
- package/agent-omp-sdk/dist/agent.d.ts +64 -0
- package/agent-omp-sdk/dist/agent.js +1178 -0
- package/agent-omp-sdk/dist/discovery.d.ts +57 -0
- package/agent-omp-sdk/dist/discovery.js +116 -0
- package/agent-omp-sdk/dist/inbox.d.ts +28 -0
- package/agent-omp-sdk/dist/inbox.js +53 -0
- package/agent-omp-sdk/dist/index.d.ts +60 -0
- package/agent-omp-sdk/dist/index.js +632 -0
- package/agent-omp-sdk/dist/knobs.d.ts +34 -0
- package/agent-omp-sdk/dist/knobs.js +50 -0
- package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
- package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
- package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
- package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
- package/agent-omp-sdk/dist/mobile-boot.js +38 -0
- package/agent-omp-sdk/dist/models.d.ts +39 -0
- package/agent-omp-sdk/dist/models.js +299 -0
- package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
- package/agent-omp-sdk/dist/omp-cli.js +164 -0
- package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
- package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
- package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
- package/agent-omp-sdk/dist/omp-executable.js +30 -0
- package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
- package/agent-omp-sdk/dist/omp-store.js +526 -0
- package/agent-omp-sdk/dist/pairing.d.ts +7 -0
- package/agent-omp-sdk/dist/pairing.js +45 -0
- package/agent-omp-sdk/dist/permission.d.ts +138 -0
- package/agent-omp-sdk/dist/permission.js +161 -0
- package/agent-omp-sdk/dist/protocol.d.ts +189 -0
- package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
- package/agent-omp-sdk/dist/rpc-types.js +1 -0
- package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
- package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
- package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
- package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
- package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
- package/agent-omp-sdk/dist/sidecar-client.js +197 -0
- package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
- package/agent-omp-sdk/dist/store/adapter.js +47 -0
- package/agent-omp-sdk/dist/store/db.d.ts +70 -0
- package/agent-omp-sdk/dist/store/db.js +210 -0
- package/agent-omp-sdk/dist/store/index.d.ts +23 -0
- package/agent-omp-sdk/dist/store/index.js +59 -0
- package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
- package/agent-omp-sdk/dist/store/reconcile.js +51 -0
- package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
- package/agent-omp-sdk/dist/store/schema.js +40 -0
- package/agent-omp-sdk/dist/transport.d.ts +49 -0
- package/agent-omp-sdk/dist/transport.js +35 -0
- package/agent-omp-sdk/dist/world-home.d.ts +4 -0
- package/agent-omp-sdk/dist/world-home.js +23 -0
- package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
- package/agent-omp-sdk/dist/world-plugin.js +137 -0
- package/agent-omp-sdk/package.json +87 -0
- package/client/index.js +1883 -287
- package/cordis.patch.yml +80 -0
- package/package.json +23 -9
- package/superd-hub/dist/a2a-delivery.d.ts +189 -0
- package/superd-hub/dist/a2a-delivery.js +240 -0
- package/superd-hub/dist/a2a-host.d.ts +76 -0
- package/superd-hub/dist/a2a-host.js +145 -0
- package/superd-hub/dist/a2a-ingest.d.ts +31 -0
- package/superd-hub/dist/a2a-ingest.js +636 -0
- package/superd-hub/dist/a2a-log.d.ts +17 -0
- package/superd-hub/dist/a2a-log.js +42 -0
- package/superd-hub/dist/a2a-row.d.ts +65 -0
- package/superd-hub/dist/a2a-row.js +162 -0
- package/superd-hub/dist/a2a-service.d.ts +357 -0
- package/superd-hub/dist/a2a-service.js +980 -0
- package/superd-hub/dist/a2a-wire.d.ts +220 -0
- package/superd-hub/dist/a2a-wire.js +305 -0
- package/superd-hub/dist/agent-ids.d.ts +10 -0
- package/superd-hub/dist/agent-ids.js +68 -0
- package/superd-hub/dist/agent-roster.d.ts +29 -18
- package/superd-hub/dist/agent-roster.js +54 -15
- package/superd-hub/dist/agent-status.d.ts +119 -0
- package/superd-hub/dist/agent-status.js +386 -0
- package/superd-hub/dist/client/agent-settings.d.ts +210 -0
- package/superd-hub/dist/client/agent-settings.js +300 -0
- package/superd-hub/dist/client/mesh-api.d.ts +134 -0
- package/superd-hub/dist/client/mesh-api.js +219 -0
- package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
- package/superd-hub/dist/client/mesh-groups.js +66 -0
- package/superd-hub/dist/client/roster.d.ts +48 -0
- package/superd-hub/dist/client/roster.js +102 -0
- package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
- package/superd-hub/dist/client/runtime-settings-api.js +137 -0
- package/superd-hub/dist/client/switch.d.ts +165 -0
- package/superd-hub/dist/client/switch.js +295 -0
- package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
- package/superd-hub/dist/exec-pty-channel.js +162 -0
- package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
- package/superd-hub/dist/exec-pty-framing.js +81 -0
- package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
- package/superd-hub/dist/exec-pty-rpc.js +128 -0
- package/superd-hub/dist/exec-pty-session.d.ts +76 -0
- package/superd-hub/dist/exec-pty-session.js +234 -0
- package/superd-hub/dist/exec.d.ts +56 -0
- package/superd-hub/dist/exec.js +225 -0
- package/superd-hub/dist/gateway.d.ts +39 -2
- package/superd-hub/dist/gateway.js +62 -3
- package/superd-hub/dist/hub-tool.d.ts +174 -0
- package/superd-hub/dist/hub-tool.js +464 -0
- package/superd-hub/dist/index.d.ts +15 -1
- package/superd-hub/dist/index.js +16 -0
- package/superd-hub/dist/machines.d.ts +45 -0
- package/superd-hub/dist/machines.js +125 -0
- package/superd-hub/dist/mesh-federate.d.ts +138 -0
- package/superd-hub/dist/mesh-federate.js +223 -0
- package/superd-hub/dist/mesh-gateway.d.ts +67 -0
- package/superd-hub/dist/mesh-gateway.js +408 -0
- package/superd-hub/dist/mesh-link.d.ts +274 -0
- package/superd-hub/dist/mesh-link.js +441 -0
- package/superd-hub/dist/mesh-rpc.d.ts +123 -0
- package/superd-hub/dist/mesh-rpc.js +250 -0
- package/superd-hub/dist/mux-client.d.ts +71 -0
- package/superd-hub/dist/mux-client.js +281 -0
- package/superd-hub/dist/pack-version.d.ts +2 -0
- package/superd-hub/dist/pack-version.js +30 -0
- package/superd-hub/dist/roster.d.ts +6 -0
- package/superd-hub/dist/rpc.d.ts +18 -2
- package/superd-hub/dist/rpc.js +34 -4
- package/superd-hub/dist/spawn-world.js +3 -1
- package/superd-hub/dist/switch-core.d.ts +62 -25
- package/superd-hub/dist/switch-core.js +53 -30
- package/superd-hub/dist/system-head.d.ts +22 -12
- package/superd-hub/dist/system-head.js +100 -84
- package/superd-hub/dist/wire-endpoints.d.ts +264 -0
- package/superd-hub/dist/wire-endpoints.js +180 -0
- package/superd-hub/lib/client/index.js +1883 -287
- package/superd-hub/package.json +11 -2
- package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
- package/agent-agy/dist/agent-preset-projection.js +0 -33
- package/agent-claude/dist/agent-preset-projection.js +0 -30
- package/agent-codex/dist/agent-preset-projection.js +0 -37
- package/agent-codex/dist/app-home.js +0 -65
- package/agent-codex/dist/models.js +0 -152
- package/agent-codex/dist/spike.js +0 -46
- package/agent-hermes/dist/agent-preset-projection.js +0 -34
- package/agent-omp/dist/agent-preset-projection.js +0 -36
- package/agent-omp/dist/app-home.js +0 -86
- package/agent-omp/dist/sidecar-client.js +0 -195
- package/agent-pi/dist/agent-preset-projection.js +0 -29
- package/superd-hub/dist/client-shim.d.ts +0 -28
- package/superd-hub/dist/client-shim.js +0 -251
- package/superd-hub/dist/index-pass.d.ts +0 -22
- package/superd-hub/dist/index-pass.js +0 -47
- package/superd-hub/dist/labels.d.ts +0 -45
- package/superd-hub/dist/labels.js +0 -83
- /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
- /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
- /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
- /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
- /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// PTY session machinery — PORTED from better-dsh src/remote/pty-session.ts
|
|
2
|
+
// (spec §8.2 / D12 "原样照抄"), with the ONE sanctioned adaptation: the
|
|
3
|
+
// subprocess spawn is replaced by the PtyChannel seam (exec-pty-channel.ts) —
|
|
4
|
+
// every state transition (init nonce handshake, FIFO dispatch, P8 two-stage
|
|
5
|
+
// interrupt \x03 + injected 130-marker, 3s grace, group-kill fallback, idle
|
|
6
|
+
// TTL, OUTPUT_HARD_CAP with surrogate-pair-safe slicing, I1 waiter hygiene)
|
|
7
|
+
// is the ported logic, unchanged.
|
|
8
|
+
import { buildInitCommand, createNonceFrameParser, genNonce, wrapPtyCommand } from './exec-pty-framing.js';
|
|
9
|
+
/** spec §五.2 逐字。 */
|
|
10
|
+
export const RECONNECT_NOTICE = '[remote: session reconnected to fresh shell; cwd reset to default]';
|
|
11
|
+
const DEFAULT_INIT_TIMEOUT_SEC = 30;
|
|
12
|
+
const INTERRUPT_GRACE_MS = 3_000;
|
|
13
|
+
/** pty 收集内存安全帽(Ruling P15,与 oneshot HARD_CAP 对齐):模型面只见尾窗,保留尾部。 */
|
|
14
|
+
const OUTPUT_HARD_CAP = 4_000_000;
|
|
15
|
+
export class PtySession {
|
|
16
|
+
opts;
|
|
17
|
+
state = 'cold';
|
|
18
|
+
startPromise = null;
|
|
19
|
+
queue = Promise.resolve();
|
|
20
|
+
idleTimer = null;
|
|
21
|
+
onChunk = null;
|
|
22
|
+
deathWaiters = [];
|
|
23
|
+
inFlight = 0;
|
|
24
|
+
lastSettleAt = 0;
|
|
25
|
+
constructor(opts) {
|
|
26
|
+
this.opts = opts;
|
|
27
|
+
this.opts.channel.onDead(() => this.markDead());
|
|
28
|
+
}
|
|
29
|
+
get sessionState() { return this.state; }
|
|
30
|
+
/** status 面快照:state + busy(有命令在跑)+ idleMs(上次 settle 至今)。 */
|
|
31
|
+
get snapshot() {
|
|
32
|
+
return { state: this.state, busy: this.inFlight > 0, idleMs: this.lastSettleAt > 0 ? Date.now() - this.lastSettleAt : null };
|
|
33
|
+
}
|
|
34
|
+
/** 同一会话命令严格串行(FIFO);死亡/冷态先重启。 */
|
|
35
|
+
dispatch(cmd, dOpts = {}) {
|
|
36
|
+
const attempt = async () => {
|
|
37
|
+
const started = Date.now();
|
|
38
|
+
const reconnected = this.state === 'dead';
|
|
39
|
+
if (this.state !== 'ready')
|
|
40
|
+
await this.start();
|
|
41
|
+
this.inFlight++;
|
|
42
|
+
return await new Promise((resolve) => {
|
|
43
|
+
const nonce = genNonce();
|
|
44
|
+
let output = '';
|
|
45
|
+
let interrupted = false;
|
|
46
|
+
let settled = false;
|
|
47
|
+
let interruptTimer;
|
|
48
|
+
let killTimer;
|
|
49
|
+
const deathWaiter = () => finish({ output, exit: null, cwd: null, timedOut: interrupted });
|
|
50
|
+
const cleanup = () => {
|
|
51
|
+
this.onChunk = null;
|
|
52
|
+
// I1(终审):已结算 dispatch 的死亡等待必须摘除——否则闭包钉住整段 output,
|
|
53
|
+
// 长命会话每调用累积一个 waiter(init 路径的 filter 同款形态)
|
|
54
|
+
this.deathWaiters = this.deathWaiters.filter((w) => w !== deathWaiter);
|
|
55
|
+
if (interruptTimer !== undefined)
|
|
56
|
+
clearTimeout(interruptTimer);
|
|
57
|
+
if (killTimer !== undefined)
|
|
58
|
+
clearTimeout(killTimer);
|
|
59
|
+
};
|
|
60
|
+
const finish = (r) => {
|
|
61
|
+
if (settled)
|
|
62
|
+
return;
|
|
63
|
+
settled = true;
|
|
64
|
+
cleanup();
|
|
65
|
+
this.inFlight--;
|
|
66
|
+
this.lastSettleAt = Date.now();
|
|
67
|
+
this.resetIdleTimer();
|
|
68
|
+
resolve({ ...r, reconnected, durationMs: Date.now() - started });
|
|
69
|
+
};
|
|
70
|
+
const parser = createNonceFrameParser(nonce, {
|
|
71
|
+
onOutput: (t) => {
|
|
72
|
+
output += t;
|
|
73
|
+
// C1(终审):内存安全帽——无帽时一条 cat /dev/urandom 能在 120s 看门狗内
|
|
74
|
+
// 吃垮宿主 daemon。帽上切片避开代理对(首字符为低位代理则让出一字符)。
|
|
75
|
+
if (output.length > OUTPUT_HARD_CAP) {
|
|
76
|
+
output = output.slice(output.length - OUTPUT_HARD_CAP);
|
|
77
|
+
if (output.charCodeAt(0) >= 0xdc00 && output.charCodeAt(0) <= 0xdfff)
|
|
78
|
+
output = output.slice(1);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
onFrame: ({ exit, cwd }) => finish({ output, exit, cwd, timedOut: interrupted }),
|
|
82
|
+
});
|
|
83
|
+
this.onChunk = (c) => parser.feed(c);
|
|
84
|
+
this.write(wrapPtyCommand(cmd, nonce));
|
|
85
|
+
if (dOpts.stdin !== undefined)
|
|
86
|
+
this.write(dOpts.stdin); // Ruling 11: REPL 式紧随
|
|
87
|
+
interruptTimer = setTimeout(() => {
|
|
88
|
+
interrupted = true;
|
|
89
|
+
// P8 两段打断:\x03 中断前台作业后,交互 bash 会放弃当前命令行的剩余部分
|
|
90
|
+
// (尾部 marker 不再执行);紧随注入的同 nonce 130-marker 行在 bash 回到
|
|
91
|
+
// 读取态后被执行。无论 bash 弃行还是续行,帧都在宽限内到达且 exit=130,
|
|
92
|
+
// 会话存活;若命令 trap 掉 SIGINT,原 marker 先帧、注入行帧后字节被丢弃。
|
|
93
|
+
this.write('\x03');
|
|
94
|
+
this.write(`printf '\\033]133;D;${nonce};130;%s\\007' "$PWD"\n`);
|
|
95
|
+
killTimer = setTimeout(() => {
|
|
96
|
+
// Ruling 8 的 kill 兜底:宽限无帧 = 会话不可恢复,杀整组后收尸
|
|
97
|
+
// (否则 trap-INT 的卡死进程永生,重连后旧组再不可达)
|
|
98
|
+
this.opts.channel.kill();
|
|
99
|
+
this.markDead();
|
|
100
|
+
finish({ output, exit: null, cwd: null, timedOut: true });
|
|
101
|
+
}, INTERRUPT_GRACE_MS);
|
|
102
|
+
}, dOpts.timeoutSec === undefined ? 120_000 : dOpts.timeoutSec * 1_000);
|
|
103
|
+
this.deathWaiters.push(deathWaiter);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const run = this.queue.then(attempt, attempt);
|
|
107
|
+
this.queue = run.then(() => undefined, () => undefined);
|
|
108
|
+
return run;
|
|
109
|
+
}
|
|
110
|
+
/** TTL / row teardown:尽力 exit,1s 后杀组,出池。 */
|
|
111
|
+
async dispose() {
|
|
112
|
+
if (this.state === 'disposed')
|
|
113
|
+
return;
|
|
114
|
+
this.clearIdleTimer();
|
|
115
|
+
this.state = 'disposed';
|
|
116
|
+
this.startPromise = null;
|
|
117
|
+
this.opts.channel.close();
|
|
118
|
+
this.opts.onDead?.();
|
|
119
|
+
}
|
|
120
|
+
write(s) {
|
|
121
|
+
this.opts.channel.write(s);
|
|
122
|
+
}
|
|
123
|
+
async start() {
|
|
124
|
+
if (this.state === 'disposed')
|
|
125
|
+
throw new Error(`[E_SESSION_DISPOSED] remote pty session '${this.opts.key}' was disposed — the pool replaces disposed sessions; this reference is stale`);
|
|
126
|
+
if (this.startPromise === null) {
|
|
127
|
+
this.state = 'starting';
|
|
128
|
+
this.startPromise = this.doStart().catch((err) => {
|
|
129
|
+
this.startPromise = null;
|
|
130
|
+
this.markDead();
|
|
131
|
+
throw err;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return this.startPromise;
|
|
135
|
+
}
|
|
136
|
+
async doStart() {
|
|
137
|
+
const initTimeoutSec = this.opts.initTimeoutSec ?? DEFAULT_INIT_TIMEOUT_SEC;
|
|
138
|
+
const initNonce = genNonce();
|
|
139
|
+
await new Promise((resolve, reject) => {
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
this.opts.channel.kill();
|
|
142
|
+
reject(new Error(`[E_SESSION_START] remote pty session '${this.opts.key}' did not initialize in ${initTimeoutSec}s` +
|
|
143
|
+
(this.diag().length > 0 ? `; transport stderr tail: ${this.diag()}` : '')));
|
|
144
|
+
}, initTimeoutSec * 1_000);
|
|
145
|
+
const waiter = () => {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
reject(new Error(`[E_SESSION_DIED] remote pty session '${this.opts.key}' died before initializing` +
|
|
148
|
+
(this.diag().length > 0 ? `; transport stderr tail: ${this.diag()}` : '')));
|
|
149
|
+
};
|
|
150
|
+
this.deathWaiters.push(waiter);
|
|
151
|
+
const parser = createNonceFrameParser(initNonce, {
|
|
152
|
+
onFrame: () => {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
this.deathWaiters = this.deathWaiters.filter((w) => w !== waiter);
|
|
155
|
+
if (this.state === 'disposed') {
|
|
156
|
+
// dispose 竞态 init:不得复活已终态会话(否则重挂 idle timer + 幽灵 ready)
|
|
157
|
+
reject(new Error(`[E_SESSION_DISPOSED] remote pty session '${this.opts.key}' disposed during init`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.state = 'ready';
|
|
161
|
+
this.resetIdleTimer();
|
|
162
|
+
resolve();
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
this.onChunk = (c) => parser.feed(c);
|
|
166
|
+
this.opts.channel.open((chunk) => this.onChunk?.(chunk))
|
|
167
|
+
.catch((err) => {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
this.deathWaiters = this.deathWaiters.filter((w) => w !== waiter);
|
|
170
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
171
|
+
});
|
|
172
|
+
this.write(buildInitCommand(initNonce));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
diag() { return this.opts.channel.diag; }
|
|
176
|
+
markDead() {
|
|
177
|
+
if (this.state === 'disposed') {
|
|
178
|
+
// dispose 是终态:不再翻转 state,但在飞 dispatch 的死亡等待仍需立即结算
|
|
179
|
+
// (row teardown 不是超时——不得等 123s 看门狗,也不得错标 timedOut)
|
|
180
|
+
const waiters = this.deathWaiters;
|
|
181
|
+
this.deathWaiters = [];
|
|
182
|
+
for (const w of waiters)
|
|
183
|
+
w();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.state = 'dead';
|
|
187
|
+
this.startPromise = null;
|
|
188
|
+
this.clearIdleTimer();
|
|
189
|
+
const waiters = this.deathWaiters;
|
|
190
|
+
this.deathWaiters = [];
|
|
191
|
+
for (const w of waiters)
|
|
192
|
+
w();
|
|
193
|
+
}
|
|
194
|
+
resetIdleTimer() {
|
|
195
|
+
this.clearIdleTimer();
|
|
196
|
+
this.idleTimer = setTimeout(() => void this.dispose(), this.opts.idleTtlSec * 1_000);
|
|
197
|
+
this.idleTimer.unref();
|
|
198
|
+
}
|
|
199
|
+
clearIdleTimer() {
|
|
200
|
+
if (this.idleTimer !== null) {
|
|
201
|
+
clearTimeout(this.idleTimer);
|
|
202
|
+
this.idleTimer = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** 会话池:键 → 会话。意外死亡的会话留在池内(下次 dispatch 带 reconnect 提示)。 */
|
|
207
|
+
export class PtyPool {
|
|
208
|
+
defaults;
|
|
209
|
+
sessions = new Map();
|
|
210
|
+
constructor(defaults) {
|
|
211
|
+
this.defaults = defaults;
|
|
212
|
+
}
|
|
213
|
+
get size() { return this.sessions.size; }
|
|
214
|
+
/** status 面:键上的会话快照(无会话 = undefined → "none — dials on first exec")。 */
|
|
215
|
+
inspect(key) {
|
|
216
|
+
return this.sessions.get(key)?.snapshot;
|
|
217
|
+
}
|
|
218
|
+
/** roster 面:池内全部会话。 */
|
|
219
|
+
list() {
|
|
220
|
+
return [...this.sessions.entries()].map(([key, s]) => ({ key, snapshot: s.snapshot }));
|
|
221
|
+
}
|
|
222
|
+
getOrCreate(key, channelOf) {
|
|
223
|
+
let s = this.sessions.get(key);
|
|
224
|
+
if (s === undefined || s.sessionState === 'disposed') {
|
|
225
|
+
s = new PtySession({ key, channel: channelOf(), ...this.defaults, onDead: () => this.sessions.delete(key) });
|
|
226
|
+
this.sessions.set(key, s);
|
|
227
|
+
}
|
|
228
|
+
return s;
|
|
229
|
+
}
|
|
230
|
+
async disposeAll() {
|
|
231
|
+
await Promise.all([...this.sessions.values()].map((s) => s.dispose()));
|
|
232
|
+
this.sessions.clear();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { A2aLogFn } from './a2a-log.js';
|
|
2
|
+
/** Shared-channel Fetch route path this API owns. */
|
|
3
|
+
export declare const EXEC_API_BASE = "/api/superd/exec";
|
|
4
|
+
export interface OneShotExecInput {
|
|
5
|
+
cmd: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
timeoutSec?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface OneShotExecResult {
|
|
10
|
+
exit: number | null;
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
timedOut: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Why the process ended (0.1.7-e, first-person finding F1/F2: `exit:null`
|
|
18
|
+
* alone conflated spawn failure with signal death):
|
|
19
|
+
* `exited` — normal exit (exit carries the code);
|
|
20
|
+
* `signaled` — died by a signal we did not send (exit null);
|
|
21
|
+
* `timeout` — OUR two-stage kill fired (timedOut true);
|
|
22
|
+
* `spawn-error` — the process never started (bad cwd / ENOENT); the
|
|
23
|
+
* diagnostic rides stderr as `exec: spawn failed — <reason>`.
|
|
24
|
+
*/
|
|
25
|
+
outcome: 'exited' | 'signaled' | 'timeout' | 'spawn-error';
|
|
26
|
+
}
|
|
27
|
+
/** Per-stream tail window returned to the caller (spec §8.1: 30k). */
|
|
28
|
+
export declare const EXEC_TAIL_WINDOW = 30000;
|
|
29
|
+
/** Per-stream hard retention while running (spec §8.1: 4M ring). */
|
|
30
|
+
export declare const EXEC_RING_HARD_CAP: number;
|
|
31
|
+
export declare const EXEC_DEFAULT_TIMEOUT_SEC = 120;
|
|
32
|
+
export declare const EXEC_MAX_TIMEOUT_SEC = 600;
|
|
33
|
+
export type OneShotExecParsed = {
|
|
34
|
+
ok: true;
|
|
35
|
+
value: OneShotExecInput;
|
|
36
|
+
} | {
|
|
37
|
+
ok: false;
|
|
38
|
+
error: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Validate an exec request body (endpoint and tool share the exact rules):
|
|
42
|
+
* non-empty `cmd` (≤4096 chars), optional absolute `cwd`, optional integer
|
|
43
|
+
* `timeoutSec` in [1, 600].
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseOneShotExecBody(body: unknown): OneShotExecParsed;
|
|
46
|
+
/**
|
|
47
|
+
* Execute one command locally: `bash -lc <cmd>`, detached into its own
|
|
48
|
+
* process group so the timeout's group kill cannot leak to the hub process.
|
|
49
|
+
*/
|
|
50
|
+
export declare function runOneShotExec(input: OneShotExecInput, log?: A2aLogFn): Promise<OneShotExecResult>;
|
|
51
|
+
/** Cross-machine wire leg: POST the exec request to a paired hub (cookie rides, a2a envelope pattern). */
|
|
52
|
+
export declare function postOneShotExec(origin: string, input: OneShotExecInput, opts?: {
|
|
53
|
+
cookie?: string;
|
|
54
|
+
fetchImpl?: typeof fetch;
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
}): Promise<OneShotExecResult>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// One-shot remote execution (spec §8.1, P4): `bash -lc <cmd>` exactly once,
|
|
2
|
+
// per-stream tail windows in the response, hard ring retention behind them,
|
|
3
|
+
// two-stage group kill on timeout. This module is the EXECUTING side (used by
|
|
4
|
+
// the /api/superd/exec endpoint) and the WIRE leg (used by the initiator-side
|
|
5
|
+
// tool); it never decides policy — auth is the shared /api fence's, and the
|
|
6
|
+
// initiator-side per-machine knob lives on the caller's registry row.
|
|
7
|
+
//
|
|
8
|
+
// Output discipline (better-dash lesson, spec §8.1): return the LAST 30k of
|
|
9
|
+
// each stream, retain at most 4M per stream while running (the tail of a
|
|
10
|
+
// capped ring is still the tail of the stream), mark `truncated` whenever a
|
|
11
|
+
// stream's total exceeded what is returned. Kill discipline: SIGTERM to the
|
|
12
|
+
// process GROUP, 2s grace, SIGKILL to the group — `bash -lc` children must
|
|
13
|
+
// not outlive their parent's timeout.
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
/** Shared-channel Fetch route path this API owns. */
|
|
16
|
+
export const EXEC_API_BASE = '/api/superd/exec';
|
|
17
|
+
/** Per-stream tail window returned to the caller (spec §8.1: 30k). */
|
|
18
|
+
export const EXEC_TAIL_WINDOW = 30_000;
|
|
19
|
+
/** Per-stream hard retention while running (spec §8.1: 4M ring). */
|
|
20
|
+
export const EXEC_RING_HARD_CAP = 4 * 1024 * 1024;
|
|
21
|
+
export const EXEC_DEFAULT_TIMEOUT_SEC = 120;
|
|
22
|
+
export const EXEC_MAX_TIMEOUT_SEC = 600;
|
|
23
|
+
const CMD_MAX_CHARS = 4096;
|
|
24
|
+
const KILL_GRACE_MS = 2_000;
|
|
25
|
+
/**
|
|
26
|
+
* Validate an exec request body (endpoint and tool share the exact rules):
|
|
27
|
+
* non-empty `cmd` (≤4096 chars), optional absolute `cwd`, optional integer
|
|
28
|
+
* `timeoutSec` in [1, 600].
|
|
29
|
+
*/
|
|
30
|
+
export function parseOneShotExecBody(body) {
|
|
31
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
32
|
+
return { ok: false, error: 'exec: request body must be a JSON object' };
|
|
33
|
+
}
|
|
34
|
+
const raw = body;
|
|
35
|
+
if (typeof raw.cmd !== 'string' || raw.cmd.trim() === '') {
|
|
36
|
+
return { ok: false, error: 'exec: cmd must be a non-empty string' };
|
|
37
|
+
}
|
|
38
|
+
if (raw.cmd.length > CMD_MAX_CHARS) {
|
|
39
|
+
return { ok: false, error: `exec: cmd exceeds ${CMD_MAX_CHARS} characters` };
|
|
40
|
+
}
|
|
41
|
+
if (raw.cwd !== undefined && (typeof raw.cwd !== 'string' || !raw.cwd.startsWith('/') || raw.cwd.includes('\0'))) {
|
|
42
|
+
return { ok: false, error: 'exec: cwd must be an absolute path' };
|
|
43
|
+
}
|
|
44
|
+
if (raw.timeoutSec !== undefined && (typeof raw.timeoutSec !== 'number' || !Number.isInteger(raw.timeoutSec) || raw.timeoutSec < 1 || raw.timeoutSec > EXEC_MAX_TIMEOUT_SEC)) {
|
|
45
|
+
return { ok: false, error: `exec: timeoutSec must be an integer in [1, ${EXEC_MAX_TIMEOUT_SEC}]` };
|
|
46
|
+
}
|
|
47
|
+
const known = new Set(['cmd', 'cwd', 'timeoutSec']);
|
|
48
|
+
for (const key of Object.keys(raw)) {
|
|
49
|
+
if (!known.has(key))
|
|
50
|
+
return { ok: false, error: `exec: unknown field "${key}"` };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
ok: true,
|
|
54
|
+
value: {
|
|
55
|
+
cmd: raw.cmd,
|
|
56
|
+
...(raw.cwd === undefined ? {} : { cwd: raw.cwd }),
|
|
57
|
+
...(raw.timeoutSec === undefined ? {} : { timeoutSec: raw.timeoutSec }),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A capped tail-retaining accumulator: `total` counts every byte ever seen,
|
|
63
|
+
* `ring` keeps at most EXEC_RING_HARD_CAP of the most recent ones.
|
|
64
|
+
*/
|
|
65
|
+
class TailRing {
|
|
66
|
+
total = 0;
|
|
67
|
+
chunks = [];
|
|
68
|
+
retained = 0;
|
|
69
|
+
push(chunk) {
|
|
70
|
+
this.total += chunk.length;
|
|
71
|
+
this.chunks.push(chunk);
|
|
72
|
+
this.retained += chunk.length;
|
|
73
|
+
if (this.retained > EXEC_RING_HARD_CAP) {
|
|
74
|
+
let drop = this.retained - EXEC_RING_HARD_CAP;
|
|
75
|
+
while (drop > 0 && this.chunks.length > 0) {
|
|
76
|
+
const head = this.chunks[0];
|
|
77
|
+
if (head.length <= drop) {
|
|
78
|
+
drop -= head.length;
|
|
79
|
+
this.retained -= head.length;
|
|
80
|
+
this.chunks.shift();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.chunks[0] = head.subarray(drop);
|
|
84
|
+
this.retained -= drop;
|
|
85
|
+
drop = 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
tail(maxBytes) {
|
|
91
|
+
if (this.retained <= maxBytes)
|
|
92
|
+
return Buffer.concat(this.chunks);
|
|
93
|
+
let skip = this.retained - maxBytes;
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const chunk of this.chunks) {
|
|
96
|
+
if (skip >= chunk.length) {
|
|
97
|
+
skip -= chunk.length;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
out.push(skip === 0 ? chunk : chunk.subarray(skip));
|
|
101
|
+
skip = 0;
|
|
102
|
+
}
|
|
103
|
+
return Buffer.concat(out);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Execute one command locally: `bash -lc <cmd>`, detached into its own
|
|
108
|
+
* process group so the timeout's group kill cannot leak to the hub process.
|
|
109
|
+
*/
|
|
110
|
+
export async function runOneShotExec(input, log) {
|
|
111
|
+
const started = Date.now();
|
|
112
|
+
const stdout = new TailRing();
|
|
113
|
+
const stderr = new TailRing();
|
|
114
|
+
const timeoutSec = input.timeoutSec ?? EXEC_DEFAULT_TIMEOUT_SEC;
|
|
115
|
+
let terminationRequested = false;
|
|
116
|
+
const child = spawn('bash', ['-lc', input.cmd], {
|
|
117
|
+
...(input.cwd === undefined ? {} : { cwd: input.cwd }),
|
|
118
|
+
detached: true,
|
|
119
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
120
|
+
});
|
|
121
|
+
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
|
122
|
+
child.stderr.on('data', (chunk) => stderr.push(chunk));
|
|
123
|
+
let graceTimer;
|
|
124
|
+
const killTimer = setTimeout(() => {
|
|
125
|
+
terminationRequested = true;
|
|
126
|
+
try {
|
|
127
|
+
if (child.pid !== undefined)
|
|
128
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
129
|
+
}
|
|
130
|
+
catch { /* already gone */ }
|
|
131
|
+
graceTimer = setTimeout(() => {
|
|
132
|
+
try {
|
|
133
|
+
if (child.pid !== undefined)
|
|
134
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
135
|
+
}
|
|
136
|
+
catch { /* already gone */ }
|
|
137
|
+
}, KILL_GRACE_MS);
|
|
138
|
+
if (graceTimer.unref)
|
|
139
|
+
graceTimer.unref();
|
|
140
|
+
}, timeoutSec * 1000);
|
|
141
|
+
if (killTimer.unref)
|
|
142
|
+
killTimer.unref();
|
|
143
|
+
let spawnError;
|
|
144
|
+
const exit = await new Promise((resolve) => {
|
|
145
|
+
child.once('close', (code) => resolve(code));
|
|
146
|
+
child.once('error', (err) => { spawnError = err.message; resolve(null); });
|
|
147
|
+
});
|
|
148
|
+
clearTimeout(killTimer);
|
|
149
|
+
if (graceTimer !== undefined)
|
|
150
|
+
clearTimeout(graceTimer);
|
|
151
|
+
const outText = stdout.tail(EXEC_TAIL_WINDOW).toString('utf8');
|
|
152
|
+
const errText = spawnError !== undefined
|
|
153
|
+
? `exec: spawn failed — ${spawnError}${input.cwd === undefined ? '' : ` (cwd: ${input.cwd})`}`
|
|
154
|
+
: stderr.tail(EXEC_TAIL_WINDOW).toString('utf8');
|
|
155
|
+
const timedOut = terminationRequested;
|
|
156
|
+
const outcome = spawnError !== undefined
|
|
157
|
+
? 'spawn-error'
|
|
158
|
+
: timedOut
|
|
159
|
+
? 'timeout'
|
|
160
|
+
: exit === null
|
|
161
|
+
? 'signaled'
|
|
162
|
+
: 'exited';
|
|
163
|
+
const result = {
|
|
164
|
+
exit,
|
|
165
|
+
stdout: outText,
|
|
166
|
+
stderr: errText,
|
|
167
|
+
durationMs: Date.now() - started,
|
|
168
|
+
truncated: stdout.total > EXEC_TAIL_WINDOW || stderr.total > EXEC_TAIL_WINDOW,
|
|
169
|
+
timedOut,
|
|
170
|
+
outcome,
|
|
171
|
+
};
|
|
172
|
+
log?.('info', 'exec.run', {
|
|
173
|
+
cmd: input.cmd,
|
|
174
|
+
exit: result.exit ?? 'none',
|
|
175
|
+
durationMs: result.durationMs,
|
|
176
|
+
...(outcome === 'exited' ? {} : { outcome }),
|
|
177
|
+
...(result.timedOut ? { timedOut: true } : {}),
|
|
178
|
+
...(result.truncated ? { truncated: true } : {}),
|
|
179
|
+
});
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
/** Cross-machine wire leg: POST the exec request to a paired hub (cookie rides, a2a envelope pattern). */
|
|
183
|
+
export async function postOneShotExec(origin, input, opts = {}) {
|
|
184
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
185
|
+
const cookie = opts.cookie ?? '';
|
|
186
|
+
const budgetMs = (input.timeoutSec ?? EXEC_DEFAULT_TIMEOUT_SEC) * 1000 + 15_000;
|
|
187
|
+
let res;
|
|
188
|
+
try {
|
|
189
|
+
res = await fetchImpl(`${origin}${EXEC_API_BASE}`, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'content-type': 'application/json', ...(cookie === '' ? {} : { cookie }) },
|
|
192
|
+
body: JSON.stringify(input),
|
|
193
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? budgetMs),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
throw new Error(`exec: peer unreachable — ${err instanceof Error ? err.message : String(err)}`);
|
|
198
|
+
}
|
|
199
|
+
const body = await res.json().catch(() => undefined);
|
|
200
|
+
if (res.status !== 200) {
|
|
201
|
+
const message = typeof body === 'object' && body !== null && typeof body.error === 'string'
|
|
202
|
+
? body.error
|
|
203
|
+
: `exec: peer answered status ${res.status}`;
|
|
204
|
+
throw new Error(message);
|
|
205
|
+
}
|
|
206
|
+
if (typeof body !== 'object' || body === null)
|
|
207
|
+
throw new Error('exec: peer answered a non-object body');
|
|
208
|
+
const raw = body;
|
|
209
|
+
const exit = typeof raw.exit === 'number' || raw.exit === null ? raw.exit : null;
|
|
210
|
+
const timedOut = raw.timedOut === true;
|
|
211
|
+
const parsedOutcome = raw.outcome;
|
|
212
|
+
const outcome = parsedOutcome === 'exited' || parsedOutcome === 'signaled' || parsedOutcome === 'timeout' || parsedOutcome === 'spawn-error'
|
|
213
|
+
? parsedOutcome
|
|
214
|
+
// a pre-0.1.7-e peer carries no outcome — derive the closest honest read
|
|
215
|
+
: timedOut ? 'timeout' : exit === null ? 'signaled' : 'exited';
|
|
216
|
+
return {
|
|
217
|
+
exit,
|
|
218
|
+
stdout: typeof raw.stdout === 'string' ? raw.stdout : '',
|
|
219
|
+
stderr: typeof raw.stderr === 'string' ? raw.stderr : '',
|
|
220
|
+
durationMs: typeof raw.durationMs === 'number' ? raw.durationMs : 0,
|
|
221
|
+
truncated: raw.truncated === true,
|
|
222
|
+
timedOut,
|
|
223
|
+
outcome,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -12,14 +12,36 @@
|
|
|
12
12
|
* per-world mount registers through `carrier.ts` (AW-B tasks 2-9), which calls
|
|
13
13
|
* the addressed world's own `typertGateway` instance directly — again without
|
|
14
14
|
* any shared selection value.
|
|
15
|
+
*
|
|
16
|
+
* Mesh P2 Task 3 adds the federation wiring: mesh-link's machine lifecycle
|
|
17
|
+
* events drive mesh-federate's registrar (pair/reprobe success → converge the
|
|
18
|
+
* remote roster into the registries, probe failure → degrade, removal →
|
|
19
|
+
* defederate), and the `/hub` family's demux gains the federation table ahead
|
|
20
|
+
* of the local target registry (spec §3.2 dual-key: a6 canonical, label
|
|
21
|
+
* aliases compatible).
|
|
15
22
|
*/
|
|
16
23
|
import type { Context } from '@deepseek-ai/cordis';
|
|
24
|
+
import z from '@deepseek-ai/schemastery';
|
|
17
25
|
/** Stable Cordis plugin name. */
|
|
18
26
|
export declare const name = "superd-hub";
|
|
27
|
+
/** Row config schema (mesh P3 Task 5a): the periodic reprobe sweep's knob —
|
|
28
|
+
* seconds between sweeps, default the user mandate's 5 minutes, floored at
|
|
29
|
+
* 60 by the schema AND clamped again in `startReprobeSweep` (a bare-library
|
|
30
|
+
* caller cannot bypass the floor). Patch rows restate the whole config
|
|
31
|
+
* object; schema defaults fill the un-configured one. */
|
|
32
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
33
|
+
reprobeIntervalSec: z<number, number, "defined">;
|
|
34
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
35
|
+
reprobeIntervalSec: z<number, number, "defined">;
|
|
36
|
+
}>>, "plain">;
|
|
37
|
+
/** The row config's TypeScript face (the schema-declared knob). */
|
|
38
|
+
export interface HubConfig {
|
|
39
|
+
reprobeIntervalSec?: number;
|
|
40
|
+
}
|
|
19
41
|
/**
|
|
20
42
|
* The plugin form (row contract): applied when the row mounts.
|
|
21
43
|
*
|
|
22
|
-
*
|
|
44
|
+
* CTX0-owned contributions, none carrying any selection value:
|
|
23
45
|
* - the world inventory control plane (read-only);
|
|
24
46
|
* - the agent roster row baked into ctx0's own index, so the selector lists
|
|
25
47
|
* every mount path the hub currently serves;
|
|
@@ -28,6 +50,21 @@ export declare const name = "superd-hub";
|
|
|
28
50
|
* World trees compose this same row, but their `webServer` is the VIRTUAL
|
|
29
51
|
* per-world translation surface — the `/hub` family belongs to ctx0's
|
|
30
52
|
* real listener only (one family per process, demuxed by `?agent=`).
|
|
53
|
+
* - the mesh federation wiring (mesh P2 Task 3): home resolved HERE at
|
|
54
|
+
* registration time (mesh-rpc stays a pure route module; tests inject a
|
|
55
|
+
* tmpdir home instead); the one-way seam is mesh-link's federation event
|
|
56
|
+
* bus — mesh-federate subscribes, mesh-link never imports it.
|
|
57
|
+
* - the periodic reprobe sweep (mesh P3 Task 5a I1-a, user mandate
|
|
58
|
+
* "状态更新走单向 API + 5 分钟周期轮询"): every `reprobeIntervalSec`
|
|
59
|
+
* (default 300, floor 60) one sweep re-probes every ENABLED remote —
|
|
60
|
+
* success converges federation, failure degrades (both via the existing
|
|
61
|
+
* mesh-link hooks). CTX0-ONLY, gated by the same WorldWebServer check as
|
|
62
|
+
* the `/hub` family: world trees compose this row too (world-mount.ts
|
|
63
|
+
* WORLD_HUB_ROW) and must never start their own sweep against the shared
|
|
64
|
+
* machines registry.
|
|
65
|
+
* @param ctx - host plugin context.
|
|
66
|
+
* @param config - the row config (`reprobeIntervalSec`; schema defaults
|
|
67
|
+
* apply when the row is composed without one).
|
|
31
68
|
*/
|
|
32
|
-
export declare function apply(ctx: Context): void;
|
|
69
|
+
export declare function apply(ctx: Context, config?: HubConfig): void;
|
|
33
70
|
export default apply;
|
|
@@ -1,14 +1,29 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
1
2
|
import { registerSelectorRpc, registryTargets } from './rpc.js';
|
|
3
|
+
import { registerMeshRpc } from './mesh-rpc.js';
|
|
4
|
+
import { startReprobeSweep } from './mesh-link.js';
|
|
5
|
+
import { federatedGatewayOf, startMeshFederation } from './mesh-federate.js';
|
|
6
|
+
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
|
|
2
7
|
import { agentRosterRow } from './agent-roster.js';
|
|
3
8
|
import { getTarget } from './targets.js';
|
|
4
9
|
import { registerHubRoute } from './hub-route.js';
|
|
5
10
|
import { WorldWebServer } from './world-web-server.js';
|
|
11
|
+
import { registerPtyExecutor } from './exec-pty-rpc.js';
|
|
12
|
+
import { consoleA2aLog } from './a2a-log.js';
|
|
6
13
|
/** Stable Cordis plugin name. */
|
|
7
14
|
export const name = 'superd-hub';
|
|
15
|
+
/** Row config schema (mesh P3 Task 5a): the periodic reprobe sweep's knob —
|
|
16
|
+
* seconds between sweeps, default the user mandate's 5 minutes, floored at
|
|
17
|
+
* 60 by the schema AND clamped again in `startReprobeSweep` (a bare-library
|
|
18
|
+
* caller cannot bypass the floor). Patch rows restate the whole config
|
|
19
|
+
* object; schema defaults fill the un-configured one. */
|
|
20
|
+
export const Config = z.object({
|
|
21
|
+
reprobeIntervalSec: z.number().min(60).default(300),
|
|
22
|
+
});
|
|
8
23
|
/**
|
|
9
24
|
* The plugin form (row contract): applied when the row mounts.
|
|
10
25
|
*
|
|
11
|
-
*
|
|
26
|
+
* CTX0-owned contributions, none carrying any selection value:
|
|
12
27
|
* - the world inventory control plane (read-only);
|
|
13
28
|
* - the agent roster row baked into ctx0's own index, so the selector lists
|
|
14
29
|
* every mount path the hub currently serves;
|
|
@@ -17,9 +32,27 @@ export const name = 'superd-hub';
|
|
|
17
32
|
* World trees compose this same row, but their `webServer` is the VIRTUAL
|
|
18
33
|
* per-world translation surface — the `/hub` family belongs to ctx0's
|
|
19
34
|
* real listener only (one family per process, demuxed by `?agent=`).
|
|
35
|
+
* - the mesh federation wiring (mesh P2 Task 3): home resolved HERE at
|
|
36
|
+
* registration time (mesh-rpc stays a pure route module; tests inject a
|
|
37
|
+
* tmpdir home instead); the one-way seam is mesh-link's federation event
|
|
38
|
+
* bus — mesh-federate subscribes, mesh-link never imports it.
|
|
39
|
+
* - the periodic reprobe sweep (mesh P3 Task 5a I1-a, user mandate
|
|
40
|
+
* "状态更新走单向 API + 5 分钟周期轮询"): every `reprobeIntervalSec`
|
|
41
|
+
* (default 300, floor 60) one sweep re-probes every ENABLED remote —
|
|
42
|
+
* success converges federation, failure degrades (both via the existing
|
|
43
|
+
* mesh-link hooks). CTX0-ONLY, gated by the same WorldWebServer check as
|
|
44
|
+
* the `/hub` family: world trees compose this row too (world-mount.ts
|
|
45
|
+
* WORLD_HUB_ROW) and must never start their own sweep against the shared
|
|
46
|
+
* machines registry.
|
|
47
|
+
* @param ctx - host plugin context.
|
|
48
|
+
* @param config - the row config (`reprobeIntervalSec`; schema defaults
|
|
49
|
+
* apply when the row is composed without one).
|
|
20
50
|
*/
|
|
21
|
-
export function apply(ctx) {
|
|
51
|
+
export function apply(ctx, config) {
|
|
22
52
|
registerSelectorRpc(ctx, registryTargets);
|
|
53
|
+
const home = resolveDshHome();
|
|
54
|
+
registerMeshRpc(ctx, { home });
|
|
55
|
+
ctx.effect(() => startMeshFederation({ home }), 'superd-hub: mesh federation wiring');
|
|
23
56
|
const on = ctx.on;
|
|
24
57
|
on?.call(ctx, 'webserver/index-inject', (table) => { table.push(agentRosterRow()); });
|
|
25
58
|
ctx.inject(['webServer', 'connection'], (host) => {
|
|
@@ -32,13 +65,39 @@ export function apply(ctx) {
|
|
|
32
65
|
// table — never a shared selection. The requestRejection face is the
|
|
33
66
|
// ctx0 browser-trust fence: auth first, then Host/Origin — the family is
|
|
34
67
|
// never weaker than the neighbors carrying identical traffic.
|
|
68
|
+
//
|
|
69
|
+
// Demux (mesh P2 Task 3, spec §3.2 dual-key): the mesh FEDERATION table
|
|
70
|
+
// first — an exact a6 match is the canonical spelling and beats label
|
|
71
|
+
// aliases — then the shared local target registry (local runtime keys;
|
|
72
|
+
// federated a6s are registered there too, so the fallback re-arms a
|
|
73
|
+
// registration whose memo was cleared independently). Local label aliases
|
|
74
|
+
// and `?agent=<a6>` therefore both demux; under the astronomically rare
|
|
75
|
+
// a6↔local-key collision the federation wins deterministically (documented
|
|
76
|
+
// in mesh-federate's header) instead of last-write-wins in the registry.
|
|
77
|
+
// I1(a) periodic reprobe sweep — CTX0-ONLY by the same gate as the
|
|
78
|
+
// `/hub` family below (world trees see a WorldWebServer and returned
|
|
79
|
+
// early): the machines registry is ctx0-owned, and a per-world timer
|
|
80
|
+
// would multiply probes against every paired remote. The effect's
|
|
81
|
+
// disposer IS the sweep disposer — the timer dies with the row
|
|
82
|
+
// (unref'd, overlap-skipping, per-machine errors swallowed; mesh-link).
|
|
83
|
+
ctx.effect(() => startReprobeSweep({ home, intervalSec: config?.reprobeIntervalSec, immediate: true }), 'superd-hub: mesh reprobe sweep');
|
|
35
84
|
const dispose = registerHubRoute({
|
|
36
85
|
real: real,
|
|
37
86
|
authorize: (req, res) => connection.authorizeIndex(req, res),
|
|
38
87
|
requestRejection: (req) => connection.requestRejection(req),
|
|
39
|
-
gatewayOf: (label) => getTarget(label)?.gateway,
|
|
88
|
+
gatewayOf: (label) => (federatedGatewayOf(label) ?? getTarget(label)?.gateway),
|
|
40
89
|
});
|
|
41
90
|
ctx.effect(() => dispose, 'superd-hub: /hub route family');
|
|
91
|
+
// P4 §8.2: the PTY executor's upgrade route — same gate as the /hub family
|
|
92
|
+
// (authorizeIndex cookie auth first, then the browser-trust fence). The
|
|
93
|
+
// dumb pump lives in exec-pty-rpc.ts; a2a-row owns the initiator half.
|
|
94
|
+
void registerPtyExecutor(real, {
|
|
95
|
+
authorize: (req, res) => connection.authorizeIndex(req, res),
|
|
96
|
+
requestRejection: (req) => connection.requestRejection(req),
|
|
97
|
+
log: consoleA2aLog(),
|
|
98
|
+
}).then((disposePty) => {
|
|
99
|
+
ctx.effect(() => disposePty, 'superd-hub: PTY executor upgrade route');
|
|
100
|
+
}, () => { });
|
|
42
101
|
});
|
|
43
102
|
}
|
|
44
103
|
export default apply;
|