super-dsh 0.3.2-e → 0.3.3-b
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/agent-agy/dist/agent.d.ts +4 -1
- package/agent-agy/dist/agent.js +15 -1
- package/agent-agy/dist/index.js +5 -8
- package/agent-agy/package.json +6 -6
- package/agent-agy-sdk/dist/agent.d.ts +4 -1
- package/agent-agy-sdk/dist/agent.js +15 -1
- package/agent-agy-sdk/dist/index.js +5 -8
- package/agent-agy-sdk/package.json +5 -5
- package/agent-claude/dist/agent.js +15 -1
- package/agent-claude/package.json +8 -8
- package/agent-codex/dist/agent.js +15 -1
- package/agent-codex/package.json +7 -7
- package/agent-codex-sdk/dist/agent.js +15 -1
- package/agent-codex-sdk/dist/index.js +5 -11
- package/agent-codex-sdk/package.json +7 -7
- package/agent-hermes/dist/agent.js +15 -1
- package/agent-hermes/dist/index.js +5 -8
- package/agent-hermes/package.json +7 -7
- package/agent-omp/dist/agent.js +26 -2
- package/agent-omp/package.json +8 -8
- package/agent-omp-sdk/dist/agent.d.ts +4 -1
- package/agent-omp-sdk/dist/agent.js +15 -1
- package/agent-omp-sdk/package.json +8 -8
- package/agent-pi/dist/agent.js +20 -2
- package/agent-pi/dist/index.js +5 -11
- package/agent-pi/package.json +7 -7
- package/package.json +21 -21
- package/remote/plugin.d.ts +93 -20
- package/remote/plugin.js +921 -452
- package/remote/provenance.json +15 -13
- package/superd-hub/dist/agent-model-face.d.ts +93 -0
- package/superd-hub/dist/agent-model-face.js +113 -0
- package/superd-hub/dist/index.d.ts +1 -0
- package/superd-hub/dist/index.js +1 -0
- package/superd-hub/dist/world-entry.js +30 -0
- package/superd-hub/package.json +14 -10
package/remote/plugin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { Context } from "@deepseek-ai/cordis";
|
|
3
3
|
import { chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import { constants, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { constants, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { dirname, isAbsolute, join, posix, resolve } from "node:path";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
@@ -1466,6 +1466,15 @@ function startBinaryProcess(argv, options = {}) {
|
|
|
1466
1466
|
let diagnostic;
|
|
1467
1467
|
let timedOut = false, finished = false, cancelled = false;
|
|
1468
1468
|
let cleanup;
|
|
1469
|
+
let cancelledNotified = false;
|
|
1470
|
+
const notifyCancel = () => {
|
|
1471
|
+
if (!cancelledNotified) {
|
|
1472
|
+
cancelledNotified = true;
|
|
1473
|
+
try {
|
|
1474
|
+
options.onCancel?.();
|
|
1475
|
+
} catch {}
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1469
1478
|
let timer;
|
|
1470
1479
|
const signal = (sig) => {
|
|
1471
1480
|
if (child.pid === void 0) return false;
|
|
@@ -1505,6 +1514,7 @@ function startBinaryProcess(argv, options = {}) {
|
|
|
1505
1514
|
const cancel = () => {
|
|
1506
1515
|
if (!finished) {
|
|
1507
1516
|
cancelled = true;
|
|
1517
|
+
notifyCancel();
|
|
1508
1518
|
reap();
|
|
1509
1519
|
}
|
|
1510
1520
|
};
|
|
@@ -2503,150 +2513,6 @@ function sshArgv(connection, options = {}) {
|
|
|
2503
2513
|
], options.extraArgs ?? [], options.command);
|
|
2504
2514
|
}
|
|
2505
2515
|
|
|
2506
|
-
//#endregion
|
|
2507
|
-
//#region src/remote/input.ts
|
|
2508
|
-
/** Local carrier validation precedes any target discovery, connection or spawn. */
|
|
2509
|
-
async function prepareRemoteInput(input) {
|
|
2510
|
-
if (input.mode !== void 0 && input.mode !== "oneshot" && input.mode !== "pty") throw new Error("[E_BAD_MODE] remote: mode must be oneshot or pty");
|
|
2511
|
-
if (input.scriptPath !== void 0) {
|
|
2512
|
-
if (input.cmd !== void 0 || input.stdin !== void 0 || input.mode === "pty" || input.spawn !== void 0) throw new Error("[E_SCRIPT_PARAMS] remote: script is oneshot-only and cannot be combined with cmd, stdin, spawn or PTY lanes");
|
|
2513
|
-
if (typeof input.scriptPath !== "string" || !isAbsolute(input.scriptPath) || input.scriptPath.includes("\0")) throw new Error("[E_SCRIPT_PATH] remote: script must be a local absolute path without NUL");
|
|
2514
|
-
}
|
|
2515
|
-
if (!input.target && input.spawn === void 0 && input.ssh === void 0) throw new Error("[E_PARAMS] remote: target is required (spawn / ssh are BYO entry points and may stand alone)");
|
|
2516
|
-
if (input.scriptPath !== void 0) {
|
|
2517
|
-
let stdin;
|
|
2518
|
-
try {
|
|
2519
|
-
stdin = await readFile(input.scriptPath, "utf8");
|
|
2520
|
-
} catch (error) {
|
|
2521
|
-
throw new Error(`[E_SCRIPT_READ] remote: cannot read local script ${JSON.stringify(input.scriptPath)} — ${error instanceof Error ? error.message : String(error)}`);
|
|
2522
|
-
}
|
|
2523
|
-
return {
|
|
2524
|
-
target: input.target,
|
|
2525
|
-
ssh: input.ssh,
|
|
2526
|
-
cmd: "bash -s",
|
|
2527
|
-
stdin,
|
|
2528
|
-
mode: "oneshot",
|
|
2529
|
-
timeout: input.timeout
|
|
2530
|
-
};
|
|
2531
|
-
}
|
|
2532
|
-
return {
|
|
2533
|
-
target: input.target,
|
|
2534
|
-
spawn: input.spawn,
|
|
2535
|
-
ssh: input.ssh,
|
|
2536
|
-
cmd: input.cmd ?? "",
|
|
2537
|
-
stdin: input.stdin,
|
|
2538
|
-
mode: input.mode,
|
|
2539
|
-
timeout: input.timeout
|
|
2540
|
-
};
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
//#endregion
|
|
2544
|
-
//#region src/remote/oneshot.ts
|
|
2545
|
-
async function runOneShot(argv, opts = {}) {
|
|
2546
|
-
const process$1 = startOneShot(argv, opts);
|
|
2547
|
-
const result = await process$1.done;
|
|
2548
|
-
const stdout = process$1.read("stdout", 0), stderr = process$1.read("stderr", 0);
|
|
2549
|
-
return {
|
|
2550
|
-
...result,
|
|
2551
|
-
stdout: stdout.text,
|
|
2552
|
-
stderr: [stderr.text, stderr.diagnostic].filter(Boolean).join("\n"),
|
|
2553
|
-
outputStats: {
|
|
2554
|
-
stdout: stdout.stats,
|
|
2555
|
-
stderr: stderr.stats
|
|
2556
|
-
}
|
|
2557
|
-
};
|
|
2558
|
-
}
|
|
2559
|
-
|
|
2560
|
-
//#endregion
|
|
2561
|
-
//#region src/remote/helper.ts
|
|
2562
|
-
/** Reads helper-bin/MANIFEST.json next to the compiled module (lib/remote in the npm
|
|
2563
|
-
* package, super-dsh/remote in the Super closure — same relative layout). Missing or
|
|
2564
|
-
* malformed assets ⇒ null ⇒ shim-only mode; never throws. */
|
|
2565
|
-
function helperAssets(baseDir) {
|
|
2566
|
-
const dir = baseDir ?? join(dirname(fileURLToPath(import.meta.url)), "helper-bin");
|
|
2567
|
-
try {
|
|
2568
|
-
const manifest = JSON.parse(readFileSync(join(dir, "MANIFEST.json"), "utf8"));
|
|
2569
|
-
if (manifest?.format !== 1 || typeof manifest.ver !== "string" || typeof manifest.protocol !== "number" || !manifest.targets || typeof manifest.targets !== "object") return null;
|
|
2570
|
-
return {
|
|
2571
|
-
dir,
|
|
2572
|
-
manifest
|
|
2573
|
-
};
|
|
2574
|
-
} catch {
|
|
2575
|
-
return null;
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
/** `uname -s`/`uname -m` → shipped target triple. Unknown combos ⇒ null (shim). */
|
|
2579
|
-
function archTriple(sys, machine, shipped) {
|
|
2580
|
-
const s = sys.trim().toLowerCase(), m = machine.trim().toLowerCase();
|
|
2581
|
-
const candidates = s === "linux" ? m === "x86_64" || m === "amd64" ? "x86_64-unknown-linux-musl" : m === "aarch64" || m === "arm64" ? "aarch64-unknown-linux-musl" : null : s === "darwin" ? m === "arm64" || m === "aarch64" ? "aarch64-apple-darwin" : null : null;
|
|
2582
|
-
if (candidates === null) return null;
|
|
2583
|
-
return shipped.has(candidates) ? candidates : null;
|
|
2584
|
-
}
|
|
2585
|
-
function planKey(plan) {
|
|
2586
|
-
switch (plan.kind) {
|
|
2587
|
-
case "ssh": {
|
|
2588
|
-
const c = plan.connection;
|
|
2589
|
-
const host = c?.host ?? plan.host;
|
|
2590
|
-
return `ssh|${c?.user ? c.user + "@" : ""}${host}${c?.port ? ":" + c.port : ""}${c?.proxyJump ? "|" + c.proxyJump : ""}`;
|
|
2591
|
-
}
|
|
2592
|
-
case "docker": return `docker|${plan.container}`;
|
|
2593
|
-
case "incus": return `incus|${plan.container}`;
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
const PROBE_RE = /^SUPERD-PROBE (\S+) (\S+)$/m;
|
|
2597
|
-
const READY_RE = /^SUPERD-READY (\S+)$/m;
|
|
2598
|
-
var HelperRegistry = class {
|
|
2599
|
-
states = /* @__PURE__ */ new Map();
|
|
2600
|
-
constructor(assets, exec, probeArgv, uploadArgv) {
|
|
2601
|
-
this.assets = assets;
|
|
2602
|
-
this.exec = exec;
|
|
2603
|
-
this.probeArgv = probeArgv;
|
|
2604
|
-
this.uploadArgv = uploadArgv;
|
|
2605
|
-
}
|
|
2606
|
-
/** Never throws: any bootstrap failure resolves null (sticky) and the caller stays on the shim. */
|
|
2607
|
-
ensure(plan) {
|
|
2608
|
-
if (plan.kind !== "ssh" && plan.kind !== "docker" && plan.kind !== "incus") return Promise.resolve(null);
|
|
2609
|
-
const key = planKey(plan);
|
|
2610
|
-
let entry = this.states.get(key);
|
|
2611
|
-
if (entry === void 0) {
|
|
2612
|
-
entry = this.bootstrap(plan).catch(() => null);
|
|
2613
|
-
entry.catch(() => {});
|
|
2614
|
-
this.states.set(key, entry);
|
|
2615
|
-
}
|
|
2616
|
-
return entry;
|
|
2617
|
-
}
|
|
2618
|
-
async bootstrap(plan) {
|
|
2619
|
-
const { manifest } = this.assets;
|
|
2620
|
-
const first = await this.exec(this.probeArgv(plan, this.assets), { timeoutSec: 15 });
|
|
2621
|
-
const ready = this.parseReady(first.stdout, manifest.ver, manifest.protocol);
|
|
2622
|
-
if (ready !== null) return ready;
|
|
2623
|
-
const probe = PROBE_RE.exec(first.stdout);
|
|
2624
|
-
if (probe === null || first.exit !== 90 && first.exit !== 0) return null;
|
|
2625
|
-
const triple = archTriple(probe[1], probe[2], new Set(Object.keys(manifest.targets)));
|
|
2626
|
-
if (triple === null) return null;
|
|
2627
|
-
const target = manifest.targets[triple];
|
|
2628
|
-
const binary = await readFile(join(this.assets.dir, triple, target.file));
|
|
2629
|
-
if ((await this.exec(this.uploadArgv(plan, triple, target.file, this.assets), {
|
|
2630
|
-
stdin: binary,
|
|
2631
|
-
timeoutSec: 60
|
|
2632
|
-
})).exit !== 0) return null;
|
|
2633
|
-
const second = await this.exec(this.probeArgv(plan, this.assets), { timeoutSec: 15 });
|
|
2634
|
-
return this.parseReady(second.stdout, manifest.ver, manifest.protocol);
|
|
2635
|
-
}
|
|
2636
|
-
parseReady(stdout, ver, protocol) {
|
|
2637
|
-
const ready = READY_RE.exec(stdout);
|
|
2638
|
-
const banner = /^SUPERD-RUN proto=(\d+) name=superd-run ver=(\S+) target=(\S+)/m.exec(stdout);
|
|
2639
|
-
if (ready === null || banner === null) return null;
|
|
2640
|
-
if (banner[1] !== String(protocol) || banner[2] !== ver) return null;
|
|
2641
|
-
return {
|
|
2642
|
-
binPath: ready[1],
|
|
2643
|
-
triple: banner[3],
|
|
2644
|
-
ver,
|
|
2645
|
-
protocol
|
|
2646
|
-
};
|
|
2647
|
-
}
|
|
2648
|
-
};
|
|
2649
|
-
|
|
2650
2516
|
//#endregion
|
|
2651
2517
|
//#region src/remote/transports.ts
|
|
2652
2518
|
/** POSIX 单引号机械转义(唯一一层转义,spec §三"一层机械转义")。 */
|
|
@@ -2675,7 +2541,10 @@ bash -lc ${shQuote(cmd)}`;
|
|
|
2675
2541
|
wrapped
|
|
2676
2542
|
];
|
|
2677
2543
|
switch (plan.kind) {
|
|
2678
|
-
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
|
|
2544
|
+
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
|
|
2545
|
+
command: `setsid --wait bash -c ${shQuote(wrapped)}`,
|
|
2546
|
+
extraArgs: options.muxArgv
|
|
2547
|
+
});
|
|
2679
2548
|
case "docker": return [
|
|
2680
2549
|
"docker",
|
|
2681
2550
|
"exec",
|
|
@@ -2749,18 +2618,21 @@ function buildHubPtyArgv(endpoint, assets) {
|
|
|
2749
2618
|
/** superd-run helper argv (spec: docs/specs/remote-rescope §5). Probe and upload are single
|
|
2750
2619
|
* `sh -c` hops (busybox/alpine/darwin-safe); run/killgroup over ssh go through exactly one
|
|
2751
2620
|
* remote-shell parse, same discipline as buildOneShotArgv. */
|
|
2752
|
-
function buildHelperProbeArgv(plan, assets) {
|
|
2621
|
+
function buildHelperProbeArgv(plan, assets, muxArgv) {
|
|
2753
2622
|
const { ver, targets } = assets.manifest;
|
|
2754
|
-
return helperShellArgv(plan, `${Object.entries(targets).map(([triple, t]) => `d="$HOME/.cache/superd/run/${triple}-${ver}"; [ -x "$d/${t.file}" ] && { echo "SUPERD-READY $d/${t.file}"; "$d/${t.file}" banner; exit 0; };`).join(" ")} echo "SUPERD-PROBE $(uname -s 2>/dev/null || echo unknown) $(uname -m 2>/dev/null || echo unknown)"; exit 90
|
|
2623
|
+
return helperShellArgv(plan, `${Object.entries(targets).map(([triple, t]) => `d="$HOME/.cache/superd/run/${triple}-${ver}"; [ -x "$d/${t.file}" ] && { echo "SUPERD-READY $d/${t.file}"; "$d/${t.file}" banner; exit 0; };`).join(" ")} echo "SUPERD-PROBE $(uname -s 2>/dev/null || echo unknown) $(uname -m 2>/dev/null || echo unknown)"; exit 90`, muxArgv);
|
|
2755
2624
|
}
|
|
2756
|
-
function buildHelperUploadArgv(plan, triple, file, assets) {
|
|
2625
|
+
function buildHelperUploadArgv(plan, triple, file, assets, muxArgv) {
|
|
2757
2626
|
const dir = `$HOME/.cache/superd/run/${triple}-${assets.manifest.ver}`;
|
|
2758
|
-
return helperShellArgv(plan, `mkdir -p "${dir}" && cat > "${dir}/${file}.tmp" && chmod 0755 "${dir}/${file}.tmp" && mv -f "${dir}/${file}.tmp" "${dir}/${file}"
|
|
2627
|
+
return helperShellArgv(plan, `mkdir -p "${dir}" && cat > "${dir}/${file}.tmp" && chmod 0755 "${dir}/${file}.tmp" && mv -f "${dir}/${file}.tmp" "${dir}/${file}"`, muxArgv);
|
|
2759
2628
|
}
|
|
2760
2629
|
function buildHelperOneShotArgv(plan, binPath, options) {
|
|
2761
2630
|
const { tag, cmd } = options;
|
|
2762
2631
|
switch (plan.kind) {
|
|
2763
|
-
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
|
|
2632
|
+
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
|
|
2633
|
+
command: `exec ${shQuote(binPath)} run --tag ${shQuote(tag)} -- bash -lc ${shQuote(cmd)}`,
|
|
2634
|
+
extraArgs: options.muxArgv
|
|
2635
|
+
});
|
|
2764
2636
|
case "docker": return [
|
|
2765
2637
|
"docker",
|
|
2766
2638
|
"exec",
|
|
@@ -2793,9 +2665,12 @@ function buildHelperOneShotArgv(plan, binPath, options) {
|
|
|
2793
2665
|
];
|
|
2794
2666
|
}
|
|
2795
2667
|
}
|
|
2796
|
-
function buildHelperKillgroupArgv(plan, binPath, tag) {
|
|
2668
|
+
function buildHelperKillgroupArgv(plan, binPath, tag, muxArgv) {
|
|
2797
2669
|
switch (plan.kind) {
|
|
2798
|
-
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
|
|
2670
|
+
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
|
|
2671
|
+
command: `${shQuote(binPath)} killgroup --tag ${shQuote(tag)}`,
|
|
2672
|
+
extraArgs: muxArgv
|
|
2673
|
+
});
|
|
2799
2674
|
case "docker": return [
|
|
2800
2675
|
"docker",
|
|
2801
2676
|
"exec",
|
|
@@ -2819,9 +2694,12 @@ function buildHelperKillgroupArgv(plan, binPath, tag) {
|
|
|
2819
2694
|
];
|
|
2820
2695
|
}
|
|
2821
2696
|
}
|
|
2822
|
-
function helperShellArgv(plan, script) {
|
|
2697
|
+
function helperShellArgv(plan, script, muxArgv) {
|
|
2823
2698
|
switch (plan.kind) {
|
|
2824
|
-
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
|
|
2699
|
+
case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
|
|
2700
|
+
command: script,
|
|
2701
|
+
extraArgs: muxArgv
|
|
2702
|
+
});
|
|
2825
2703
|
case "docker": return [
|
|
2826
2704
|
"docker",
|
|
2827
2705
|
"exec",
|
|
@@ -2843,7 +2721,243 @@ function helperShellArgv(plan, script) {
|
|
|
2843
2721
|
script
|
|
2844
2722
|
];
|
|
2845
2723
|
}
|
|
2846
|
-
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
//#endregion
|
|
2727
|
+
//#region src/remote/input.ts
|
|
2728
|
+
/** Local carrier validation precedes any target discovery, connection or spawn. */
|
|
2729
|
+
async function prepareRemoteInput(input) {
|
|
2730
|
+
if (input.mode !== void 0 && input.mode !== "oneshot" && input.mode !== "pty") throw new Error("[E_BAD_MODE] remote: mode must be oneshot or pty");
|
|
2731
|
+
if (input.scriptPath !== void 0) {
|
|
2732
|
+
if (input.cmd !== void 0 || input.spawn !== void 0) throw new Error("[E_SCRIPT_PARAMS] remote: script and cmd are alternative carriers — pass one; spawn belongs to BYO PTY creation, not scripts");
|
|
2733
|
+
if (typeof input.scriptPath !== "string" || !isAbsolute(input.scriptPath) || input.scriptPath.includes("\0")) throw new Error("[E_SCRIPT_PATH] remote: script must be a local absolute path without NUL");
|
|
2734
|
+
}
|
|
2735
|
+
if (!input.target && input.spawn === void 0 && input.ssh === void 0) throw new Error("[E_PARAMS] remote: target is required (spawn / ssh are BYO entry points and may stand alone)");
|
|
2736
|
+
if (input.scriptPath !== void 0) {
|
|
2737
|
+
let script;
|
|
2738
|
+
try {
|
|
2739
|
+
script = await readFile(input.scriptPath, "utf8");
|
|
2740
|
+
} catch (error) {
|
|
2741
|
+
throw new Error(`[E_SCRIPT_READ] remote: cannot read local script ${JSON.stringify(input.scriptPath)} — ${error instanceof Error ? error.message : String(error)}`);
|
|
2742
|
+
}
|
|
2743
|
+
if (script.includes("\0")) throw new Error("[E_SCRIPT_NUL] remote: script contains NUL bytes — the command channel cannot carry it; transfer the file and run it with cmd instead");
|
|
2744
|
+
return {
|
|
2745
|
+
target: input.target,
|
|
2746
|
+
ssh: input.ssh,
|
|
2747
|
+
cmd: `bash -c ${shQuote(script)}`,
|
|
2748
|
+
stdin: input.stdin,
|
|
2749
|
+
mode: input.mode,
|
|
2750
|
+
timeout: input.timeout
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
return {
|
|
2754
|
+
target: input.target,
|
|
2755
|
+
spawn: input.spawn,
|
|
2756
|
+
ssh: input.ssh,
|
|
2757
|
+
cmd: input.cmd ?? "",
|
|
2758
|
+
stdin: input.stdin,
|
|
2759
|
+
mode: input.mode,
|
|
2760
|
+
timeout: input.timeout
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
//#endregion
|
|
2765
|
+
//#region src/remote/oneshot.ts
|
|
2766
|
+
async function runOneShot(argv, opts = {}) {
|
|
2767
|
+
const process$1 = startOneShot(argv, opts);
|
|
2768
|
+
const result = await process$1.done;
|
|
2769
|
+
const stdout = process$1.read("stdout", 0), stderr = process$1.read("stderr", 0);
|
|
2770
|
+
return {
|
|
2771
|
+
...result,
|
|
2772
|
+
stdout: stdout.text,
|
|
2773
|
+
stderr: [stderr.text, stderr.diagnostic].filter(Boolean).join("\n"),
|
|
2774
|
+
outputStats: {
|
|
2775
|
+
stdout: stdout.stats,
|
|
2776
|
+
stderr: stderr.stats
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
//#endregion
|
|
2782
|
+
//#region src/remote/helper.ts
|
|
2783
|
+
/** Reads helper-bin/MANIFEST.json next to the compiled module (lib/remote in the npm
|
|
2784
|
+
* package, super-dsh/remote in the Super closure — same relative layout). Missing or
|
|
2785
|
+
* malformed assets ⇒ null ⇒ shim-only mode; never throws. */
|
|
2786
|
+
function helperAssets(baseDir) {
|
|
2787
|
+
const dir = baseDir ?? join(dirname(fileURLToPath(import.meta.url)), "helper-bin");
|
|
2788
|
+
try {
|
|
2789
|
+
const manifest = JSON.parse(readFileSync(join(dir, "MANIFEST.json"), "utf8"));
|
|
2790
|
+
if (manifest?.format !== 1 || typeof manifest.ver !== "string" || typeof manifest.protocol !== "number" || !manifest.targets || typeof manifest.targets !== "object") return null;
|
|
2791
|
+
return {
|
|
2792
|
+
dir,
|
|
2793
|
+
manifest
|
|
2794
|
+
};
|
|
2795
|
+
} catch {
|
|
2796
|
+
return null;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
/** `uname -s`/`uname -m` → shipped target triple. Unknown combos ⇒ null (shim). */
|
|
2800
|
+
function archTriple(sys, machine, shipped) {
|
|
2801
|
+
const s = sys.trim().toLowerCase(), m = machine.trim().toLowerCase();
|
|
2802
|
+
const candidates = s === "linux" ? m === "x86_64" || m === "amd64" ? "x86_64-unknown-linux-musl" : m === "aarch64" || m === "arm64" ? "aarch64-unknown-linux-musl" : null : s === "darwin" ? m === "arm64" || m === "aarch64" ? "aarch64-apple-darwin" : null : null;
|
|
2803
|
+
if (candidates === null) return null;
|
|
2804
|
+
return shipped.has(candidates) ? candidates : null;
|
|
2805
|
+
}
|
|
2806
|
+
function planKey(plan) {
|
|
2807
|
+
switch (plan.kind) {
|
|
2808
|
+
case "ssh": {
|
|
2809
|
+
const c = plan.connection;
|
|
2810
|
+
const host = c?.host ?? plan.host;
|
|
2811
|
+
return `ssh|${c?.user ? c.user + "@" : ""}${host}${c?.port ? ":" + c.port : ""}${c?.proxyJump ? "|" + c.proxyJump : ""}`;
|
|
2812
|
+
}
|
|
2813
|
+
case "docker": return `docker|${plan.container}`;
|
|
2814
|
+
case "incus": return `incus|${plan.container}`;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
const PROBE_RE = /^SUPERD-PROBE (\S+) (\S+)$/m;
|
|
2818
|
+
const READY_RE = /^SUPERD-READY (\S+)$/m;
|
|
2819
|
+
var HelperRegistry = class {
|
|
2820
|
+
states = /* @__PURE__ */ new Map();
|
|
2821
|
+
constructor(assets, exec, probeArgv, uploadArgv) {
|
|
2822
|
+
this.assets = assets;
|
|
2823
|
+
this.exec = exec;
|
|
2824
|
+
this.probeArgv = probeArgv;
|
|
2825
|
+
this.uploadArgv = uploadArgv;
|
|
2826
|
+
}
|
|
2827
|
+
/** Never throws: any bootstrap failure resolves null (sticky) and the caller stays on the shim. */
|
|
2828
|
+
ensure(plan, muxArgv = []) {
|
|
2829
|
+
if (plan.kind !== "ssh" && plan.kind !== "docker" && plan.kind !== "incus") return Promise.resolve(null);
|
|
2830
|
+
const key = planKey(plan);
|
|
2831
|
+
let entry = this.states.get(key);
|
|
2832
|
+
if (entry === void 0) {
|
|
2833
|
+
entry = this.bootstrap(plan, muxArgv).catch(() => null);
|
|
2834
|
+
entry.catch(() => {});
|
|
2835
|
+
this.states.set(key, entry);
|
|
2836
|
+
}
|
|
2837
|
+
return entry;
|
|
2838
|
+
}
|
|
2839
|
+
async bootstrap(plan, muxArgv) {
|
|
2840
|
+
const { manifest } = this.assets;
|
|
2841
|
+
const first = await this.exec(this.probeArgv(plan, this.assets, muxArgv), { timeoutSec: 15 });
|
|
2842
|
+
const ready = this.parseReady(first.stdout, manifest.ver, manifest.protocol);
|
|
2843
|
+
if (ready !== null) return ready;
|
|
2844
|
+
const probe = PROBE_RE.exec(first.stdout);
|
|
2845
|
+
if (probe === null || first.exit !== 90 && first.exit !== 0) return null;
|
|
2846
|
+
const triple = archTriple(probe[1], probe[2], new Set(Object.keys(manifest.targets)));
|
|
2847
|
+
if (triple === null) return null;
|
|
2848
|
+
const target = manifest.targets[triple];
|
|
2849
|
+
const binary = await readFile(join(this.assets.dir, triple, target.file));
|
|
2850
|
+
if ((await this.exec(this.uploadArgv(plan, triple, target.file, this.assets, muxArgv), {
|
|
2851
|
+
stdin: binary,
|
|
2852
|
+
timeoutSec: 60
|
|
2853
|
+
})).exit !== 0) return null;
|
|
2854
|
+
const second = await this.exec(this.probeArgv(plan, this.assets, muxArgv), { timeoutSec: 15 });
|
|
2855
|
+
return this.parseReady(second.stdout, manifest.ver, manifest.protocol);
|
|
2856
|
+
}
|
|
2857
|
+
parseReady(stdout, ver, protocol) {
|
|
2858
|
+
const ready = READY_RE.exec(stdout);
|
|
2859
|
+
const banner = /^SUPERD-RUN proto=(\d+) name=superd-run ver=(\S+) target=(\S+)/m.exec(stdout);
|
|
2860
|
+
if (ready === null || banner === null) return null;
|
|
2861
|
+
if (banner[1] !== String(protocol) || banner[2] !== ver) return null;
|
|
2862
|
+
return {
|
|
2863
|
+
binPath: ready[1],
|
|
2864
|
+
triple: banner[3],
|
|
2865
|
+
ver,
|
|
2866
|
+
protocol
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
|
|
2871
|
+
//#endregion
|
|
2872
|
+
//#region src/remote/ssh-mux.ts
|
|
2873
|
+
/** SSH oneshot 连接复用(plan 2026-11-01 Task E;探针基线 = 发起端 OpenSSH 10.2p1,2026-11-01 实测):
|
|
2874
|
+
* - master 与客户端完全脱钩(fork 后 PPID=init、自有 pgid/session)→ 本地 killpg 取消/超时永不波及
|
|
2875
|
+
* master,后续 oneshot 继续复用;这正好落在 startBinaryProcess 的组杀清理语义之内。
|
|
2876
|
+
* - ControlPersist 到期 master 自行退出并 unlink socket;`ssh -O exit` 主动拆除亦同。
|
|
2877
|
+
* - master 被 SIGKILL 会留下残 socket 文件:ControlMaster=auto 的下一个客户端会自愈重建
|
|
2878
|
+
* (实测 exit 0),失败语义按 ssh 真实回退,不冒充复用。
|
|
2879
|
+
* - 只注入 SSH oneshot 执行链(POSIX wrapper / helper probe·upload·run·killgroup / cleanup /
|
|
2880
|
+
* status 探测);PTY、scp、rsync、relay 与 copy 的 mkdir pre-hop 一律不注入。
|
|
2881
|
+
* - CLI `-o` 覆盖用户 config 同名项是 OpenSSH 语义(last-option-wins),其余身份选项照常读 config。 */
|
|
2882
|
+
const SSH_MUX_CONTROL_PERSIST_SEC = 600;
|
|
2883
|
+
/** Linux sun_path 上限 108;留余量并在超限时诚实退化为不复用(MUX 是优化,不是前提)。 */
|
|
2884
|
+
const MAX_SOCKET_PATH = 104;
|
|
2885
|
+
function identityKey(identity) {
|
|
2886
|
+
const c = identity.connection;
|
|
2887
|
+
const raw = JSON.stringify([
|
|
2888
|
+
identity.target,
|
|
2889
|
+
c.host,
|
|
2890
|
+
c.user ?? null,
|
|
2891
|
+
c.port ?? null,
|
|
2892
|
+
c.key ?? null,
|
|
2893
|
+
c.proxyJump ?? null
|
|
2894
|
+
]);
|
|
2895
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 16);
|
|
2896
|
+
}
|
|
2897
|
+
/** 每 Agent 一个(driver 私有):tool-owned 0700 socket 目录 + 按连接身份的 socket 名。
|
|
2898
|
+
* 目录名含随机 owner 后缀,跨 Agent 天然不共享;dispose 逐 socket `-O exit` 后整目录删除。 */
|
|
2899
|
+
var SshMux = class {
|
|
2900
|
+
persistSec;
|
|
2901
|
+
rootDir;
|
|
2902
|
+
runner;
|
|
2903
|
+
owner = randomUUID().slice(0, 8);
|
|
2904
|
+
sockets = /* @__PURE__ */ new Map();
|
|
2905
|
+
dir;
|
|
2906
|
+
disposal;
|
|
2907
|
+
constructor(options = {}) {
|
|
2908
|
+
this.persistSec = options.persistSec ?? SSH_MUX_CONTROL_PERSIST_SEC;
|
|
2909
|
+
this.rootDir = options.rootDir ?? tmpdir();
|
|
2910
|
+
this.runner = options.runner ?? runOneShot;
|
|
2911
|
+
}
|
|
2912
|
+
/** 注入 SSH argv 的 MUX 选项(trusted internal argv,非模型输入)。目录懒建(纯 docker/hub
|
|
2913
|
+
* Agent 永不触盘);超限/已 dispose 返回 [] = 诚实退化为普通连接。 */
|
|
2914
|
+
argvFor(identity) {
|
|
2915
|
+
if (this.disposal !== void 0) return [];
|
|
2916
|
+
const socket = join(this.dir ?? (this.dir = join(this.rootDir, `superd-mux-${this.owner}`)), identityKey(identity));
|
|
2917
|
+
if (socket.length + 1 > MAX_SOCKET_PATH) return [];
|
|
2918
|
+
try {
|
|
2919
|
+
mkdirSync(this.dir, {
|
|
2920
|
+
recursive: true,
|
|
2921
|
+
mode: 448
|
|
2922
|
+
});
|
|
2923
|
+
} catch {
|
|
2924
|
+
return [];
|
|
2925
|
+
}
|
|
2926
|
+
this.sockets.set(socket, identity.connection.host);
|
|
2927
|
+
return [
|
|
2928
|
+
"-o",
|
|
2929
|
+
`ControlPath=${socket}`,
|
|
2930
|
+
"-o",
|
|
2931
|
+
"ControlMaster=auto",
|
|
2932
|
+
"-o",
|
|
2933
|
+
`ControlPersist=${this.persistSec}`
|
|
2934
|
+
];
|
|
2935
|
+
}
|
|
2936
|
+
/** Teardown:先 `-O exit` 逐 socket 礼貌拆除 master(不拨号、5s 上限、错误全吞——
|
|
2937
|
+
* master 已死/残 socket 属正常),再删目录。单飞(重复调用同一 Promise)。 */
|
|
2938
|
+
dispose() {
|
|
2939
|
+
if (this.disposal !== void 0) return this.disposal;
|
|
2940
|
+
const sockets = [...this.sockets.entries()];
|
|
2941
|
+
this.sockets.clear();
|
|
2942
|
+
return this.disposal = (async () => {
|
|
2943
|
+
await Promise.allSettled(sockets.map(([socket, host]) => this.runner([
|
|
2944
|
+
"ssh",
|
|
2945
|
+
"-o",
|
|
2946
|
+
`ControlPath=${socket}`,
|
|
2947
|
+
"-O",
|
|
2948
|
+
"exit",
|
|
2949
|
+
"--",
|
|
2950
|
+
host
|
|
2951
|
+
], { timeoutSec: 5 })));
|
|
2952
|
+
if (this.dir !== void 0) try {
|
|
2953
|
+
rmSync(this.dir, {
|
|
2954
|
+
recursive: true,
|
|
2955
|
+
force: true
|
|
2956
|
+
});
|
|
2957
|
+
} catch {}
|
|
2958
|
+
})();
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2847
2961
|
|
|
2848
2962
|
//#endregion
|
|
2849
2963
|
//#region src/remote/nonce-framing.ts
|
|
@@ -2899,7 +3013,7 @@ function createNonceFrameParser(nonce, events) {
|
|
|
2899
3013
|
const prefix = `\x1b]133;D;${nonce};`;
|
|
2900
3014
|
const prefixE = `\x1b]133;E;${nonce};`;
|
|
2901
3015
|
const prefixes = [prefix, prefixE];
|
|
2902
|
-
let buf = "";
|
|
3016
|
+
let buf$1 = "";
|
|
2903
3017
|
let done = false;
|
|
2904
3018
|
const emit = (raw) => {
|
|
2905
3019
|
const text = normalizePtyText(raw);
|
|
@@ -2909,8 +3023,8 @@ function createNonceFrameParser(nonce, events) {
|
|
|
2909
3023
|
const holdback = () => {
|
|
2910
3024
|
let best = 0;
|
|
2911
3025
|
for (const p of prefixes) {
|
|
2912
|
-
const max = Math.min(buf.length, p.length - 1);
|
|
2913
|
-
for (let k = max; k > best; k--) if (buf.endsWith(p.slice(0, k))) {
|
|
3026
|
+
const max = Math.min(buf$1.length, p.length - 1);
|
|
3027
|
+
for (let k = max; k > best; k--) if (buf$1.endsWith(p.slice(0, k))) {
|
|
2914
3028
|
best = k;
|
|
2915
3029
|
break;
|
|
2916
3030
|
}
|
|
@@ -2923,7 +3037,7 @@ function createNonceFrameParser(nonce, events) {
|
|
|
2923
3037
|
let idx = -1;
|
|
2924
3038
|
let isEnv = false;
|
|
2925
3039
|
for (const p of prefixes) {
|
|
2926
|
-
const i = buf.indexOf(p);
|
|
3040
|
+
const i = buf$1.indexOf(p);
|
|
2927
3041
|
if (i >= 0 && (idx === -1 || i < idx)) {
|
|
2928
3042
|
idx = i;
|
|
2929
3043
|
isEnv = p === prefixE;
|
|
@@ -2931,26 +3045,26 @@ function createNonceFrameParser(nonce, events) {
|
|
|
2931
3045
|
}
|
|
2932
3046
|
if (idx < 0) {
|
|
2933
3047
|
const keep = holdback();
|
|
2934
|
-
emit(buf.slice(0, buf.length - keep));
|
|
2935
|
-
buf = buf.slice(buf.length - keep);
|
|
3048
|
+
emit(buf$1.slice(0, buf$1.length - keep));
|
|
3049
|
+
buf$1 = buf$1.slice(buf$1.length - keep);
|
|
2936
3050
|
return;
|
|
2937
3051
|
}
|
|
2938
|
-
emit(buf.slice(0, idx));
|
|
2939
|
-
const rest = buf.slice(idx + (isEnv ? prefixE.length : prefix.length));
|
|
3052
|
+
emit(buf$1.slice(0, idx));
|
|
3053
|
+
const rest = buf$1.slice(idx + (isEnv ? prefixE.length : prefix.length));
|
|
2940
3054
|
if (isEnv) {
|
|
2941
3055
|
const m$1 = ENV_BODY_RE.exec(rest);
|
|
2942
3056
|
if (m$1 === null) {
|
|
2943
|
-
buf = buf.slice(idx);
|
|
3057
|
+
buf$1 = buf$1.slice(idx);
|
|
2944
3058
|
return;
|
|
2945
3059
|
}
|
|
2946
3060
|
if (m$1[1].length > 0) events.onEnv?.(m$1[1]);
|
|
2947
|
-
buf = rest.slice(m$1[0].length);
|
|
3061
|
+
buf$1 = rest.slice(m$1[0].length);
|
|
2948
3062
|
consume();
|
|
2949
3063
|
return;
|
|
2950
3064
|
}
|
|
2951
3065
|
const m = FRAME_BODY_RE.exec(rest);
|
|
2952
3066
|
if (m === null) {
|
|
2953
|
-
buf = buf.slice(idx);
|
|
3067
|
+
buf$1 = buf$1.slice(idx);
|
|
2954
3068
|
return;
|
|
2955
3069
|
}
|
|
2956
3070
|
events.onFrame({
|
|
@@ -2958,16 +3072,16 @@ function createNonceFrameParser(nonce, events) {
|
|
|
2958
3072
|
cwd: m[2].length > 0 ? m[2] : null
|
|
2959
3073
|
});
|
|
2960
3074
|
done = true;
|
|
2961
|
-
buf = "";
|
|
3075
|
+
buf$1 = "";
|
|
2962
3076
|
};
|
|
2963
3077
|
return {
|
|
2964
3078
|
feed(chunk) {
|
|
2965
|
-
buf += chunk;
|
|
3079
|
+
buf$1 += chunk;
|
|
2966
3080
|
consume();
|
|
2967
3081
|
},
|
|
2968
3082
|
flush() {
|
|
2969
|
-
if (buf.length > 0) events.onOutput?.(buf);
|
|
2970
|
-
buf = "";
|
|
3083
|
+
if (buf$1.length > 0) events.onOutput?.(buf$1);
|
|
3084
|
+
buf$1 = "";
|
|
2971
3085
|
}
|
|
2972
3086
|
};
|
|
2973
3087
|
}
|
|
@@ -3010,6 +3124,67 @@ function diffEnv(prev, next) {
|
|
|
3010
3124
|
};
|
|
3011
3125
|
}
|
|
3012
3126
|
|
|
3127
|
+
//#endregion
|
|
3128
|
+
//#region src/remote/trace.ts
|
|
3129
|
+
/**
|
|
3130
|
+
* R1 定位轮插桩(2026-11-01):`SUPERD_REMOTE_TRACE=<file>` 时输出 JSONL 事件,否则全 no-op。
|
|
3131
|
+
* 动机:rig-only「PTY background + startup stdin」丢失只在 rig 长驻进程复现(bg-read 形状 13/13),
|
|
3132
|
+
* 本地全组合 0 复现——必须在实际 rig 宿主进程内拿到第一手事件流。
|
|
3133
|
+
*
|
|
3134
|
+
* 设计约束:
|
|
3135
|
+
* - 热路径(write/onOutput)零同步 IO:事件先进内存缓冲,异步 flush(settle/dispose/满 200 条)。
|
|
3136
|
+
* - env 未设置时每个入口都是一次函数调用 + 立即 return,不改变交付语义。
|
|
3137
|
+
* - 失败静默(trace 永不破坏工具行为);缓冲上限 5000 条防失控。
|
|
3138
|
+
*/
|
|
3139
|
+
const TRACE_PATH = process.env.SUPERD_REMOTE_TRACE ?? "";
|
|
3140
|
+
const ENABLED = TRACE_PATH !== "";
|
|
3141
|
+
let buf = [];
|
|
3142
|
+
let flushing = false;
|
|
3143
|
+
function serialize(ev, fields) {
|
|
3144
|
+
let s;
|
|
3145
|
+
try {
|
|
3146
|
+
s = JSON.stringify({
|
|
3147
|
+
t: Date.now(),
|
|
3148
|
+
mono: Number(process.hrtime.bigint() / 1000000n),
|
|
3149
|
+
ev,
|
|
3150
|
+
...fields
|
|
3151
|
+
});
|
|
3152
|
+
} catch {
|
|
3153
|
+
s = JSON.stringify({
|
|
3154
|
+
t: Date.now(),
|
|
3155
|
+
ev,
|
|
3156
|
+
err: "unserializable-fields"
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
return s ?? `{\"t\":${Date.now()},\"ev\":${JSON.stringify(ev)},\"err\":\"null\"}`;
|
|
3160
|
+
}
|
|
3161
|
+
function trace(ev, fields = {}) {
|
|
3162
|
+
if (!ENABLED) return;
|
|
3163
|
+
buf.push(serialize(ev, fields));
|
|
3164
|
+
if (buf.length > 5e3) buf.splice(0, buf.length - 5e3);
|
|
3165
|
+
if (buf.length >= 200) traceFlush();
|
|
3166
|
+
}
|
|
3167
|
+
async function traceFlush() {
|
|
3168
|
+
if (!ENABLED || flushing || buf.length === 0) return;
|
|
3169
|
+
flushing = true;
|
|
3170
|
+
const out = buf.splice(0).join("\n") + "\n";
|
|
3171
|
+
try {
|
|
3172
|
+
const [{ appendFile }, { dirname: dirname$1 }] = await Promise.all([import("node:fs/promises"), import("node:path")]);
|
|
3173
|
+
const dir = dirname$1(TRACE_PATH);
|
|
3174
|
+
try {
|
|
3175
|
+
await (await import("node:fs/promises")).mkdir(dir, { recursive: true });
|
|
3176
|
+
} catch {}
|
|
3177
|
+
await appendFile(TRACE_PATH, out);
|
|
3178
|
+
} catch {}
|
|
3179
|
+
flushing = false;
|
|
3180
|
+
if (buf.length > 0) traceFlush();
|
|
3181
|
+
}
|
|
3182
|
+
/** 事件里的字节安全截断表示(JSON 转义 + 长度上限)。 */
|
|
3183
|
+
function traceRepr(s, max) {
|
|
3184
|
+
const j = JSON.stringify(s) ?? "null";
|
|
3185
|
+
return j.length > max ? `${j.slice(0, max)}…` : j;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3013
3188
|
//#endregion
|
|
3014
3189
|
//#region src/remote/pty-session.ts
|
|
3015
3190
|
/** spec §五.2 逐字。 */
|
|
@@ -3021,11 +3196,6 @@ function displayKey(key) {
|
|
|
3021
3196
|
const raw = key.includes("|") ? key.slice(key.indexOf("|") + 1) : key;
|
|
3022
3197
|
return raw.startsWith("p:") ? `pty:${raw.slice(2)}` : raw;
|
|
3023
3198
|
}
|
|
3024
|
-
const LEDGER_MAX = 20;
|
|
3025
|
-
const summaryOf = (cmd) => {
|
|
3026
|
-
const first = cmd.trim().split(/\r?\n/, 1)[0] ?? "";
|
|
3027
|
-
return first.length > 72 ? `${first.slice(0, 71)}…` : first;
|
|
3028
|
-
};
|
|
3029
3199
|
const DEFAULT_INIT_TIMEOUT_SEC = 30;
|
|
3030
3200
|
const INTERRUPT_GRACE_MS = 3e3;
|
|
3031
3201
|
/** F2(0.3.2-d):\x03 触发 ISIG 时内核 flush 输入队列——130-marker 必须等 flush 完成
|
|
@@ -3042,12 +3212,11 @@ var PtySession = class {
|
|
|
3042
3212
|
deathWaiters = [];
|
|
3043
3213
|
diag = "";
|
|
3044
3214
|
inFlight = 0;
|
|
3215
|
+
cmdSeq = 0;
|
|
3045
3216
|
/** P4/P5:当前占用前台的 detached job(background/超时 handoff)。非 null 时新命令拒绝(Q6)。 */
|
|
3046
3217
|
detachedJob = null;
|
|
3047
3218
|
/** §3.2(2026-10-25):当前占用会话的命令——dispatch 同步占位,settle/cleanup 释放;input 的唯一写入对象。 */
|
|
3048
3219
|
activeCommand = null;
|
|
3049
|
-
/** 账本(§5):本会话最近 20 条命令(FIFO,超出丢最旧)。 */
|
|
3050
|
-
ledger = [];
|
|
3051
3220
|
/** env 侧信道基线(init E 帧);undefined = 侧信道不可用(不报假 delta)。 */
|
|
3052
3221
|
envBaseline;
|
|
3053
3222
|
/** idle 起点(ready / settle 时重置);null = 尚无 idle 时钟(cold 或已 dispose)。 */
|
|
@@ -3078,10 +3247,12 @@ var PtySession = class {
|
|
|
3078
3247
|
get detachedCommandId() {
|
|
3079
3248
|
return this.detachedJob?.commandId ?? null;
|
|
3080
3249
|
}
|
|
3081
|
-
/**
|
|
3250
|
+
/** handoff/card 面(2026-11-01 默认 PTY 轮):当前占用会话的命令与可选 job 句柄;无 = null。 */
|
|
3251
|
+
get active() {
|
|
3252
|
+
return this.activeCommand === null ? null : { ...this.activeCommand };
|
|
3253
|
+
}
|
|
3254
|
+
/** job 通道收养后回填 jobId(timeout handoff 时 registry id ≠ commandId);运行中的命令同步携带(stdin-only 写入回执用)。 */
|
|
3082
3255
|
attachJob(commandId, jobId) {
|
|
3083
|
-
const entry = [...this.ledger].reverse().find((e) => e.commandId === commandId);
|
|
3084
|
-
if (entry !== void 0) entry.jobId = jobId;
|
|
3085
3256
|
if (this.activeCommand !== null && this.activeCommand.commandId === commandId) this.activeCommand.jobId = jobId;
|
|
3086
3257
|
}
|
|
3087
3258
|
/** Workbench 身份(§3):id + 可选 label + connection + byo 标记。 */
|
|
@@ -3094,10 +3265,6 @@ var PtySession = class {
|
|
|
3094
3265
|
builtOn: this.opts.builtOn === true
|
|
3095
3266
|
};
|
|
3096
3267
|
}
|
|
3097
|
-
/** 账本快照(session card 面,只读)。 */
|
|
3098
|
-
ledgerSnapshot() {
|
|
3099
|
-
return this.ledger;
|
|
3100
|
-
}
|
|
3101
3268
|
/** spawn 创建原文(仅 BYO 会话;v2 起仅存档,不参与复用判定)。 */
|
|
3102
3269
|
get origin() {
|
|
3103
3270
|
return this.opts.origin;
|
|
@@ -3126,24 +3293,15 @@ var PtySession = class {
|
|
|
3126
3293
|
if (this.state === "disposed") return Promise.reject(/* @__PURE__ */ new Error(`[E_SESSION_DISPOSED] remote pty session '${this.opts.id}' was disposed before dispatch`));
|
|
3127
3294
|
if (this.activeCommand !== null) {
|
|
3128
3295
|
const handle = this.activeCommand.jobId ?? this.activeCommand.commandId;
|
|
3129
|
-
return Promise.reject(/* @__PURE__ */ new Error(`[E_SESSION_BUSY] remote: pty session '${this.opts.id}' still runs a command (${this.activeCommand.commandId}) —
|
|
3296
|
+
return Promise.reject(/* @__PURE__ */ new Error(`[E_SESSION_BUSY] remote: pty session '${this.opts.id}' still runs a command (${this.activeCommand.commandId}) — feed it more stdin with {action:"exec", target:..., stdin:...} (stdin-only write), follow its output via job_output ${handle} (a synchronous exec hands its job off there on timeout), cancel with job_kill ${handle}, or address other work to another session / oneshot`));
|
|
3130
3297
|
}
|
|
3131
3298
|
const commandId = dOpts.commandId;
|
|
3132
|
-
const entry = {
|
|
3133
|
-
commandId,
|
|
3134
|
-
summary: summaryOf(cmd),
|
|
3135
|
-
exit: null,
|
|
3136
|
-
durationMs: 0,
|
|
3137
|
-
running: true,
|
|
3138
|
-
...dOpts.jobId !== void 0 ? { jobId: dOpts.jobId } : {}
|
|
3139
|
-
};
|
|
3140
|
-
this.ledger.push(entry);
|
|
3141
|
-
if (this.ledger.length > LEDGER_MAX) this.ledger.shift();
|
|
3142
3299
|
this.activeCommand = {
|
|
3143
3300
|
commandId,
|
|
3144
3301
|
...dOpts.jobId !== void 0 ? { jobId: dOpts.jobId } : {}
|
|
3145
3302
|
};
|
|
3146
3303
|
this.inFlight++;
|
|
3304
|
+
this.cmdSeq++;
|
|
3147
3305
|
const attempt = async () => {
|
|
3148
3306
|
const started = Date.now();
|
|
3149
3307
|
const reconnected = this.state === "dead";
|
|
@@ -3152,8 +3310,6 @@ var PtySession = class {
|
|
|
3152
3310
|
const abandon = () => {
|
|
3153
3311
|
this.activeCommand = null;
|
|
3154
3312
|
this.inFlight--;
|
|
3155
|
-
const index = this.ledger.indexOf(entry);
|
|
3156
|
-
if (index >= 0) this.ledger.splice(index, 1);
|
|
3157
3313
|
};
|
|
3158
3314
|
if (this.state !== "ready") try {
|
|
3159
3315
|
await this.start();
|
|
@@ -3168,6 +3324,20 @@ var PtySession = class {
|
|
|
3168
3324
|
return await new Promise((resolve$1) => {
|
|
3169
3325
|
const nonce = genNonce();
|
|
3170
3326
|
const ring = new Utf16TailRing(OUTPUT_HARD_CAP);
|
|
3327
|
+
let traceChunkSeq = 0;
|
|
3328
|
+
trace("disp", {
|
|
3329
|
+
s: this.opts.id,
|
|
3330
|
+
key: this.opts.key,
|
|
3331
|
+
commandId,
|
|
3332
|
+
bg: dOpts.background === true,
|
|
3333
|
+
stdinGiven: dOpts.stdin !== void 0,
|
|
3334
|
+
cmdLen: cmd.length,
|
|
3335
|
+
stdinLen: dOpts.stdin?.length ?? 0,
|
|
3336
|
+
cmdRepr: traceRepr(cmd, 140),
|
|
3337
|
+
stdinRepr: traceRepr(dOpts.stdin ?? "", 60),
|
|
3338
|
+
state: this.state,
|
|
3339
|
+
priorCmds: this.cmdSeq
|
|
3340
|
+
});
|
|
3171
3341
|
let interrupted = false;
|
|
3172
3342
|
let settled = false;
|
|
3173
3343
|
let jobSettled = false;
|
|
@@ -3190,6 +3360,13 @@ var PtySession = class {
|
|
|
3190
3360
|
};
|
|
3191
3361
|
this.detachedJob = job;
|
|
3192
3362
|
settled = true;
|
|
3363
|
+
trace("detach", {
|
|
3364
|
+
s: this.opts.id,
|
|
3365
|
+
commandId,
|
|
3366
|
+
outLen: ring.text.length,
|
|
3367
|
+
tail: traceRepr(ring.text, 220)
|
|
3368
|
+
});
|
|
3369
|
+
traceFlush();
|
|
3193
3370
|
resolve$1({
|
|
3194
3371
|
output: ring.text,
|
|
3195
3372
|
outputStats: { stdout: ring.stats() },
|
|
@@ -3216,6 +3393,16 @@ var PtySession = class {
|
|
|
3216
3393
|
const settle = (r) => {
|
|
3217
3394
|
if (jobSettled) return;
|
|
3218
3395
|
jobSettled = true;
|
|
3396
|
+
trace("settle", {
|
|
3397
|
+
s: this.opts.id,
|
|
3398
|
+
commandId,
|
|
3399
|
+
exit: r.exit,
|
|
3400
|
+
timedOut: r.timedOut,
|
|
3401
|
+
cwd: r.cwd,
|
|
3402
|
+
outLen: r.output.length,
|
|
3403
|
+
tail: traceRepr(r.output, 260)
|
|
3404
|
+
});
|
|
3405
|
+
traceFlush();
|
|
3219
3406
|
cleanup();
|
|
3220
3407
|
this.inFlight--;
|
|
3221
3408
|
this.idleSince = Date.now();
|
|
@@ -3223,9 +3410,6 @@ var PtySession = class {
|
|
|
3223
3410
|
if (r.cwd !== null) this.lastCwdSeen = r.cwd;
|
|
3224
3411
|
const envDelta = envNext !== void 0 ? diffEnv(this.envBaseline, envNext) : void 0;
|
|
3225
3412
|
if (envNext !== void 0) this.envBaseline = envNext;
|
|
3226
|
-
entry.exit = r.exit;
|
|
3227
|
-
entry.durationMs = Date.now() - started;
|
|
3228
|
-
entry.running = false;
|
|
3229
3413
|
const full = {
|
|
3230
3414
|
...r,
|
|
3231
3415
|
outputStats: { stdout: ring.stats() },
|
|
@@ -3251,6 +3435,13 @@ var PtySession = class {
|
|
|
3251
3435
|
const parser = createNonceFrameParser(nonce, {
|
|
3252
3436
|
onOutput: (t) => {
|
|
3253
3437
|
ring.push(t);
|
|
3438
|
+
if (traceChunkSeq < 60) trace("out", {
|
|
3439
|
+
s: this.opts.id,
|
|
3440
|
+
commandId,
|
|
3441
|
+
n: traceChunkSeq++,
|
|
3442
|
+
len: t.length,
|
|
3443
|
+
text: traceRepr(t, 180)
|
|
3444
|
+
});
|
|
3254
3445
|
},
|
|
3255
3446
|
onEnv: (b64) => {
|
|
3256
3447
|
const m = decodeEnvSnapshot(b64);
|
|
@@ -3281,8 +3472,41 @@ var PtySession = class {
|
|
|
3281
3472
|
}, INTERRUPT_GRACE_MS);
|
|
3282
3473
|
};
|
|
3283
3474
|
this.onChunk = (c) => parser.feed(c);
|
|
3284
|
-
|
|
3285
|
-
|
|
3475
|
+
const startup = dOpts.stdin === void 0 ? wrapPtyCommand(cmd, nonce) : wrapPtyCommand(cmd, nonce) + dOpts.stdin;
|
|
3476
|
+
const wStdin = this.proc?.stdin;
|
|
3477
|
+
if (wStdin?.writable === true) {
|
|
3478
|
+
const wl0 = wStdin.writableLength;
|
|
3479
|
+
let wret;
|
|
3480
|
+
try {
|
|
3481
|
+
wret = wStdin.write(startup);
|
|
3482
|
+
} catch (error) {
|
|
3483
|
+
trace("werr", {
|
|
3484
|
+
s: this.opts.id,
|
|
3485
|
+
commandId,
|
|
3486
|
+
msg: error instanceof Error ? error.message : String(error)
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
if (wret !== void 0) {
|
|
3490
|
+
trace("w1", {
|
|
3491
|
+
s: this.opts.id,
|
|
3492
|
+
commandId,
|
|
3493
|
+
bytes: Buffer$1.byteLength(startup),
|
|
3494
|
+
ret: wret,
|
|
3495
|
+
wl0,
|
|
3496
|
+
wl1: wStdin.writableLength,
|
|
3497
|
+
payloadTail: traceRepr(startup.slice(-90), 120)
|
|
3498
|
+
});
|
|
3499
|
+
if (wret === false) wStdin.once("drain", () => {
|
|
3500
|
+
trace("drain", {
|
|
3501
|
+
s: this.opts.id,
|
|
3502
|
+
commandId
|
|
3503
|
+
});
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
} else trace("wskip", {
|
|
3507
|
+
s: this.opts.id,
|
|
3508
|
+
commandId
|
|
3509
|
+
});
|
|
3286
3510
|
this.deathWaiters.push(deathWaiter);
|
|
3287
3511
|
if (dOpts.background === true) {
|
|
3288
3512
|
detach();
|
|
@@ -3309,6 +3533,12 @@ var PtySession = class {
|
|
|
3309
3533
|
if (stdin?.writable !== true) return fail("[E_SESSION_WRITE_FAILED]", `pty session '${displayKey(this.opts.key)}' has no writable stdin channel (state ${this.state}) — follow the running command via job_output for its settled result`);
|
|
3310
3534
|
try {
|
|
3311
3535
|
stdin.write(options.stdin);
|
|
3536
|
+
trace("in", {
|
|
3537
|
+
s: this.opts.id,
|
|
3538
|
+
commandId: active.commandId,
|
|
3539
|
+
bytes: Buffer$1.byteLength(options.stdin, "utf8"),
|
|
3540
|
+
repr: traceRepr(options.stdin, 60)
|
|
3541
|
+
});
|
|
3312
3542
|
} catch (error) {
|
|
3313
3543
|
return fail("[E_SESSION_WRITE_FAILED]", `writing stdin to pty session '${displayKey(this.opts.key)}' failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3314
3544
|
}
|
|
@@ -3325,6 +3555,11 @@ var PtySession = class {
|
|
|
3325
3555
|
this.clearIdleTimer();
|
|
3326
3556
|
this.clearDeathTimer();
|
|
3327
3557
|
this.state = "disposed";
|
|
3558
|
+
trace("dispose", {
|
|
3559
|
+
s: this.opts.id,
|
|
3560
|
+
key: this.opts.key
|
|
3561
|
+
});
|
|
3562
|
+
traceFlush();
|
|
3328
3563
|
this.startPromise = null;
|
|
3329
3564
|
this.detachedJob?.interrupt();
|
|
3330
3565
|
const proc = this.proc;
|
|
@@ -3371,6 +3606,12 @@ var PtySession = class {
|
|
|
3371
3606
|
detached: true
|
|
3372
3607
|
});
|
|
3373
3608
|
this.proc = proc;
|
|
3609
|
+
trace("start", {
|
|
3610
|
+
s: this.opts.id,
|
|
3611
|
+
key: this.opts.key,
|
|
3612
|
+
argv: this.opts.argv,
|
|
3613
|
+
pid: proc.pid
|
|
3614
|
+
});
|
|
3374
3615
|
proc.stdout.setEncoding("utf8");
|
|
3375
3616
|
proc.stdout.on("data", (d) => {
|
|
3376
3617
|
this.onChunk?.(d);
|
|
@@ -3432,6 +3673,11 @@ var PtySession = class {
|
|
|
3432
3673
|
return;
|
|
3433
3674
|
}
|
|
3434
3675
|
this.state = "dead";
|
|
3676
|
+
trace("dead", {
|
|
3677
|
+
s: this.opts.id,
|
|
3678
|
+
diagTail: this.diag.slice(-500)
|
|
3679
|
+
});
|
|
3680
|
+
traceFlush();
|
|
3435
3681
|
this.startPromise = null;
|
|
3436
3682
|
this.clearIdleTimer();
|
|
3437
3683
|
this.armDeathReaper();
|
|
@@ -3644,50 +3890,20 @@ function parseTargetForm(target) {
|
|
|
3644
3890
|
form: "hub",
|
|
3645
3891
|
machine: segs[0]
|
|
3646
3892
|
};
|
|
3647
|
-
if (segs.length
|
|
3648
|
-
form: "hub-pty",
|
|
3649
|
-
machine: segs[0]
|
|
3650
|
-
};
|
|
3651
|
-
if (segs.length === 3 && segs[1] === "pty") {
|
|
3652
|
-
if (segs[2] === void 0 || segs[2].length === 0) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — empty label after ":pty:"`);
|
|
3653
|
-
return {
|
|
3654
|
-
form: "hub-pty",
|
|
3655
|
-
machine: segs[0],
|
|
3656
|
-
label: segs[2]
|
|
3657
|
-
};
|
|
3658
|
-
}
|
|
3659
|
-
throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — hub PTY is hub:<m4>:pty[:label]`);
|
|
3893
|
+
if (segs.length > 1) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — hub takes a machine id (hub:<m4>); a hub machine's exec opens its default PTY, and handoff detaches it as pty:<id>`);
|
|
3660
3894
|
}
|
|
3661
3895
|
for (const transport of TRANSPORTS) {
|
|
3662
3896
|
if (!target.startsWith(`${transport}:`)) continue;
|
|
3663
3897
|
const rest = target.slice(transport.length + 1);
|
|
3664
3898
|
if (rest.length === 0) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — ${transport} takes a name`);
|
|
3665
|
-
|
|
3666
|
-
|
|
3899
|
+
if (/(^|:)pty(:|$)/.test(rest)) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — the :pty creation suffix is gone; a machine's exec opens its default PTY and handoff detaches it as pty:<id>`);
|
|
3900
|
+
return {
|
|
3667
3901
|
form: "direct",
|
|
3668
3902
|
transport,
|
|
3669
3903
|
name: rest
|
|
3670
3904
|
};
|
|
3671
|
-
const base = rest.slice(0, ptyAt);
|
|
3672
|
-
if (base.length === 0) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — ":pty" needs a transport target before it ("ssh:pty" is the host named pty)`);
|
|
3673
|
-
const tail = rest.slice(ptyAt + 4);
|
|
3674
|
-
if (tail.startsWith(":")) {
|
|
3675
|
-
if (tail.length === 1) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — empty label after ":pty:"`);
|
|
3676
|
-
return {
|
|
3677
|
-
form: "create-pty",
|
|
3678
|
-
transport,
|
|
3679
|
-
name: base,
|
|
3680
|
-
label: tail.slice(1)
|
|
3681
|
-
};
|
|
3682
|
-
}
|
|
3683
|
-
if (tail.length > 0) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — unexpected ${JSON.stringify(tail)} after ":pty"`);
|
|
3684
|
-
return {
|
|
3685
|
-
form: "create-pty",
|
|
3686
|
-
transport,
|
|
3687
|
-
name: base
|
|
3688
|
-
};
|
|
3689
3905
|
}
|
|
3690
|
-
if (target.includes(":")) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: unknown target ${JSON.stringify(target)} — transports are ssh: / docker: / incus: / hub:, the PTY selector is pty:<id|label
|
|
3906
|
+
if (target.includes(":")) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: unknown target ${JSON.stringify(target)} — transports are ssh: / docker: / incus: / hub:, the PTY selector is pty:<id|label>; a machine's exec opens its default PTY and handoff detaches it (the old :pty creation suffix is gone)`);
|
|
3691
3907
|
return {
|
|
3692
3908
|
form: "bare",
|
|
3693
3909
|
name: target
|
|
@@ -3831,7 +4047,11 @@ async function probeTarget(plan, opts = {}) {
|
|
|
3831
4047
|
const runner = opts.runner ?? runOneShot;
|
|
3832
4048
|
const started = Date.now();
|
|
3833
4049
|
const r = await runner(plan.kind === "ssh" ? sshArgv(plan.connection ?? { host: plan.host }, {
|
|
3834
|
-
extraArgs: [
|
|
4050
|
+
extraArgs: [
|
|
4051
|
+
...opts.muxArgvFor?.(plan) ?? [],
|
|
4052
|
+
"-o",
|
|
4053
|
+
`ConnectTimeout=${ct}`
|
|
4054
|
+
],
|
|
3835
4055
|
command: "true"
|
|
3836
4056
|
}) : plan.kind === "docker" ? [
|
|
3837
4057
|
"docker",
|
|
@@ -3890,12 +4110,6 @@ function renderProbe(p) {
|
|
|
3890
4110
|
case "container": return `probe: container ${p.status} (just now, on demand)`;
|
|
3891
4111
|
}
|
|
3892
4112
|
}
|
|
3893
|
-
/** session card(§5.5,`pty:<id|label>` 无 cmd):connection/state/cwd/ttl 由调用方拼头行,这里渲染账本。 */
|
|
3894
|
-
function renderLedger(ledger) {
|
|
3895
|
-
if (ledger.length === 0) return "ledger: empty — no command has run in this session yet";
|
|
3896
|
-
const rows = [...ledger].reverse().map((e) => ` ${e.commandId}${e.jobId !== void 0 && e.jobId !== e.commandId ? ` (job ${e.jobId})` : ""} · ${e.running ? "running" : `exit ${e.exit}`}${e.running ? "" : ` · ${(e.durationMs / 1e3).toFixed(1)}s`} · ${e.summary}`);
|
|
3897
|
-
return `ledger (last ${ledger.length}):\n${rows.join("\n")}`;
|
|
3898
|
-
}
|
|
3899
4113
|
|
|
3900
4114
|
//#endregion
|
|
3901
4115
|
//#region src/remote/ssh-config.ts
|
|
@@ -4046,7 +4260,7 @@ async function scanLocal(opts = {}) {
|
|
|
4046
4260
|
};
|
|
4047
4261
|
}
|
|
4048
4262
|
/** 统一池视图的 roster 呈现:机器三段 + BYO ssh 同位(built on)+ pty 会话段(caller-scoped)。 */
|
|
4049
|
-
function renderRoster(scan, entries, sessions, hubTargets) {
|
|
4263
|
+
function renderRoster(scan, entries, sessions, hubTargets, defaults) {
|
|
4050
4264
|
const lines = [];
|
|
4051
4265
|
if (hubTargets !== null) lines.push(`hub machines: ${hubTargets.map((t) => `hub:${t.id} (${t.label}${t.execEnabled ? "" : "; exec disabled"})`).join(", ") || "none"}`);
|
|
4052
4266
|
if (scan.ssh.error !== void 0) lines.push(`ssh hosts: no readable ~/.ssh/config (${scan.ssh.error})`);
|
|
@@ -4068,7 +4282,7 @@ function renderRoster(scan, entries, sessions, hubTargets) {
|
|
|
4068
4282
|
const id = s.identity;
|
|
4069
4283
|
const snap = s.snapshot;
|
|
4070
4284
|
const state = snap.busy ? "busy" : snap.state === "dead" ? "died" : `idle ${Math.round((snap.idleMs ?? 0) / 1e3)}s`;
|
|
4071
|
-
return `pty:${id.id}${id.label !== void 0 ? ` (label ${id.label})` : ""} ${id.connection}${id.byo ? " (byo-pty)" : ""} [${state}]`;
|
|
4285
|
+
return `pty:${id.id}${id.label !== void 0 ? ` (label ${id.label})` : ""} ${id.connection}${id.byo ? " (byo-pty)" : ""} [${state}${defaults?.get(id.id) !== void 0 ? `; default on ${defaults.get(id.id)}` : ""}]`;
|
|
4072
4286
|
}).join(" · ")}` : "pty sessions: none");
|
|
4073
4287
|
return lines.join("\n");
|
|
4074
4288
|
}
|
|
@@ -4078,7 +4292,9 @@ function renderRoster(scan, entries, sessions, hubTargets) {
|
|
|
4078
4292
|
/** R1:idle TTL 15 min——idle 随活动重置,在用会话永不超时(PtySession 双条件)。 */
|
|
4079
4293
|
const REMOTE_PTY_IDLE_TTL_SEC = 900;
|
|
4080
4294
|
/** P3(user 2026-10-19):每 agent 10 个 PTY 会话,跨所有 connection 统一计;仅 error message 披露。 */
|
|
4081
|
-
|
|
4295
|
+
/** 二/三轮裁决(2026-11-01):每 Agent session 全局 30 个 active PTY(默认/handoff/BYO 全计入);
|
|
4296
|
+
* 仅触达上限时的错误回执与 roster 可见性披露,任何模型可见 surface 不预先公布。 */
|
|
4297
|
+
const REMOTE_PTY_SESSION_LIMIT = 30;
|
|
4082
4298
|
/** hub 线 one-shot 监督披露(0.3.2-c):对端 = 我们自己的 hub server(bash -lc 于独立进程组,
|
|
4083
4299
|
* Node 原生 TERM→2s→KILL + 输出环 + 租约生命周期);superd-run helper 是 dumb-pipe 专用物,
|
|
4084
4300
|
* 从不经 hub 上传(non-goal,docs/10_plans/2026-10-25-hub-remote-parity.md §3 O1/O8)。 */
|
|
@@ -4094,6 +4310,10 @@ const genCommandId = () => `remote-${randomUUID().replace(/-/g, "").slice(0, 16)
|
|
|
4094
4310
|
/** 双轨调度器:参数校验 → 目标语法(parseTargetForm)→ 统一 label 池 → oneshot 子进程 / pty 会话池。 */
|
|
4095
4311
|
var RemoteDriver = class {
|
|
4096
4312
|
pool;
|
|
4313
|
+
/** 默认 PTY 槽(spec remote-default-pty §1):sessionKey → 归一 machineKey → pool key。
|
|
4314
|
+
* 会话本体是普通 PtySession(id 创建即认领,roster 全可见);槽登记在 dispose/TTL 后悬空,
|
|
4315
|
+
* 取槽时经 pool.get(disposed 视同出池)惰性清理。 */
|
|
4316
|
+
defaultSlots = /* @__PURE__ */ new Map();
|
|
4097
4317
|
labels = new LabelPool();
|
|
4098
4318
|
hubOwner = randomUUID();
|
|
4099
4319
|
hubProviders = /* @__PURE__ */ new Set();
|
|
@@ -4106,8 +4326,10 @@ var RemoteDriver = class {
|
|
|
4106
4326
|
runnerOf = /* @__PURE__ */ new WeakMap();
|
|
4107
4327
|
disposal;
|
|
4108
4328
|
ptyIdleTtlSec;
|
|
4329
|
+
mux;
|
|
4109
4330
|
constructor(opts = {}) {
|
|
4110
4331
|
this.opts = opts;
|
|
4332
|
+
this.mux = opts.sshMux ?? new SshMux();
|
|
4111
4333
|
this.ptyIdleTtlSec = opts.idleTtlSec ?? REMOTE_PTY_IDLE_TTL_SEC;
|
|
4112
4334
|
this.pool = new PtyPool({ idleTtlSec: this.ptyIdleTtlSec });
|
|
4113
4335
|
}
|
|
@@ -4146,15 +4368,21 @@ var RemoteDriver = class {
|
|
|
4146
4368
|
const hasSsh = params.ssh !== void 0;
|
|
4147
4369
|
if (hasSpawn && hasSsh) throw new Error("[E_PARAMS] remote: spawn and ssh are two different BYO entry points — pass one, not both");
|
|
4148
4370
|
const ptyForm = form.form === "pty-selector" || form.form === "empty" || form.form === "bare";
|
|
4149
|
-
const sshForm = form.form === "ssh-prefix" || form.form === "empty" || form.form === "bare" || form.form === "direct" && form.transport === "ssh"
|
|
4371
|
+
const sshForm = form.form === "ssh-prefix" || form.form === "empty" || form.form === "bare" || form.form === "direct" && form.transport === "ssh";
|
|
4150
4372
|
if (hasSpawn && !ptyForm) throw new Error("[E_PARAMS] remote: spawn pairs only with a pty target (pty, pty:<label>, or a bare label) — machines are dialed, not spawned");
|
|
4151
|
-
if (hasSsh && !sshForm) throw new Error("[E_PARAMS] remote: ssh registration pairs with ssh-form targets only (ssh, ssh:<label>,
|
|
4373
|
+
if (hasSsh && !sshForm) throw new Error("[E_PARAMS] remote: ssh registration pairs with ssh-form targets only (ssh, ssh:<label>, a bare label, or no target)");
|
|
4152
4374
|
}
|
|
4153
4375
|
async execute(params, form, callCtx) {
|
|
4154
4376
|
const timeoutSec = params.timeout ?? this.opts.execTimeoutSec ?? 120;
|
|
4155
4377
|
const sessionKey = callCtx.sessionKey ?? "no-session";
|
|
4156
4378
|
const hasCmd = params.cmd !== void 0 && params.cmd.length > 0;
|
|
4157
|
-
params.spawn !== void 0 && params.spawn.length;
|
|
4379
|
+
const hasSpawn = params.spawn !== void 0 && params.spawn.length > 0;
|
|
4380
|
+
const bareHitsPty = form.form === "bare" && this.labels.resolve(form.name, sessionKey)?.kind === "pty";
|
|
4381
|
+
if ((form.form === "direct" || form.form === "hub" || form.form === "bare" && !bareHitsPty) && params.mode !== "oneshot" && params.ssh === void 0 && !hasSpawn) {
|
|
4382
|
+
const { session } = await this.defaultSlot(form, sessionKey);
|
|
4383
|
+
if (!hasCmd) return this.cardReceipt(session);
|
|
4384
|
+
return await this.dispatchPty(session, params, callCtx, timeoutSec);
|
|
4385
|
+
}
|
|
4158
4386
|
if (form.form === "hub") {
|
|
4159
4387
|
const hub = this.requireHub();
|
|
4160
4388
|
this.hubProviders.add(hub);
|
|
@@ -4351,7 +4579,7 @@ var RemoteDriver = class {
|
|
|
4351
4579
|
const parsed = parseFileEndpoint(value);
|
|
4352
4580
|
if (!parsed.target) return { path: parsed.path };
|
|
4353
4581
|
const form = parseTargetForm(parsed.target);
|
|
4354
|
-
if (form.form === "hub" || form.form === "
|
|
4582
|
+
if (form.form === "hub" || form.form === "pty-selector") throw new Error("rsync supports SSH endpoints only (plus one local endpoint); no mixed-transport or copy fallback");
|
|
4355
4583
|
const plan = await this.resolveConnectionTarget(parsed.target, callCtx);
|
|
4356
4584
|
this.assertOpen();
|
|
4357
4585
|
combined.throwIfAborted();
|
|
@@ -4393,49 +4621,68 @@ var RemoteDriver = class {
|
|
|
4393
4621
|
const plan = form.transport === "ssh" ? this.sshPlanFor(form.name, callCtx.sessionKey ?? "no-session") : resolveTarget(`${form.transport}:${form.name}`);
|
|
4394
4622
|
this.assertOpen();
|
|
4395
4623
|
signal.throwIfAborted();
|
|
4396
|
-
process$1 = await this.nativeProcess(plan, params, params.timeout, signal);
|
|
4624
|
+
process$1 = await this.nativeProcess(plan, `${form.transport}:${form.name}`, params, params.timeout, signal);
|
|
4397
4625
|
} else if (form.form === "bare") {
|
|
4398
4626
|
const entry = await this.resolvePoolEntry(form.name, callCtx.sessionKey ?? "no-session");
|
|
4399
4627
|
this.assertOpen();
|
|
4400
4628
|
signal.throwIfAborted();
|
|
4401
4629
|
if (entry.kind === "pty" || entry.kind === "hub") throw new Error("remote: background requires an ssh/docker/incus target");
|
|
4402
|
-
process$1 = await this.nativeProcess(this.planOf(entry), params, params.timeout, signal);
|
|
4630
|
+
process$1 = await this.nativeProcess(this.planOf(entry), form.name, params, params.timeout, signal);
|
|
4403
4631
|
} else throw new Error("remote: background oneshot takes a machine target");
|
|
4404
4632
|
this.own(process$1);
|
|
4405
4633
|
if (signal.aborted || this.disposed) process$1.cancel("owner closed during start");
|
|
4406
4634
|
return process$1;
|
|
4407
4635
|
}
|
|
4408
|
-
async nativeProcess(plan, params, timeoutSec, signal = this.fileAbort.signal) {
|
|
4636
|
+
async nativeProcess(plan, display, params, timeoutSec, signal = this.fileAbort.signal) {
|
|
4637
|
+
const muxArgv = this.muxArgvFor(plan, display);
|
|
4409
4638
|
const cleanupTag = randomUUID().slice(0, 8);
|
|
4410
|
-
const helper = await this.ensureHelper(plan);
|
|
4639
|
+
const helper = await this.ensureHelper(plan, muxArgv);
|
|
4411
4640
|
if (helper !== null && !this.disposed) {
|
|
4641
|
+
let killed$1 = false;
|
|
4642
|
+
const kill$1 = () => {
|
|
4643
|
+
if (killed$1) return;
|
|
4644
|
+
killed$1 = true;
|
|
4645
|
+
startOneShot(buildHelperKillgroupArgv(plan, helper.binPath, cleanupTag, muxArgv), { timeoutSec: 15 }).done.catch(() => {});
|
|
4646
|
+
};
|
|
4412
4647
|
const process$2 = startOneShot(buildHelperOneShotArgv(plan, helper.binPath, {
|
|
4413
4648
|
tag: cleanupTag,
|
|
4414
|
-
cmd: params.cmd
|
|
4649
|
+
cmd: params.cmd,
|
|
4650
|
+
...muxArgv !== void 0 ? { muxArgv } : {}
|
|
4415
4651
|
}), {
|
|
4416
4652
|
stdin: params.stdin,
|
|
4417
|
-
timeoutSec
|
|
4653
|
+
timeoutSec,
|
|
4654
|
+
onCancel: kill$1
|
|
4418
4655
|
});
|
|
4419
4656
|
this.runnerOf.set(process$2, `superd-run ${helper.ver} (${helper.triple})`);
|
|
4420
4657
|
process$2.done.then((result) => {
|
|
4421
4658
|
if (!result.cancelled && !result.timedOut) return;
|
|
4422
|
-
|
|
4659
|
+
kill$1();
|
|
4423
4660
|
}).catch(() => {});
|
|
4424
4661
|
const tracked = Promise.resolve(process$2);
|
|
4425
4662
|
this.starting.add(tracked);
|
|
4426
4663
|
tracked.then(() => this.starting.delete(tracked), () => this.starting.delete(tracked));
|
|
4427
4664
|
return tracked;
|
|
4428
4665
|
}
|
|
4429
|
-
|
|
4666
|
+
let killed = false;
|
|
4667
|
+
const kill = () => {
|
|
4668
|
+
if (killed) return;
|
|
4669
|
+
killed = true;
|
|
4670
|
+
const pidfile = `"\${TMPDIR:-/tmp}/superd-oneshot-${cleanupTag}.pgid"`;
|
|
4671
|
+
const cleanup = `p=$(cat ${pidfile} 2>/dev/null) && { kill -TERM -- "-$p" 2>/dev/null; sleep 0.3; kill -KILL -- "-$p" 2>/dev/null; }; rm -f ${pidfile}`;
|
|
4672
|
+
startOneShot((this.opts.oneshotArgvFor ?? buildOneShotArgv)(plan, cleanup, { ...muxArgv !== void 0 ? { muxArgv } : {} }), { timeoutSec: 15 }).done.catch(() => {});
|
|
4673
|
+
};
|
|
4674
|
+
const process$1 = startOneShot((this.opts.oneshotArgvFor ?? buildOneShotArgv)(plan, params.cmd, {
|
|
4675
|
+
cleanupTag,
|
|
4676
|
+
...muxArgv !== void 0 ? { muxArgv } : {}
|
|
4677
|
+
}), {
|
|
4430
4678
|
stdin: params.stdin,
|
|
4431
|
-
timeoutSec
|
|
4679
|
+
timeoutSec,
|
|
4680
|
+
onCancel: kill
|
|
4432
4681
|
});
|
|
4433
4682
|
this.runnerOf.set(process$1, "posix-shim");
|
|
4434
4683
|
process$1.done.then((result) => {
|
|
4435
4684
|
if (!result.cancelled && !result.timedOut) return;
|
|
4436
|
-
|
|
4437
|
-
const cleanup = `p=$(cat ${pidfile} 2>/dev/null) && { kill -TERM -- "-$p" 2>/dev/null; sleep 0.3; kill -KILL -- "-$p" 2>/dev/null; }; rm -f ${pidfile}`;
|
|
4438
|
-
startOneShot((this.opts.oneshotArgvFor ?? buildOneShotArgv)(plan, cleanup, {}), { timeoutSec: 15 }).done.catch(() => {});
|
|
4685
|
+
kill();
|
|
4439
4686
|
}).catch(() => {});
|
|
4440
4687
|
const work = Promise.resolve(process$1);
|
|
4441
4688
|
this.starting.add(work);
|
|
@@ -4444,14 +4691,14 @@ var RemoteDriver = class {
|
|
|
4444
4691
|
}
|
|
4445
4692
|
/** Helper bootstrap is best-effort and sticky: any failure parks null for this driver
|
|
4446
4693
|
* lifetime and the shim stays the path. First call to a target pays probe (+ upload). */
|
|
4447
|
-
async ensureHelper(plan) {
|
|
4694
|
+
async ensureHelper(plan, muxArgv) {
|
|
4448
4695
|
if (this.helpers === void 0) {
|
|
4449
4696
|
const assets = this.opts.helperAssetsFor?.() ?? helperAssets();
|
|
4450
4697
|
this.helpers = assets === null ? null : new HelperRegistry(assets, (argv, options) => this.helperExec(argv, options), buildHelperProbeArgv, buildHelperUploadArgv);
|
|
4451
4698
|
}
|
|
4452
4699
|
if (this.helpers === null || this.disposed) return null;
|
|
4453
4700
|
try {
|
|
4454
|
-
return await this.helpers.ensure(plan);
|
|
4701
|
+
return await this.helpers.ensure(plan, muxArgv ?? []);
|
|
4455
4702
|
} catch {
|
|
4456
4703
|
return null;
|
|
4457
4704
|
}
|
|
@@ -4564,6 +4811,16 @@ var RemoteDriver = class {
|
|
|
4564
4811
|
host: name$1
|
|
4565
4812
|
};
|
|
4566
4813
|
}
|
|
4814
|
+
/** Task E MUX 身份:仅 SSH plan 注入(docker/incus/hub 返回 undefined)。身份键 = 原始
|
|
4815
|
+
* target 串 + 有效连接字段(host/user/port/key/ProxyJump)——同身份共享 master,跨身份
|
|
4816
|
+
* 不串 socket;目录/寿命/拆除全部由 SshMux 自持。 */
|
|
4817
|
+
muxArgvFor(plan, target) {
|
|
4818
|
+
if (plan.kind !== "ssh") return void 0;
|
|
4819
|
+
return this.mux.argvFor({
|
|
4820
|
+
target,
|
|
4821
|
+
connection: plan.connection ?? { host: plan.host }
|
|
4822
|
+
});
|
|
4823
|
+
}
|
|
4567
4824
|
/** BYO ssh 注册(v2:无 spec 对比——label 占用即拒;注册成功才认领)。返回实际名(label 或 s-id)。 */
|
|
4568
4825
|
registerByoSsh(label, connection, sessionKey) {
|
|
4569
4826
|
this.assertOpen();
|
|
@@ -4593,7 +4850,8 @@ var RemoteDriver = class {
|
|
|
4593
4850
|
const live = this.pool.list().filter(({ key: key$1 }) => key$1.startsWith(`${sessionKey}|`)).map(({ key: key$1 }) => this.pool.get(key$1)).filter((s) => s !== void 0 && s.sessionState !== "dead");
|
|
4594
4851
|
if (live.length >= REMOTE_PTY_SESSION_LIMIT) {
|
|
4595
4852
|
const listing = live.map(sessionLine).join(" · ");
|
|
4596
|
-
throw new Error(`[E_SESSION_LIMIT] remote: this Agent already holds ${live.length} live PTY sessions
|
|
4853
|
+
throw new Error(`[E_SESSION_LIMIT] remote: this Agent already holds ${live.length} live PTY sessions — the per-Agent session global limit (${REMOTE_PTY_SESSION_LIMIT}) is reached. Check the live sessions below (or {action:"status"} for the roster), then close one ({action:"close", target:"pty:<id>" or the machine name}), kill its running job (job_kill), reuse a session, or run {mode:"oneshot"} instead.
|
|
4854
|
+
live: ${listing}`);
|
|
4597
4855
|
}
|
|
4598
4856
|
const { label } = opts;
|
|
4599
4857
|
if (label !== void 0) validateLabel(label);
|
|
@@ -4642,7 +4900,7 @@ var RemoteDriver = class {
|
|
|
4642
4900
|
isBuiltOn(connection, sessionKey) {
|
|
4643
4901
|
return connection.startsWith("ssh:") && this.labels.resolve(connection.slice(4), sessionKey)?.kind === "ssh-byo";
|
|
4644
4902
|
}
|
|
4645
|
-
/** PTY 创建形唯一实现(execute 与 background 预建共用):spawn BYO
|
|
4903
|
+
/** PTY 创建形唯一实现(execute 与 background 预建共用):spawn BYO(spec 2026-11-01:native `:pty`/hub-pty 创建语法已删,machine 会话经默认槽 + handoff)。 */
|
|
4646
4904
|
createPtyLane(params, form, sessionKey) {
|
|
4647
4905
|
if (params.spawn !== void 0 && params.spawn.length > 0) {
|
|
4648
4906
|
const label = form.form === "pty-selector" && form.idOrLabel !== void 0 ? form.idOrLabel : form.form === "bare" ? form.name : void 0;
|
|
@@ -4655,42 +4913,246 @@ var RemoteDriver = class {
|
|
|
4655
4913
|
origin: params.spawn
|
|
4656
4914
|
}, sessionKey) };
|
|
4657
4915
|
}
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4916
|
+
}
|
|
4917
|
+
/** 池条目的槽键归一:同一实体(host/container/machine 身份)跨裸名/前缀/撞名前缀共享同一默认槽。 */
|
|
4918
|
+
machineKeyOfEntry(entry) {
|
|
4919
|
+
switch (entry.kind) {
|
|
4920
|
+
case "ssh": return `ssh:${entry.host}`;
|
|
4921
|
+
case "ssh-byo": return `byo:${entry.name}`;
|
|
4922
|
+
case "docker": return `docker:${entry.container}`;
|
|
4923
|
+
case "incus": return `incus:${entry.container}`;
|
|
4924
|
+
case "hub": return `hub:${entry.machine}`;
|
|
4925
|
+
case "pty": throw new Error("remote: a PTY session is not a machine slot");
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
/** 默认槽的 machine 规格:归一 key + 创建 argv + 展示 connection(hub 每次经 ptyEndpoint 重验 pairing,P10)。 */
|
|
4929
|
+
async machineSlotSpec(form, sessionKey) {
|
|
4930
|
+
if (form.form === "hub") {
|
|
4931
|
+
const hub = this.requireHub();
|
|
4932
|
+
this.hubProviders.add(hub);
|
|
4933
|
+
const endpoint = hub.ptyEndpoint(form.machine);
|
|
4934
|
+
return {
|
|
4935
|
+
key: `hub:${form.machine}`,
|
|
4936
|
+
argv: this.opts.hubPtyArgvFor !== void 0 ? this.opts.hubPtyArgvFor(endpoint) : buildHubPtyArgv(endpoint, hubPtyAssets()),
|
|
4937
|
+
connection: `hub:${form.machine}`
|
|
4665
4938
|
};
|
|
4666
|
-
|
|
4939
|
+
}
|
|
4940
|
+
if (form.form === "direct") {
|
|
4941
|
+
if (form.transport === "ssh") {
|
|
4942
|
+
const entry = this.labels.resolve(form.name, sessionKey);
|
|
4943
|
+
if (entry?.kind === "ssh-byo") {
|
|
4944
|
+
const connection = entry.connection;
|
|
4945
|
+
return {
|
|
4946
|
+
key: `byo:${entry.name}`,
|
|
4947
|
+
argv: (this.opts.ptyArgvFor ?? buildPtyArgv)({
|
|
4948
|
+
kind: "ssh",
|
|
4949
|
+
host: connection.host,
|
|
4950
|
+
connection
|
|
4951
|
+
}),
|
|
4952
|
+
connection: `ssh:${entry.name}`,
|
|
4953
|
+
builtOn: true
|
|
4954
|
+
};
|
|
4955
|
+
}
|
|
4956
|
+
return {
|
|
4957
|
+
key: `ssh:${form.name}`,
|
|
4958
|
+
argv: (this.opts.ptyArgvFor ?? buildPtyArgv)({
|
|
4959
|
+
kind: "ssh",
|
|
4960
|
+
host: form.name
|
|
4961
|
+
}),
|
|
4962
|
+
connection: `ssh:${form.name}`
|
|
4963
|
+
};
|
|
4964
|
+
}
|
|
4965
|
+
const plan = resolveTarget(`${form.transport}:${form.name}`);
|
|
4966
|
+
return {
|
|
4967
|
+
key: `${form.transport}:${form.name}`,
|
|
4667
4968
|
argv: (this.opts.ptyArgvFor ?? buildPtyArgv)(plan),
|
|
4668
|
-
connection:
|
|
4669
|
-
|
|
4670
|
-
...form.label !== void 0 ? { label: form.label } : {}
|
|
4671
|
-
}, sessionKey) };
|
|
4969
|
+
connection: `${form.transport}:${form.name}`
|
|
4970
|
+
};
|
|
4672
4971
|
}
|
|
4673
|
-
if (form.form === "
|
|
4674
|
-
const
|
|
4675
|
-
|
|
4972
|
+
if (form.form === "bare") {
|
|
4973
|
+
const entry = await this.resolvePoolEntry(form.name, sessionKey);
|
|
4974
|
+
this.assertOpen();
|
|
4975
|
+
if (entry.kind === "hub") return this.machineSlotSpec({
|
|
4976
|
+
form: "hub",
|
|
4977
|
+
machine: entry.machine
|
|
4978
|
+
}, sessionKey);
|
|
4979
|
+
if (entry.kind === "pty") throw new Error("[E_INTERNAL] remote: bare pty entries route before machine slots");
|
|
4980
|
+
const key = this.machineKeyOfEntry(entry);
|
|
4981
|
+
if (entry.kind === "ssh") return {
|
|
4982
|
+
key,
|
|
4983
|
+
argv: (this.opts.ptyArgvFor ?? buildPtyArgv)({
|
|
4984
|
+
kind: "ssh",
|
|
4985
|
+
host: entry.host
|
|
4986
|
+
}),
|
|
4987
|
+
connection: `ssh:${entry.name}`
|
|
4988
|
+
};
|
|
4989
|
+
if (entry.kind === "ssh-byo") return {
|
|
4990
|
+
key,
|
|
4991
|
+
argv: (this.opts.ptyArgvFor ?? buildPtyArgv)({
|
|
4992
|
+
kind: "ssh",
|
|
4993
|
+
host: entry.connection.host,
|
|
4994
|
+
connection: entry.connection
|
|
4995
|
+
}),
|
|
4996
|
+
connection: `ssh:${entry.name}`,
|
|
4997
|
+
builtOn: true
|
|
4998
|
+
};
|
|
4999
|
+
const plan = entry.kind === "docker" ? resolveTarget(`docker:${entry.container}`) : resolveTarget(`incus:${entry.container}`);
|
|
5000
|
+
return {
|
|
5001
|
+
key,
|
|
4676
5002
|
argv: (this.opts.ptyArgvFor ?? buildPtyArgv)(plan),
|
|
4677
|
-
connection: `${
|
|
4678
|
-
|
|
4679
|
-
...form.label !== void 0 ? { label: form.label } : {}
|
|
4680
|
-
}, sessionKey) };
|
|
5003
|
+
connection: `${entry.kind === "docker" ? "docker" : "incus"}:${entry.name}`
|
|
5004
|
+
};
|
|
4681
5005
|
}
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
5006
|
+
throw new Error(`[E_TARGET_FORMAT] remote: a machine target is required (ssh:<name>, docker:<name>, incus:<name>, hub:<m4>, or a bare pool name) — got form ${JSON.stringify(form.form)}`);
|
|
5007
|
+
}
|
|
5008
|
+
/** 取/懒建 machine 的默认 PTY 槽:dead/disposed 槽惰性重建;id 创建即认领(roster 全可见 + 防撞)。 */
|
|
5009
|
+
async defaultSlot(form, sessionKey) {
|
|
5010
|
+
const spec = await this.machineSlotSpec(form, sessionKey);
|
|
5011
|
+
this.assertOpen();
|
|
5012
|
+
const byKey = this.defaultSlots.get(sessionKey) ?? /* @__PURE__ */ new Map();
|
|
5013
|
+
this.defaultSlots.set(sessionKey, byKey);
|
|
5014
|
+
const existingKey = byKey.get(spec.key);
|
|
5015
|
+
if (existingKey !== void 0) {
|
|
5016
|
+
const existing = this.pool.get(existingKey);
|
|
5017
|
+
if (existing !== void 0) return {
|
|
5018
|
+
session: existing,
|
|
5019
|
+
machineKey: spec.key,
|
|
5020
|
+
isNew: false
|
|
5021
|
+
};
|
|
5022
|
+
byKey.delete(spec.key);
|
|
5023
|
+
}
|
|
5024
|
+
const session = this.createSession({
|
|
5025
|
+
argv: spec.argv,
|
|
5026
|
+
connection: spec.connection,
|
|
5027
|
+
byo: false,
|
|
5028
|
+
...spec.builtOn === true ? { builtOn: true } : {}
|
|
5029
|
+
}, sessionKey);
|
|
5030
|
+
byKey.set(spec.key, session.key);
|
|
5031
|
+
return {
|
|
5032
|
+
session,
|
|
5033
|
+
machineKey: spec.key,
|
|
5034
|
+
isNew: true
|
|
5035
|
+
};
|
|
5036
|
+
}
|
|
5037
|
+
releaseDefaultSlot(machineKey, sessionKey) {
|
|
5038
|
+
this.defaultSlots.get(sessionKey)?.delete(machineKey);
|
|
5039
|
+
}
|
|
5040
|
+
/** 按池键反查并解绑默认槽(background 提交即 handoff 用);非槽会话无操作。 */
|
|
5041
|
+
releaseSlotByPoolKey(poolKey, sessionKey) {
|
|
5042
|
+
const byKey = this.defaultSlots.get(sessionKey);
|
|
5043
|
+
if (byKey === void 0) return;
|
|
5044
|
+
for (const [machineKey, key] of byKey) if (key === poolKey) {
|
|
5045
|
+
byKey.delete(machineKey);
|
|
5046
|
+
return;
|
|
5047
|
+
}
|
|
5048
|
+
}
|
|
5049
|
+
/** roster/status 面:本 session 的默认槽标注(session id → machineKey;悬空条目过滤)。 */
|
|
5050
|
+
defaultMarks(sessionKey) {
|
|
5051
|
+
const marks = /* @__PURE__ */ new Map();
|
|
5052
|
+
const byKey = this.defaultSlots.get(sessionKey);
|
|
5053
|
+
if (byKey === void 0) return marks;
|
|
5054
|
+
for (const [machineKey, poolKey] of byKey) {
|
|
5055
|
+
const s = this.pool.get(poolKey);
|
|
5056
|
+
if (s !== void 0) marks.set(s.identity.id, machineKey);
|
|
5057
|
+
}
|
|
5058
|
+
return marks;
|
|
5059
|
+
}
|
|
5060
|
+
/** spec §1 handoff(2026-11-01 二/三轮裁决):把 machine 默认 PTY 解绑成可寻址 pty:<id>——同引用移动
|
|
5061
|
+
* (不重启、不复制输出流);fresh(尚无默认槽)= 后台创建空闲 shell 再隔离(默认 PTY 思维模型恒
|
|
5062
|
+
* ready,创建时机是实现细节);busy = 附当前命令与 job 句柄。machine 下次 exec 按需新建默认 PTY。 */
|
|
5063
|
+
async handoff(target, callCtx = {}) {
|
|
5064
|
+
this.assertOpen();
|
|
5065
|
+
const sessionKey = callCtx.sessionKey ?? "no-session";
|
|
5066
|
+
const form = parseTargetForm(target);
|
|
5067
|
+
if (form.form !== "direct" && form.form !== "bare" && form.form !== "hub") throw new Error(`[E_TARGET_FORMAT] remote: handoff detaches a machine's default PTY — pass a machine target (ssh:dev4, docker:c, hub:<m4>, or a bare pool name); addressable sessions are already detached, and ${JSON.stringify(target)} is not a machine`);
|
|
5068
|
+
const spec = await this.machineSlotSpec(form, sessionKey);
|
|
5069
|
+
this.assertOpen();
|
|
5070
|
+
let session;
|
|
5071
|
+
const poolKey = this.defaultSlots.get(sessionKey)?.get(spec.key);
|
|
5072
|
+
if (poolKey !== void 0) session = this.pool.get(poolKey);
|
|
5073
|
+
let created = false;
|
|
5074
|
+
if (session === void 0) {
|
|
5075
|
+
this.releaseDefaultSlot(spec.key, sessionKey);
|
|
5076
|
+
session = this.createSession({
|
|
5077
|
+
argv: spec.argv,
|
|
5078
|
+
connection: spec.connection,
|
|
4690
5079
|
byo: false,
|
|
4691
|
-
...
|
|
4692
|
-
}, sessionKey)
|
|
5080
|
+
...spec.builtOn === true ? { builtOn: true } : {}
|
|
5081
|
+
}, sessionKey);
|
|
5082
|
+
created = true;
|
|
5083
|
+
await session.warm();
|
|
5084
|
+
}
|
|
5085
|
+
this.releaseDefaultSlot(spec.key, sessionKey);
|
|
5086
|
+
const id = session.identity;
|
|
5087
|
+
const snap = session.snapshot;
|
|
5088
|
+
const activeCmd = session.active;
|
|
5089
|
+
const lines = [
|
|
5090
|
+
`handed off pty:${id.id}${id.label !== void 0 ? ` (label ${id.label})` : ""} — ${id.connection}${id.builtOn ? " (built on)" : ""}`,
|
|
5091
|
+
created ? "fresh idle shell created and detached" : snap.state === "dead" ? "session died — its next dispatch reconnects a fresh shell" : "detached",
|
|
5092
|
+
`this machine's next exec opens a fresh default PTY`
|
|
5093
|
+
];
|
|
5094
|
+
if (activeCmd !== null) lines.push(`still running: command ${activeCmd.commandId}${activeCmd.jobId !== void 0 ? ` — follow with job_output ${activeCmd.jobId}, cancel with job_kill` : " — a synchronous exec settles into its receipt; a timeout handoff is adoptable via job_output"}`);
|
|
5095
|
+
return {
|
|
5096
|
+
mode: "pty",
|
|
5097
|
+
target: `pty:${id.id}`,
|
|
5098
|
+
session: `pty:${id.id}`,
|
|
5099
|
+
...id.label !== void 0 ? { label: id.label } : {},
|
|
5100
|
+
connection: id.connection,
|
|
5101
|
+
exit: null,
|
|
5102
|
+
cwd: session.lastCwd,
|
|
5103
|
+
timedOut: false,
|
|
5104
|
+
reconnected: false,
|
|
5105
|
+
stdout: lines.join("\n"),
|
|
5106
|
+
durationMs: 0,
|
|
5107
|
+
...created ? { created: true } : {},
|
|
5108
|
+
...activeCmd !== null ? {
|
|
5109
|
+
running: true,
|
|
5110
|
+
commandId: activeCmd.commandId
|
|
5111
|
+
} : {},
|
|
5112
|
+
...this.ptyTtl(session.key)
|
|
5113
|
+
};
|
|
5114
|
+
}
|
|
5115
|
+
/** close 统一入口(spec §1 + 三轮补充裁决):machine target 只关默认槽;pty selector 关该会话;
|
|
5116
|
+
* 不存在 job 定位——取消运行中命令的唯一入口是官方 job_kill。 */
|
|
5117
|
+
async close(target, sessionKey, force = false) {
|
|
5118
|
+
this.assertOpen();
|
|
5119
|
+
const form = parseTargetForm(target);
|
|
5120
|
+
const bareIsPty = form.form === "bare" && this.labels.resolve(form.name, sessionKey)?.kind === "pty";
|
|
5121
|
+
if (form.form === "direct" || form.form === "hub" || form.form === "bare" && !bareIsPty) return this.closeDefault(form, sessionKey, force);
|
|
5122
|
+
if (form.form === "pty-selector") {
|
|
5123
|
+
if (form.idOrLabel === void 0) throw new Error("[E_PARAMS] remote: \"pty\" alone selects nothing — append an id/label (pty:pty-1a2b, pty:build)");
|
|
5124
|
+
return this.closePty(form.idOrLabel, sessionKey, force);
|
|
5125
|
+
}
|
|
5126
|
+
if (form.form === "bare") return this.closePty(form.name, sessionKey, force);
|
|
5127
|
+
throw new Error(`[E_TARGET_FORMAT] remote: close takes a machine target (closes its default PTY) or pty:<id|label> — got ${JSON.stringify(target)}`);
|
|
5128
|
+
}
|
|
5129
|
+
async closeDefault(form, sessionKey, force) {
|
|
5130
|
+
const spec = await this.machineSlotSpec(form, sessionKey);
|
|
5131
|
+
this.assertOpen();
|
|
5132
|
+
const poolKey = this.defaultSlots.get(sessionKey)?.get(spec.key);
|
|
5133
|
+
if (poolKey === void 0) return {
|
|
5134
|
+
session: spec.key,
|
|
5135
|
+
connection: spec.connection,
|
|
5136
|
+
closed: false,
|
|
5137
|
+
reason: "not-found",
|
|
5138
|
+
killed: [],
|
|
5139
|
+
message: `no default PTY on ${spec.key} — a machine's first exec opens one`
|
|
5140
|
+
};
|
|
5141
|
+
const session = this.pool.get(poolKey);
|
|
5142
|
+
if (session === void 0) {
|
|
5143
|
+
this.releaseDefaultSlot(spec.key, sessionKey);
|
|
5144
|
+
return {
|
|
5145
|
+
session: spec.key,
|
|
5146
|
+
connection: spec.connection,
|
|
5147
|
+
closed: false,
|
|
5148
|
+
reason: "already-closed-or-reaped",
|
|
5149
|
+
killed: [],
|
|
5150
|
+
message: `the default PTY on ${spec.key} was already closed or reaped after ${this.ttlMinutes()} min idle`
|
|
5151
|
+
};
|
|
4693
5152
|
}
|
|
5153
|
+
const r = this.closePty(session.identity.id, sessionKey, force);
|
|
5154
|
+
if (r.closed) this.releaseDefaultSlot(spec.key, sessionKey);
|
|
5155
|
+
return r;
|
|
4694
5156
|
}
|
|
4695
5157
|
/** background PTY lane:会话先占位(不拨号)再进官方 jobs registry——首回执必须能寻址;
|
|
4696
5158
|
* registry 拒绝时把新建的会话拆掉(复用形的会话绝不拆)。返回 undefined = 走普通 oneshot job。 */
|
|
@@ -4702,9 +5164,9 @@ var RemoteDriver = class {
|
|
|
4702
5164
|
const form = parseTargetForm(prepared.target);
|
|
4703
5165
|
const target = (id$1) => ({
|
|
4704
5166
|
target: `pty:${id$1}`,
|
|
4705
|
-
cmd:
|
|
4706
|
-
stdin:
|
|
4707
|
-
timeout:
|
|
5167
|
+
cmd: prepared.cmd,
|
|
5168
|
+
stdin: prepared.stdin,
|
|
5169
|
+
timeout: prepared.timeout,
|
|
4708
5170
|
mode: "pty"
|
|
4709
5171
|
});
|
|
4710
5172
|
const lane = this.createPtyLane(prepared, form, sessionKey);
|
|
@@ -4719,6 +5181,20 @@ var RemoteDriver = class {
|
|
|
4719
5181
|
cleanup: () => void 0
|
|
4720
5182
|
};
|
|
4721
5183
|
}
|
|
5184
|
+
if (lane === void 0 && (form.form === "direct" || form.form === "hub" || form.form === "bare" && this.labels.resolve(form.name, sessionKey)?.kind !== "pty") && prepared.mode !== "oneshot") {
|
|
5185
|
+
const slot = await this.defaultSlot(form, sessionKey);
|
|
5186
|
+
this.assertOpen();
|
|
5187
|
+
const id$1 = slot.session.identity;
|
|
5188
|
+
return {
|
|
5189
|
+
input: target(id$1.id),
|
|
5190
|
+
session: `pty:${id$1.id}`,
|
|
5191
|
+
connection: id$1.connection,
|
|
5192
|
+
...id$1.label !== void 0 ? { label: id$1.label } : {},
|
|
5193
|
+
cleanup: () => {
|
|
5194
|
+
if (slot.isNew) slot.session.dispose();
|
|
5195
|
+
}
|
|
5196
|
+
};
|
|
5197
|
+
}
|
|
4722
5198
|
if (lane === void 0) return void 0;
|
|
4723
5199
|
const id = lane.session.identity;
|
|
4724
5200
|
return {
|
|
@@ -4778,24 +5254,26 @@ var RemoteDriver = class {
|
|
|
4778
5254
|
this.pool.get(key)?.attachJob(commandId, jobId);
|
|
4779
5255
|
}
|
|
4780
5256
|
}
|
|
4781
|
-
/** §
|
|
4782
|
-
* 追加 stdin
|
|
4783
|
-
*
|
|
4784
|
-
* 一律明确拒绝并给出可恢复指引。回执附 connection/label 供工具层渲染。 */
|
|
5257
|
+
/** spec remote-default-pty §1(2026-11-01):exec stdin-only 写入核心——向已有 live PTY 的当前运行命令
|
|
5258
|
+
* 追加 exact-byte stdin。寻址:pty:<id|label> / 裸 label 命中 pty 项 / machine target(其默认槽的
|
|
5259
|
+
* 当前命令,无槽不懒建、如实报错)。不触发拨号、创建或重连;回执附 connection/label 供工具层渲染。 */
|
|
4785
5260
|
async input(target, options, callCtx = {}) {
|
|
4786
5261
|
this.assertOpen();
|
|
4787
5262
|
const sessionKey = callCtx.sessionKey ?? "no-session";
|
|
4788
5263
|
const form = parseTargetForm(target);
|
|
4789
|
-
let
|
|
5264
|
+
let session;
|
|
4790
5265
|
if (form.form === "pty-selector") {
|
|
4791
5266
|
if (form.idOrLabel === void 0) throw new Error("[E_PARAMS] remote: \"pty\" alone selects nothing — append an id/label (pty:pty-1a2b, pty:build)");
|
|
4792
|
-
|
|
4793
|
-
} else if (form.form === "bare")
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
5267
|
+
session = this.resolvePty(form.idOrLabel, sessionKey);
|
|
5268
|
+
} else if (form.form === "bare" && this.labels.resolve(form.name, sessionKey)?.kind === "pty") session = this.resolvePty(form.name, sessionKey);
|
|
5269
|
+
else if (form.form === "direct" || form.form === "hub" || form.form === "bare") {
|
|
5270
|
+
const spec = await this.machineSlotSpec(form, sessionKey);
|
|
5271
|
+
this.assertOpen();
|
|
5272
|
+
const poolKey = this.defaultSlots.get(sessionKey)?.get(spec.key);
|
|
5273
|
+
const slot = poolKey !== void 0 ? this.pool.get(poolKey) : void 0;
|
|
5274
|
+
if (slot === void 0) throw new Error(`[E_SESSION_NOT_RUNNING] remote: no default PTY on ${spec.key} yet — start a command first ({action:"exec", target:"${spec.key}", cmd:…}); a raw stdin write goes to the command running there`);
|
|
5275
|
+
session = slot;
|
|
5276
|
+
} else throw new Error(`[E_TARGET_FORMAT] remote: a raw stdin write goes into a live PTY session — pass pty:<id|label>, a bare session label, or a machine target (its default PTY); got ${JSON.stringify(target)}`);
|
|
4799
5277
|
const receipt = await session.input(options);
|
|
4800
5278
|
const id = session.identity;
|
|
4801
5279
|
return {
|
|
@@ -4821,6 +5299,7 @@ var RemoteDriver = class {
|
|
|
4821
5299
|
jobId: commandId
|
|
4822
5300
|
} : {}
|
|
4823
5301
|
});
|
|
5302
|
+
if (callCtx.ptyBackground === true && r.running === true) this.releaseSlotByPoolKey(session.key, callCtx.sessionKey ?? "no-session");
|
|
4824
5303
|
const out = disclose(r.output, r.outputStats?.stdout);
|
|
4825
5304
|
const id = session.identity;
|
|
4826
5305
|
return {
|
|
@@ -4884,14 +5363,14 @@ var RemoteDriver = class {
|
|
|
4884
5363
|
cwd: session.lastCwd,
|
|
4885
5364
|
timedOut: false,
|
|
4886
5365
|
reconnected: false,
|
|
4887
|
-
stdout: [head,
|
|
5366
|
+
stdout: [head, activeLine(session)].join("\n"),
|
|
4888
5367
|
durationMs: 0,
|
|
4889
5368
|
...this.ptyTtl(session.key)
|
|
4890
5369
|
};
|
|
4891
5370
|
}
|
|
4892
5371
|
async oneshot(plan, display, params, timeoutSec) {
|
|
4893
5372
|
this.assertOpen();
|
|
4894
|
-
const process$1 = this.own(await this.nativeProcess(plan, params, timeoutSec));
|
|
5373
|
+
const process$1 = this.own(await this.nativeProcess(plan, display, params, timeoutSec));
|
|
4895
5374
|
if (this.disposed) process$1.cancel("owner disposed");
|
|
4896
5375
|
const result = await process$1.done;
|
|
4897
5376
|
const stdout = process$1.read("stdout", 0), stderr = process$1.read("stderr", 0);
|
|
@@ -4977,17 +5456,20 @@ var RemoteDriver = class {
|
|
|
4977
5456
|
return s === void 0 ? null : s;
|
|
4978
5457
|
}).filter((s) => s !== null);
|
|
4979
5458
|
const hub = this.opts.getHub?.();
|
|
4980
|
-
return renderRoster(scan, this.labels.list(callCtx.sessionKey ?? "no-session"), sessions, hub === void 0 ? null : hub.listTargets());
|
|
5459
|
+
return renderRoster(scan, this.labels.list(callCtx.sessionKey ?? "no-session"), sessions, hub === void 0 ? null : hub.listTargets(), this.defaultMarks(callCtx.sessionKey ?? "no-session"));
|
|
4981
5460
|
}
|
|
4982
5461
|
/** status probe(target 无 cmd):机器面 = probe + 该 connection 上的会话;pty selector = session card。 */
|
|
4983
5462
|
async status(target, callCtx = {}) {
|
|
4984
5463
|
this.assertOpen();
|
|
4985
5464
|
const sessionKey = callCtx.sessionKey ?? "no-session";
|
|
4986
5465
|
const form = parseTargetForm(target);
|
|
4987
|
-
if (form.form === "hub"
|
|
5466
|
+
if (form.form === "hub") {
|
|
4988
5467
|
const row = this.requireHub().listTargets().find((t) => t.id === form.machine);
|
|
4989
5468
|
if (!row) throw new Error(`unknown remote machine ${form.machine} — select a remote machine from the Hub roster; local execution is not available`);
|
|
4990
|
-
|
|
5469
|
+
const hubSlotKey = `hub:${form.machine}`;
|
|
5470
|
+
const hubSlotPoolKey = this.defaultSlots.get(sessionKey)?.get(hubSlotKey);
|
|
5471
|
+
const hubSlot = hubSlotPoolKey !== void 0 ? this.pool.get(hubSlotPoolKey) : void 0;
|
|
5472
|
+
return [`hub:${form.machine} — Hub remote machine (${row.label}); exec ${row.execEnabled ? "enabled" : "disabled"}; not probed`, hubSlot === void 0 ? `default PTY: none — this machine's first exec opens one` : `default PTY: ${sessionLine(hubSlot)}`].join("\n");
|
|
4991
5473
|
}
|
|
4992
5474
|
if (form.form === "pty-selector") {
|
|
4993
5475
|
if (form.idOrLabel === void 0) throw new Error("[E_PARAMS] remote: \"pty\" alone selects nothing — append an id/label (pty:pty-1a2b, pty:build)");
|
|
@@ -5005,15 +5487,23 @@ var RemoteDriver = class {
|
|
|
5005
5487
|
const plan = this.planOf(entry);
|
|
5006
5488
|
const head = `${target} — ${plan.kind === "ssh" ? "ssh host" : `${plan.kind} container`}${entry.kind === "ssh-byo" ? " (built on)" : ""}`;
|
|
5007
5489
|
const runner = this.opts.probeRunner ?? runOneShot;
|
|
5008
|
-
const probe = await probeTarget(plan, {
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5490
|
+
const probe = await probeTarget(plan, {
|
|
5491
|
+
runner: (argv, options) => {
|
|
5492
|
+
this.assertOpen();
|
|
5493
|
+
return runner(argv, options);
|
|
5494
|
+
},
|
|
5495
|
+
muxArgvFor: (p) => this.muxArgvFor(p, target) ?? []
|
|
5496
|
+
});
|
|
5012
5497
|
const prefix = `${sessionKey}|`;
|
|
5013
5498
|
const onConnection = this.pool.list().filter(({ key }) => key.startsWith(prefix)).map(({ key }) => this.pool.get(key)).filter((s) => s !== void 0 && s.identity.connection === `${plan.kind === "ssh" ? "ssh" : plan.kind}:${name$1}`);
|
|
5499
|
+
const slotKey = this.machineKeyOfEntry(entry);
|
|
5500
|
+
const slotPoolKey = this.defaultSlots.get(sessionKey)?.get(slotKey);
|
|
5501
|
+
const slotSession = slotPoolKey !== void 0 ? this.pool.get(slotPoolKey) : void 0;
|
|
5502
|
+
const slotLine = slotSession === void 0 ? `default PTY: none — this machine's first exec opens one` : `default PTY: ${sessionLine(slotSession)}`;
|
|
5014
5503
|
return [
|
|
5015
5504
|
head,
|
|
5016
5505
|
renderProbe(probe),
|
|
5506
|
+
slotLine,
|
|
5017
5507
|
onConnection.length > 0 ? `pty sessions on this connection: ${onConnection.map(sessionLine).join(" · ")}` : "pty sessions on this connection: none"
|
|
5018
5508
|
].join("\n");
|
|
5019
5509
|
}
|
|
@@ -5028,8 +5518,7 @@ var RemoteDriver = class {
|
|
|
5028
5518
|
this.assertOpen();
|
|
5029
5519
|
const form = parseTargetForm(target);
|
|
5030
5520
|
if (form.form === "pty-selector") throw new Error("remote: a PTY session is not a filesystem connection");
|
|
5031
|
-
if (form.form === "hub"
|
|
5032
|
-
if (form.form === "create-pty") throw new Error("remote: a PTY session is not a filesystem connection");
|
|
5521
|
+
if (form.form === "hub") throw new Error("remote: hub endpoints resolve through the Hub file lane, not a CLI connection");
|
|
5033
5522
|
if (form.form === "direct") return form.transport === "ssh" ? this.sshPlanFor(form.name, callCtx.sessionKey ?? "no-session") : resolveTarget(`${form.transport}:${form.name}`);
|
|
5034
5523
|
if (form.form === "ssh-prefix" || form.form === "empty") throw new Error("[E_TARGET_FORMAT] remote: connection targets need a name");
|
|
5035
5524
|
const entry = await this.resolvePoolEntry(form.name, callCtx.sessionKey ?? "no-session");
|
|
@@ -5059,6 +5548,7 @@ var RemoteDriver = class {
|
|
|
5059
5548
|
if (this.disposal) return this.disposal;
|
|
5060
5549
|
this.disposed = true;
|
|
5061
5550
|
this.labels.clear();
|
|
5551
|
+
this.defaultSlots.clear();
|
|
5062
5552
|
this.fileAbort.abort(/* @__PURE__ */ new Error("remote owner disposed"));
|
|
5063
5553
|
for (const process$1 of this.processes) process$1.cancel("remote owner disposed");
|
|
5064
5554
|
return this.disposal = (async () => {
|
|
@@ -5069,12 +5559,18 @@ var RemoteDriver = class {
|
|
|
5069
5559
|
...[...this.hubProviders].map((hub) => hub.releaseOwner(this.hubOwner))
|
|
5070
5560
|
]);
|
|
5071
5561
|
this.hubProviders.clear();
|
|
5562
|
+
await this.mux.dispose();
|
|
5072
5563
|
})();
|
|
5073
5564
|
}
|
|
5074
5565
|
};
|
|
5075
5566
|
/** direct 形态回退到 resolveTarget 可解析的名字(ssh:dev4 → dev4)。 */
|
|
5076
5567
|
function directName(form) {
|
|
5077
|
-
return form.form === "direct" ? form.name :
|
|
5568
|
+
return form.form === "direct" ? form.name : "";
|
|
5569
|
+
}
|
|
5570
|
+
/** session card 的当前命令行(spec 2026-11-01:历史 ledger 已删,只报活动命令)。 */
|
|
5571
|
+
function activeLine(s) {
|
|
5572
|
+
const a = s.active;
|
|
5573
|
+
return a === null ? "no command running" : `running: ${a.commandId}${a.jobId !== void 0 ? ` (job ${a.jobId})` : ""} — feed it stdin with exec stdin-only, follow job_output, cancel job_kill`;
|
|
5078
5574
|
}
|
|
5079
5575
|
/** roster/status 一行的会话描述:`p-ab12 (label build) ssh:dev4 idle 12s`。 */
|
|
5080
5576
|
function sessionLine(s) {
|
|
@@ -5109,7 +5605,8 @@ const ptyOutcome = (aborted, result) => {
|
|
|
5109
5605
|
function startRemoteJob(registry, input, exec, driver) {
|
|
5110
5606
|
if (!exec.agent) throw new Error("remote: background jobs require a live Agent owner");
|
|
5111
5607
|
const form = parseTargetForm(input.target);
|
|
5112
|
-
|
|
5608
|
+
const machineForm = form.form === "direct" || form.form === "bare" || form.form === "hub";
|
|
5609
|
+
if (form.form === "pty-selector" || input.spawn !== void 0 || input.mode === "pty" || machineForm && input.mode !== "oneshot") {
|
|
5113
5610
|
const holder = {};
|
|
5114
5611
|
return registry.start({
|
|
5115
5612
|
kind: "remote",
|
|
@@ -5246,83 +5743,32 @@ const NO_SESSION_KEY$1 = "remote:no-session";
|
|
|
5246
5743
|
* interact-able stdin / managed output / persistent env——回执形状与 oneshot 相同、只增状态,
|
|
5247
5744
|
* 运行中的命令可分步观察后再补输(input 动作);多步与交互任务默认进 PTY。
|
|
5248
5745
|
*/
|
|
5249
|
-
const REMOTE_TOOL_DESCRIPTION = `Remote shell
|
|
5250
|
-
|
|
5251
|
-
Actions (action is the only dispatch dimension; unknown fields are rejected):
|
|
5252
|
-
- exec {target, cmd | scriptPath, stdin?, timeout?, run_in_background?}: run a command. Machine target → oneshot; pty:<id|label> → run inside that live session; <machine>:pty[:label] → create a NEW session and run there.
|
|
5253
|
-
- add {ssh:{...} | spawn:"<cmd>", target?}: register a BYO endpoint (identity or interactive-shell command); no command runs. Returns the addressable label; a taken label answers [E_LABEL_IN_USE] — address existing names directly, never add again.
|
|
5254
|
-
- status {}: roster — the one unified pool (machines, identities, live PTY sessions).
|
|
5255
|
-
- status {target}: machine probe, or a session card (connection, cwd, ledger of the last 20 commands).
|
|
5256
|
-
- input {target:"pty:<id|label>", stdin, commandId?}: write stdin into the command currently running in that session — passwords, OTP, tokens, menu choices, REPL lines. Never creates a session, never dials.
|
|
5257
|
-
- close {target, force?}: close a PTY session; a running command refuses it (job_kill first) unless force:true kills it in the same step.
|
|
5258
|
-
|
|
5259
|
-
Targets:
|
|
5260
|
-
- ssh:<name> / docker:<name> / incus:<name> / bare pool name → stateless oneshot.
|
|
5261
|
-
- <machine>:pty[:label] → NEW PTY every call — creation is a target suffix, not a different tool.
|
|
5262
|
-
- pty:<id|label> (or its bare label) → REUSE the live session.
|
|
5263
|
-
After creation, reuse the returned session; repeating :pty opens another. Prefixes force the channel; bare names resolve through the roster.
|
|
5264
|
-
|
|
5265
|
-
PTY:
|
|
5266
|
-
- stateful oneshot, interact-able stdin, managed output, persistent env
|
|
5267
|
-
- timeout (default 120s) → command keeps running; returns job handle.
|
|
5268
|
-
- run_in_background → same job lane. jobId = commandId.
|
|
5269
|
-
- Read: job_output(jobId). Cancel: job_kill; try Ctrl-C first.
|
|
5270
|
-
- Busy session: input / poll / cancel / use another session.
|
|
5271
|
-
- exec.stdin = startup input; input = subsequent input.
|
|
5272
|
-
- input is verbatim: "\\n" submits; "\\u0003" Ctrl-C; "\\u0004" Ctrl-D; "" writes nothing. Ctrl-D follows terminal EOF rules.
|
|
5273
|
-
- commandId, if supplied, must match running command.
|
|
5274
|
-
- Input receipt confirms write only; read result via SAME job.
|
|
5275
|
-
- Output returns in full up to retention (4M chars per stream; Hub oneshot 4MiB bytes) — beyond it the retained tail plus outputStats. Redirect bulk output to files.
|
|
5276
|
-
- Idle expiry: 15 min; running commands exempt.
|
|
5277
|
-
|
|
5278
|
-
Oneshot:
|
|
5279
|
-
- stdout/stderr separate, exit code verbatim; no retained shell state. stdin is startup-only: there is no input after start — a prompt on a oneshot means cancel (job_kill) and switch to PTY. Heredocs and input redirection remain valid bulk-input alternatives.
|
|
5280
|
-
- Supervision prefers a pinned superd-run helper cached on the endpoint (~/.cache/superd/run), falls back to a POSIX shim — the receipt field "runner" says which ran. Either way children are reaped on completion and the whole group is killed on timeout/cancel. The endpoint needs bash only — no Python.
|
|
5281
|
-
- scriptPath (oneshot only): a LOCAL absolute script read before any dialing, sent literally over stdin to a fixed bash -s — the file carrier for multi-line scripts; pairs with neither cmd nor stdin.
|
|
5282
|
-
|
|
5283
|
-
Examples
|
|
5746
|
+
const REMOTE_TOOL_DESCRIPTION = `Remote shell (SSH, Docker, Incus, Hub): 1 default interactive PTY per machine (retains cwd/env). \`mode:"oneshot"\` = isolated, stateless.
|
|
5284
5747
|
|
|
5285
|
-
|
|
5748
|
+
Actions:
|
|
5749
|
+
- exec {target, cmd? | scriptPath?, stdin?, mode?:"pty"|"oneshot"="pty", timeout?, run_in_background?:boolean, commandId?:string}: run command / script / raw stdin write. Machine target = default PTY. PTY \`stdin\` alone = raw write into running command (optional \`commandId\` guard). \`run_in_background:true\` = async job, returns handle instantly, PTY handed off automatically.
|
|
5750
|
+
- handoff {target:<machine>}: detach machine's default PTY -> \`pty:<id>\`. Next exec spawns fresh default.
|
|
5751
|
+
- add {ssh:{...} | spawn:"<cmd>", target?}: register BYO identity/PTY. Returns label. Taken label answers \`[E_LABEL_IN_USE]\`.
|
|
5752
|
+
- status {target?}: roster pool (no target) or machine probe / session card.
|
|
5753
|
+
- close {target, force?:boolean}: close PTY shell. Machine target closes only default PTY. Running command blocks close unless \`force:true\`.
|
|
5286
5754
|
|
|
5287
|
-
|
|
5755
|
+
Targets & Semantics:
|
|
5756
|
+
- Targets: \`ssh:<n>\`, \`docker:<n>\`, \`incus:<n>\`, \`hub:<m4>\`, \`pty:<id|label>\` (or bare label). Busy PTY rejects new cmds (use input/poll/cancel/handoff/oneshot).
|
|
5757
|
+
- PTY: Stream includes echo/stderr. \`envDelta\` lists var names (no values). Expires after 15m idle. Timeout (default 120s) = sync wait limit (command keeps running as job: \`job_output\` / \`job_kill\`).
|
|
5758
|
+
- Oneshot: Separate stdout/stderr, verbatim exit code, stateless. \`stdin\` = startup only. Cached \`superd-run\` runner preferred over POSIX shim.
|
|
5759
|
+
- Script: \`scriptPath\` = local absolute file read before dial, runs as single \`bash -c\` cmd (same size limit). In PTY, runs as child process (\`cd\`/\`export\` do not affect parent shell). Hub inline cmd limit: 4096 chars.
|
|
5760
|
+
- Stdin: oneshot = \`fd0 → pipe\`; PTY = \`fd0 → pty slave\`, line disciplines. Literal bytes (\`\\n\`, \`\\u0003\` = Ctrl-C, \`\\u0004\` = Ctrl-D). Retention: 4M chars/stream (Hub oneshot 4MiB).
|
|
5288
5761
|
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
remote {
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
LISTEN 0 4096 127.0.0.1:2019 0.0.0.0:* users:(("caddy",pid=5600,fd=4))
|
|
5300
|
-
LISTEN 0 4096 127.0.0.1:19514 0.0.0.0:* users:(("multica",pid=27030,fd=6))
|
|
5301
|
-
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=26758,fd=17))
|
|
5302
|
-
[pty:pty-irsq (dev4via) (byo-pty) · conn BYOP · cmd remote-0d89215d1cc64cdc · cwd /home/u1 · idle-left ~15m · exit 0 · 0.3s]
|
|
5303
|
-
|
|
5304
|
-
Reuse the same session for a long task — returns a jobId immediately; collect with job_output, cancel with job_kill:
|
|
5305
|
-
|
|
5306
|
-
remote { "action": "exec", "target": "pty:dev4via", "cmd": "for i in 1 2 3; do sleep 1; echo \\"tick $i\\"; done; echo done", "run_in_background": true }
|
|
5307
|
-
|
|
5308
|
-
Started remote-47676dff2c6443e8 on pty:pty-irsq (dev4via) (BYOP). Use job_output to read and job_kill to cancel; completion is reported by the host.
|
|
5309
|
-
|
|
5310
|
-
job_output { "job_id": "remote-47676dff2c6443e8", "wait": true }
|
|
5311
|
-
|
|
5312
|
-
tick 1
|
|
5313
|
-
tick 2
|
|
5314
|
-
tick 3
|
|
5315
|
-
done
|
|
5316
|
-
[status: completed, exit code: 0]
|
|
5317
|
-
|
|
5318
|
-
- Bring Your Own PTY fits when the endpoint has no direct route, or the connection/transport layer must be composed yourself (jump chains, docker exec, console servers).
|
|
5319
|
-
- It also strips one or two shell-escaping layers — commands go straight to the innermost bash, quotes behave naturally.
|
|
5320
|
-
|
|
5321
|
-
Register once — the label enters the pool and works everywhere: exec, pty, copy, rsync. Worth it whenever more than one operation type is involved. Keys stay local-file references; nothing persisted.
|
|
5322
|
-
|
|
5323
|
-
remote { "action": "add", "ssh": { "host": "10.0.0.8", "user": "u1", "port": 22, "key": "/home/u1/.ssh/id_key4" }, "target": "ssh:lab" }
|
|
5324
|
-
remote { "action": "exec", "target": "ssh:lab", "cmd": "uname -a" }
|
|
5325
|
-
remote_transfer { "mode": "rsync", "src": "ssh:lab:/var/log/app/", "dst": "/home/u1/app-logs/", "argv": ["-a"] }`;
|
|
5762
|
+
Examples:
|
|
5763
|
+
remote {"action":"exec","target":"ssh:dev4","cmd":"cd /srv/app && pwd"}
|
|
5764
|
+
remote {"action":"exec","target":"ssh:dev4","cmd":"make test","run_in_background":true}
|
|
5765
|
+
remote {"action":"handoff","target":"ssh:dev4"}
|
|
5766
|
+
remote {"action":"exec","target":"pty:pty-irsq","stdin":"yes\\n","commandId":"remote-47676dff2c6443e8"}
|
|
5767
|
+
remote {"action":"exec","target":"ssh:dev4","mode":"oneshot","cmd":"uname -a"}
|
|
5768
|
+
remote {"action":"add","spawn":"ssh -t pc ssh -t dev4","target":"pty:dev4via"}
|
|
5769
|
+
remote {"action":"exec","target":"pty:dev4via","cmd":"hostname"}
|
|
5770
|
+
remote {"action":"add","ssh":{"host":"10.0.0.8","user":"u1","port":22,"key":"~/.ssh/id_key4"},"target":"ssh:lab"}
|
|
5771
|
+
remote_transfer {"mode":"rsync","src":"ssh:lab:/var/log/app/","dst":"~/app-logs/","argv":["-a"]}`;
|
|
5326
5772
|
/** action 是唯一派发维度:每个动词声明自己的字段集,超出即报错(不做在场推断、不留别名)。 */
|
|
5327
5773
|
const ACTION_PARAMS = {
|
|
5328
5774
|
exec: new Set([
|
|
@@ -5332,8 +5778,11 @@ const ACTION_PARAMS = {
|
|
|
5332
5778
|
"stdin",
|
|
5333
5779
|
"scriptPath",
|
|
5334
5780
|
"timeout",
|
|
5335
|
-
"run_in_background"
|
|
5781
|
+
"run_in_background",
|
|
5782
|
+
"mode",
|
|
5783
|
+
"commandId"
|
|
5336
5784
|
]),
|
|
5785
|
+
handoff: new Set(["action", "target"]),
|
|
5337
5786
|
add: new Set([
|
|
5338
5787
|
"action",
|
|
5339
5788
|
"target",
|
|
@@ -5345,26 +5794,20 @@ const ACTION_PARAMS = {
|
|
|
5345
5794
|
"action",
|
|
5346
5795
|
"target",
|
|
5347
5796
|
"force"
|
|
5348
|
-
]),
|
|
5349
|
-
input: new Set([
|
|
5350
|
-
"action",
|
|
5351
|
-
"target",
|
|
5352
|
-
"stdin",
|
|
5353
|
-
"commandId"
|
|
5354
5797
|
])
|
|
5355
5798
|
};
|
|
5356
5799
|
const ACTION_HINT = {
|
|
5357
|
-
exec: "exec takes target plus cmd (or scriptPath) and the execution options; ssh/spawn belong to add",
|
|
5800
|
+
exec: "exec takes target plus cmd (or scriptPath, or stdin alone as a raw write into the running command) and the execution options; ssh/spawn belong to add",
|
|
5801
|
+
handoff: "handoff takes exactly one field besides action: target — the machine whose default PTY becomes pty:<id>",
|
|
5358
5802
|
add: "add takes exactly one of ssh or spawn, plus an optional target label",
|
|
5359
5803
|
status: "status takes only an optional target — omit it for the roster",
|
|
5360
|
-
close: "close takes target pty:<id|label> and optional force"
|
|
5361
|
-
input: "input takes target (a live PTY session), stdin, and an optional commandId guard — no cmd, no timeout, no creation"
|
|
5804
|
+
close: "close takes a target (machine = its default PTY, or pty:<id|label>) and optional force — never a job id; cancel jobs with job_kill"
|
|
5362
5805
|
};
|
|
5363
5806
|
async function remoteExecute(ENV, args, exec) {
|
|
5364
5807
|
const action = args.action;
|
|
5365
|
-
if (action === void 0) throw new Error("[E_PARAMS] remote: action is required — \"exec\" (run a command), \"add\" (register a BYO endpoint), \"status\" (roster / probe / session card), \"close\" (close a PTY
|
|
5808
|
+
if (action === void 0) throw new Error("[E_PARAMS] remote: action is required — \"exec\" (run a command or raw stdin write), \"handoff\" (detach a machine default PTY), \"add\" (register a BYO endpoint), \"status\" (roster / probe / session card), \"close\" (close a PTY shell)");
|
|
5366
5809
|
const allowed = ACTION_PARAMS[action];
|
|
5367
|
-
if (allowed === void 0) throw new Error(`[E_PARAMS] remote: unknown action ${JSON.stringify(String(action))} — one of exec / add / close / status
|
|
5810
|
+
if (allowed === void 0) throw new Error(`[E_PARAMS] remote: unknown action ${JSON.stringify(String(action))} — one of exec / handoff / add / close / status`);
|
|
5368
5811
|
for (const key of Object.keys(args)) if (!allowed.has(key)) throw new Error(`[E_PARAMS] remote: "${key}" does not belong to action "${action}" — ${ACTION_HINT[action]}`);
|
|
5369
5812
|
const sessionKey = ENV.getSessionKey(exec);
|
|
5370
5813
|
const hasTarget = typeof args.target === "string" && args.target.length > 0;
|
|
@@ -5383,40 +5826,38 @@ async function remoteExecute(ENV, args, exec) {
|
|
|
5383
5826
|
durationMs: 0
|
|
5384
5827
|
};
|
|
5385
5828
|
}
|
|
5386
|
-
if (action === "
|
|
5387
|
-
if (!hasTarget) throw new Error("[E_PARAMS]
|
|
5388
|
-
const r$1 = ENV.driver.
|
|
5829
|
+
if (action === "handoff") {
|
|
5830
|
+
if (!hasTarget) throw new Error("[E_PARAMS] remote: action \"handoff\" requires target — the machine whose default PTY becomes an addressable pty:<id> (ssh:dev4, docker:c, hub:<m4>, or a bare pool name)");
|
|
5831
|
+
const r$1 = await ENV.driver.handoff(args.target, { sessionKey });
|
|
5389
5832
|
return {
|
|
5390
|
-
kind: "
|
|
5391
|
-
text: r$1.
|
|
5833
|
+
kind: "handoff",
|
|
5834
|
+
text: r$1.stdout,
|
|
5392
5835
|
exit: null,
|
|
5393
5836
|
durationMs: 0,
|
|
5394
5837
|
session: r$1.session,
|
|
5395
|
-
|
|
5396
|
-
...r$1.
|
|
5397
|
-
|
|
5838
|
+
connection: r$1.connection,
|
|
5839
|
+
...r$1.label !== void 0 ? { label: r$1.label } : {},
|
|
5840
|
+
...r$1.cwd !== null && r$1.cwd !== void 0 ? { cwd: r$1.cwd } : {},
|
|
5841
|
+
...r$1.created === true ? { created: true } : {},
|
|
5842
|
+
...r$1.running === true && r$1.commandId !== void 0 ? {
|
|
5843
|
+
running: true,
|
|
5844
|
+
commandId: r$1.commandId
|
|
5845
|
+
} : {},
|
|
5846
|
+
...r$1.ttlSec !== void 0 ? { ttlSec: r$1.ttlSec } : {}
|
|
5398
5847
|
};
|
|
5399
5848
|
}
|
|
5400
|
-
if (action === "
|
|
5401
|
-
if (!hasTarget) throw new Error("[E_PARAMS]
|
|
5402
|
-
|
|
5403
|
-
if (typeof args.commandId === "string" && args.commandId.length === 0) throw new Error("[E_PARAMS] remote: input: commandId, when given, must be the non-empty id from the exec receipt / job_output");
|
|
5404
|
-
const r$1 = await ENV.driver.input(args.target, {
|
|
5405
|
-
stdin: args.stdin,
|
|
5406
|
-
...args.commandId !== void 0 ? { commandId: args.commandId } : {}
|
|
5407
|
-
}, { sessionKey });
|
|
5408
|
-
const handle = r$1.jobId ?? r$1.commandId;
|
|
5849
|
+
if (action === "close") {
|
|
5850
|
+
if (!hasTarget) throw new Error("[E_PARAMS] close requires target — a machine (closes its default PTY) or pty:<id|label>");
|
|
5851
|
+
const r$1 = await ENV.driver.close(args.target, sessionKey, args.force === true);
|
|
5409
5852
|
return {
|
|
5410
|
-
kind: "
|
|
5411
|
-
text: `
|
|
5853
|
+
kind: "close",
|
|
5854
|
+
text: r$1.closed ? `closed ${r$1.session} (${r$1.connection})${r$1.killed.length > 0 ? ` — killed running job ${r$1.killed.join(", ")}` : ""}` : `${r$1.session}: not closed (${r$1.reason}) — ${r$1.message ?? "no live session"}`,
|
|
5412
5855
|
exit: null,
|
|
5413
5856
|
durationMs: 0,
|
|
5414
5857
|
session: r$1.session,
|
|
5415
|
-
|
|
5416
|
-
...r$1.
|
|
5417
|
-
|
|
5418
|
-
...r$1.jobId !== void 0 ? { jobId: r$1.jobId } : {},
|
|
5419
|
-
writtenBytes: r$1.writtenBytes
|
|
5858
|
+
closed: r$1.closed,
|
|
5859
|
+
...r$1.reason !== void 0 ? { reason: r$1.reason } : {},
|
|
5860
|
+
killed: r$1.killed
|
|
5420
5861
|
};
|
|
5421
5862
|
}
|
|
5422
5863
|
if (action === "add") {
|
|
@@ -5437,15 +5878,34 @@ async function remoteExecute(ENV, args, exec) {
|
|
|
5437
5878
|
}
|
|
5438
5879
|
if (!hasTarget) throw new Error("[E_PARAMS] remote: action \"exec\" requires target — a machine (ssh:dev4, docker:corti, hub:<m4>, a pool name) or a PTY session (pty:<id|label>, a bare label, or <machine>:pty[:label] to create one). Status probes and the roster live under action \"status\".");
|
|
5439
5880
|
const target = args.target;
|
|
5440
|
-
|
|
5881
|
+
if (args.commandId !== void 0 && args.commandId.length === 0) throw new Error("[E_PARAMS] remote: commandId, when given, must be the non-empty id from the exec receipt / job_output");
|
|
5441
5882
|
if (args.scriptPath !== void 0) {
|
|
5442
5883
|
if (args.scriptPath.length === 0) throw new Error("[E_PARAMS] remote: scriptPath must be a non-empty absolute path");
|
|
5443
|
-
if (ptyTarget) throw new Error("[E_SCRIPT_PARAMS] remote: scriptPath is oneshot-only — PTY lanes take cmd");
|
|
5444
5884
|
if (hasCmd) throw new Error("[E_SCRIPT_PARAMS] remote: scriptPath and cmd are alternative carriers — pass one");
|
|
5445
|
-
if (args.stdin !== void 0) throw new Error("[E_SCRIPT_PARAMS] remote: scriptPath already occupies stdin — do not pass stdin with it");
|
|
5446
5885
|
}
|
|
5447
|
-
if (args.
|
|
5448
|
-
if (!hasCmd && args.scriptPath === void 0)
|
|
5886
|
+
if (args.commandId !== void 0 && (hasCmd || args.scriptPath !== void 0)) throw new Error("[E_PARAMS] remote: commandId guards a raw stdin write (stdin alone, no cmd/scriptPath) — drop it for a normal command");
|
|
5887
|
+
if (args.stdin !== void 0 && !hasCmd && args.scriptPath === void 0) {
|
|
5888
|
+
if (args.mode === "oneshot") throw new Error("[E_STDIN_WITHOUT_CMD] remote: oneshot has no interactive lane — stdin without cmd/scriptPath is a raw write into a PTY running command; drop mode:\"oneshot\" (or pass cmd)");
|
|
5889
|
+
if (args.run_in_background === true) throw new Error("[E_PARAMS] remote: a raw stdin write cannot go background — it writes the command currently running in that PTY; follow it with job_output instead");
|
|
5890
|
+
const r$1 = await ENV.driver.input(target, {
|
|
5891
|
+
stdin: args.stdin,
|
|
5892
|
+
...args.commandId !== void 0 ? { commandId: args.commandId } : {}
|
|
5893
|
+
}, { sessionKey });
|
|
5894
|
+
const handle = r$1.jobId ?? r$1.commandId;
|
|
5895
|
+
return {
|
|
5896
|
+
kind: "input",
|
|
5897
|
+
text: `wrote ${r$1.writtenBytes} byte${r$1.writtenBytes === 1 ? "" : "s"} into ${r$1.session} for command ${r$1.commandId} — this confirms the write, not the program's response; read the response with job_output ${handle}`,
|
|
5898
|
+
exit: null,
|
|
5899
|
+
durationMs: 0,
|
|
5900
|
+
session: r$1.session,
|
|
5901
|
+
connection: r$1.connection,
|
|
5902
|
+
...r$1.label !== void 0 ? { label: r$1.label } : {},
|
|
5903
|
+
commandId: r$1.commandId,
|
|
5904
|
+
...r$1.jobId !== void 0 ? { jobId: r$1.jobId } : {},
|
|
5905
|
+
writtenBytes: r$1.writtenBytes
|
|
5906
|
+
};
|
|
5907
|
+
}
|
|
5908
|
+
if (!hasCmd && args.scriptPath === void 0) throw new Error("[E_PARAMS] remote: action \"exec\" must execute something — pass cmd, or scriptPath for a local script file, or stdin alone for a raw write into a PTY running command");
|
|
5449
5909
|
if (args.run_in_background) {
|
|
5450
5910
|
const jobs = exec.agent?.ctx.get("jobs");
|
|
5451
5911
|
if (!jobs) throw new Error("remote: background requires the official DSH jobs service and job_output/job_kill controller; foreground execution remains available");
|
|
@@ -5454,7 +5914,8 @@ async function remoteExecute(ENV, args, exec) {
|
|
|
5454
5914
|
cmd: args.cmd,
|
|
5455
5915
|
scriptPath: args.scriptPath,
|
|
5456
5916
|
stdin: args.stdin,
|
|
5457
|
-
timeout: args.timeout
|
|
5917
|
+
timeout: args.timeout,
|
|
5918
|
+
...args.mode !== void 0 ? { mode: args.mode } : {}
|
|
5458
5919
|
}, { sessionKey });
|
|
5459
5920
|
let jobId;
|
|
5460
5921
|
try {
|
|
@@ -5463,7 +5924,8 @@ async function remoteExecute(ENV, args, exec) {
|
|
|
5463
5924
|
cmd: args.cmd,
|
|
5464
5925
|
scriptPath: args.scriptPath,
|
|
5465
5926
|
stdin: args.stdin,
|
|
5466
|
-
timeout: args.timeout
|
|
5927
|
+
timeout: args.timeout,
|
|
5928
|
+
...args.mode !== void 0 ? { mode: args.mode } : {}
|
|
5467
5929
|
}, exec, ENV.driver);
|
|
5468
5930
|
} catch (error) {
|
|
5469
5931
|
lane?.cleanup();
|
|
@@ -5489,7 +5951,8 @@ async function remoteExecute(ENV, args, exec) {
|
|
|
5489
5951
|
cmd: args.cmd,
|
|
5490
5952
|
scriptPath: args.scriptPath,
|
|
5491
5953
|
stdin: args.stdin,
|
|
5492
|
-
timeout: args.timeout
|
|
5954
|
+
timeout: args.timeout,
|
|
5955
|
+
...args.mode !== void 0 ? { mode: args.mode } : {}
|
|
5493
5956
|
}, { sessionKey });
|
|
5494
5957
|
const v = workbenchValue(r);
|
|
5495
5958
|
if (r.running === true && r.ptyJob !== void 0) {
|
|
@@ -5516,17 +5979,17 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
5516
5979
|
type: "string",
|
|
5517
5980
|
enum: [
|
|
5518
5981
|
"exec",
|
|
5982
|
+
"handoff",
|
|
5519
5983
|
"add",
|
|
5520
5984
|
"status",
|
|
5521
|
-
"close"
|
|
5522
|
-
"input"
|
|
5985
|
+
"close"
|
|
5523
5986
|
],
|
|
5524
5987
|
required: true,
|
|
5525
|
-
description: "Required. exec = run a command
|
|
5988
|
+
description: "Required. exec = run a command / script / raw stdin write (machine target defaults to its reusable PTY); handoff = detach a machine's default PTY as pty:<id>; add = register a BYO ssh identity or BYO PTY into the pool; status = roster (no target) or probe/session card (target); close = close a PTY shell (no job id — cancel with job_kill)."
|
|
5526
5989
|
},
|
|
5527
5990
|
target: {
|
|
5528
5991
|
type: "string",
|
|
5529
|
-
description: "exec: required
|
|
5992
|
+
description: "exec: required — a machine (bare pool name, ssh:<name>, docker:<name>, incus:<name>, hub:<m4>; its default PTY unless mode is oneshot) or a live PTY session (pty:<id|label> or its bare label). handoff: the machine whose default PTY detaches. add: optional label for the BYO entry. status: optional — omit for the roster. close: a machine (its default PTY) or pty:<id|label>."
|
|
5530
5993
|
},
|
|
5531
5994
|
cmd: {
|
|
5532
5995
|
type: "string",
|
|
@@ -5534,19 +5997,24 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
5534
5997
|
},
|
|
5535
5998
|
stdin: {
|
|
5536
5999
|
type: "string",
|
|
5537
|
-
description: "exec:
|
|
6000
|
+
description: "exec: with cmd/scriptPath = startup input (oneshot: piped; pty: REPL-style right after the command line; with scriptPath it is the script process's own stdin). Alone (no cmd/scriptPath) = a raw write into the PTY's RUNNING command — exact bytes; \"\" writes zero bytes (not EOF); \"\\n\" submits a line; \"\\u0003\" = Ctrl-C, \"\\u0004\" = Ctrl-D as raw characters; after a raw \"\\u0003\" settle the job with job_kill."
|
|
5538
6001
|
},
|
|
5539
6002
|
commandId: {
|
|
5540
6003
|
type: "string",
|
|
5541
|
-
description: "
|
|
6004
|
+
description: "exec stdin-only: optional guard — when given it must equal the running command's id (from the exec receipt or job_output); a stale id refuses the write instead of feeding the next command. Not valid with cmd/scriptPath."
|
|
5542
6005
|
},
|
|
5543
6006
|
timeout: {
|
|
5544
6007
|
type: "number",
|
|
5545
6008
|
description: "exec: per-command timeout in seconds (default 120). oneshot: interrupts then kills the whole group. pty: the command is NOT killed on timeout — it hands off to a running job you can poll (job_output) and cancel (job_kill = Ctrl-C first; the session survives an interrupt)."
|
|
5546
6009
|
},
|
|
6010
|
+
mode: {
|
|
6011
|
+
type: "string",
|
|
6012
|
+
enum: ["pty", "oneshot"],
|
|
6013
|
+
description: "exec: pty (default) — a machine target reuses its default interactive PTY (cwd/env persist); oneshot — isolated, stateless, stdout/stderr split, verbatim exit. Redundant on pty:<id|label> targets; \"oneshot\" on an addressable PTY is an error."
|
|
6014
|
+
},
|
|
5547
6015
|
run_in_background: {
|
|
5548
6016
|
type: "boolean",
|
|
5549
|
-
description: "exec:
|
|
6017
|
+
description: "exec with cmd/scriptPath only: the command went async (not bash &) — a running job returns immediately and a completion notification will follow, so no polling is required. Oneshot runs async; on a machine's default PTY the framed command starts and that PTY is handed off in the same operation; on an addressable PTY it keeps its identity. job_output reads the command's full stream from byte 0 (overlap with the pre-handoff receipt is expected); job_kill cancels. Requires the official jobs service/controller."
|
|
5550
6018
|
},
|
|
5551
6019
|
ssh: {
|
|
5552
6020
|
type: "object",
|
|
@@ -5578,7 +6046,7 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
5578
6046
|
},
|
|
5579
6047
|
spawn: {
|
|
5580
6048
|
type: "string",
|
|
5581
|
-
description: "add: BYO PTY entry point — the full command that starts an interactive shell (e.g. \"docker exec -it img bash\"). Pairs with target pty
|
|
6049
|
+
description: "add: BYO PTY entry point — the full command that starts an interactive shell (e.g. \"docker exec -it img bash\"). Pairs with target pty:<label> / a bare <label> / nothing (auto id). Nested hops are dumb pipes (only the innermost bash frames)."
|
|
5582
6050
|
},
|
|
5583
6051
|
force: {
|
|
5584
6052
|
type: "boolean",
|
|
@@ -5595,6 +6063,7 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
5595
6063
|
required: true,
|
|
5596
6064
|
enum: [
|
|
5597
6065
|
"exec",
|
|
6066
|
+
"handoff",
|
|
5598
6067
|
"status",
|
|
5599
6068
|
"roster",
|
|
5600
6069
|
"job",
|
|
@@ -5677,7 +6146,7 @@ function createRemoteTool(driver, opts = {}) {
|
|
|
5677
6146
|
const description = tool.description;
|
|
5678
6147
|
Object.defineProperty(tool, "description", {
|
|
5679
6148
|
enumerable: true,
|
|
5680
|
-
get: () => description + (driver.hasHub() ? "\nHub transport available: hub:<m4> selects a paired remote machine (never self; the roster lists them). Hub failures never fall back to SSH.\n- hub:<m4> oneshot: the peer hub server is the runner itself — bash -lc in a detached group, TERM→2s→KILL cleanup; the receipt runner field says \"hub-lease\" (the superd-run helper is a dumb-pipe artifact, never uploaded over the Hub).
|
|
6149
|
+
get: () => description + (driver.hasHub() ? "\nHub transport available: hub:<m4> selects a paired remote machine (never self; the roster lists them). Hub failures never fall back to SSH.\n- hub:<m4> oneshot: the peer hub server is the runner itself — bash -lc in a detached group, TERM→2s→KILL cleanup; the receipt runner field says \"hub-lease\" (the superd-run helper is a dumb-pipe artifact, never uploaded over the Hub). The peer caps inline commands at 4096 chars — and a scriptPath IS carried as one bash -c command, so hub-oneshot scripts share that cap (split large work or transfer the file and run it with cmd).\n- hub:<m4> as a machine target has the SAME default-PTY semantics as every other transport — exec opens the default session (a local pump child over one authenticated WebSocket to the peer terminal), handoff / background / stdin-only / close / idle TTL all work identically." : "")
|
|
5681
6150
|
});
|
|
5682
6151
|
return tool;
|
|
5683
6152
|
}
|
|
@@ -5716,7 +6185,7 @@ function renderEnvDelta(d) {
|
|
|
5716
6185
|
return bits.length > 0 ? bits.join(" ") : void 0;
|
|
5717
6186
|
}
|
|
5718
6187
|
function renderValue(v) {
|
|
5719
|
-
if (v.kind === "status" || v.kind === "roster" || v.kind === "job" || v.kind === "close" || v.kind === "input") return v.text;
|
|
6188
|
+
if (v.kind === "status" || v.kind === "roster" || v.kind === "job" || v.kind === "close" || v.kind === "input" || v.kind === "handoff") return v.text;
|
|
5720
6189
|
const parts = [];
|
|
5721
6190
|
if (v.reconnected === true) parts.push(RECONNECT_NOTICE);
|
|
5722
6191
|
if (typeof v.notice === "string" && v.notice.length > 0) parts.push(v.notice);
|