@syrkos/filaxis 0.1.0-alpha.12 → 0.1.0-alpha.13

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.

Potentially problematic release.


This version of @syrkos/filaxis might be problematic. Click here for more details.

@@ -1,10 +1,13 @@
1
1
  /**
2
- * agent-toolkit.ts — 原生 Agent 工具集(Gemini 试用反馈 #3)
2
+ * agent-toolkit.ts — 原生 Agent 工具集(Gemini 试用反馈 #3 + 第十季层族)
3
3
  *
4
- * 一行 `createAgentToolkit(stack)` 生成 LLM 工具面(run_command / spawn_and_observe /
5
- * stop_and_verify)——OpenAI/Anthropic/Vercel AI SDK 通吃的 {tools, handlers} 双层结构:
6
- * 定义层喂模型,执行层留宿主(桥到任意框架)。语义契约(退出码是数据/终态三态/
7
- * 就绪握手)由内核承载,宿主零胶水。
4
+ * 一行 `createAgentToolkit(stack)` 生成 LLM 工具面——OpenAI/Anthropic/Vercel AI SDK
5
+ * 通吃的 {tools, handlers} 双层结构:定义层喂模型,执行层留宿主(桥到任意框架)。
6
+ * 语义契约(退出码是数据/终态三态/就绪握手)由内核承载,宿主零胶水。
7
+ *
8
+ * 第十季层族:执行类工具带可选 layer 参数(缺省=默认层——最近 push 的命名层;
9
+ * 未用层族时与旧行为一致,零破坏);push_ssh/push_into/list_layers/remove_layer
10
+ * 操作 RuntimeStack 命名层注册表(Paramiko 式句柄寻址——栈/焦点弱化归消费方)。
8
11
  */
9
12
  import type { RuntimeStack } from "./runtime-stack.js";
10
13
  import type { RuntimeDriver } from "./driver.js";
@@ -53,6 +56,7 @@ export interface AgentToolHandlers {
53
56
  duration_ms?: number;
54
57
  stream?: "stdout" | "stderr" | "both";
55
58
  name?: string;
59
+ layer?: string;
56
60
  }): Promise<{
57
61
  session_id: string;
58
62
  lines: Array<{
@@ -65,6 +69,7 @@ export interface AgentToolHandlers {
65
69
  ready_marker?: string;
66
70
  error_pattern?: string;
67
71
  name?: string;
72
+ layer?: string;
68
73
  }): Promise<{
69
74
  session_id: string;
70
75
  log_base: string;
@@ -77,6 +82,42 @@ export interface AgentToolHandlers {
77
82
  exit_code?: number | null;
78
83
  note?: string;
79
84
  }>;
85
+ push_ssh(args: {
86
+ host: string;
87
+ port?: number;
88
+ user?: string;
89
+ key?: string;
90
+ jump?: string;
91
+ name?: string;
92
+ }): Promise<{
93
+ layer: string;
94
+ reused: boolean;
95
+ reused_from?: string;
96
+ note?: string;
97
+ }>;
98
+ push_into(args: {
99
+ enter_cmd: string;
100
+ label?: string;
101
+ from?: string;
102
+ name?: string;
103
+ }): Promise<{
104
+ layer: string;
105
+ target?: string;
106
+ }>;
107
+ list_layers(args: Record<string, never>): Promise<{
108
+ text: string;
109
+ layers: Array<{
110
+ name: string;
111
+ driver: string;
112
+ target?: string;
113
+ }>;
114
+ }>;
115
+ remove_layer(args: {
116
+ name: string;
117
+ destroy?: boolean;
118
+ }): Promise<{
119
+ removed: string;
120
+ }>;
80
121
  }
81
122
  export interface AgentToolkit {
82
123
  tools: AgentToolDef[];
@@ -6,10 +6,16 @@
6
6
  *
7
7
  * 第七季盲测反馈(B2 逐秒拆解):旧实现一次 editFile = 5 次串行 SSH 往返
8
8
  * (stat/read/stat/write/stat),Windows 无 ControlMaster 下放大到 15s+。
9
- * 本版:合成 **2 次往返**(读+戳 1 次;**远端 CAS 原子写回** 1 次——GPT 并发
10
- * 安全意见采纳:「最后一次校验与写入放进同一个远端受控操作」——戳比对与写回
11
- * 在远端一条命令内原子完成,杜绝 A/B 丢失更新)+ 方言适配(stat/base64 三族,
12
- * 每 driver 缓存一次探测)。大文件(base64>30KB)自动回落旧 5 次路径(fs 层)。
9
+ * 第七季版合成「读+戳 1 次 + 远端 CAS 原子写回 1 次」——但方言探测(base64
10
+ * -w 0/-d/-D)是独立往返,SDK 常驻内存下有 dialectCache 掩护,**CLI 每次冷启动
11
+ * 实为 3 次物理往返**(第十季 B5 轨迹实锤:3×SSH 握手 ≈11.4s 突破宿主 10s 后台
12
+ * 阈值,多耗 3 轮轮询)。
13
+ *
14
+ * 本版(第十季):**真 2 次往返**——方言探测合成进读往返(① stat/sha 戳 +
15
+ * base64 -d 探测 + 文件读取一次带回;② CAS 写回用已确定 argv)。编码侧无需
16
+ * 探测:折行输出由解析侧 \s+ 归一消化(base64 < file 全方言通吃);解码侧
17
+ * 不用 `base64 -d || base64 -D` 赌 fallback(GPT 否决:双解码器共用 stdin,
18
+ * 首个消费后第二个拿不到完整载荷)。大文件(base64>30KB)自动回落 fs 层路径。
13
19
  *
14
20
  * **仅适用 UTF-8 文本**(readFile 返回 UTF-8 字符串;二进制文件的编辑走
15
21
  * readBase64 + 消费方自行改写 + writeFile)。
@@ -31,8 +37,10 @@ export declare class EditConflictError extends Error {
31
37
  constructor(message: string);
32
38
  }
33
39
  /**
34
- * 受控编辑(合成 2 次往返 + 远端 CAS 原子写回):
35
- * ① 读+戳(1 次往返):`stat 戳; __FXS__; base64 读` —— 拿 (mtime,size)+全文
40
+ * 受控编辑(真 2 次往返 + 远端 CAS 原子写回):
41
+ * ① 读+戳+方言探测(1 次往返):`stat 戳; sha 戳; __FXS__; base64 -d 探测;
42
+ * __FXD__; base64 < file` —— (mtime,size)+全文+解码 argv 一次带回
43
+ * (__FXD__ 含下划线不在 base64 字母表——与 __FXS__ 同款防撞)
36
44
  * ② 精确替换(本地):0 命中/多处命中(未开 all)拒绝
37
45
  * ③ CAS 原子写回(1 次往返):远端一条命令内完成「戳比对 + tmp 写 + mv 原子替换 + 新戳回执」
38
46
  * —— 戳不符(他人先写)→ EditConflictError;写后回执新戳
package/dist/index.js CHANGED
@@ -1182,7 +1182,7 @@ class PtyLink {
1182
1182
  registerFallbackStep() {
1183
1183
  const step = String(this.fallbackSteps.size);
1184
1184
  this.fallbackSteps.add(step);
1185
- return `; echo __FILAXIS_FB_${this.fallbackNonce}_${step}_$?_$RANDOM`;
1185
+ return `; echo __FILAXIS_FB_${this.fallbackNonce}_${step}_$?_$$`;
1186
1186
  }
1187
1187
  attach(args) {
1188
1188
  this.fallbackNonce = crypto4.randomBytes(16).toString("hex");
@@ -1232,7 +1232,7 @@ ${raw.slice(-1500)}` : `${reason}
1232
1232
  this.fallbackSeen = undefined;
1233
1233
  if (!this.pendingExitFailure)
1234
1234
  await this.waitQuiet();
1235
- this.send(`stty -echo -ixon cols 500 rows 50; export PS1= PS2=; echo PTYRDY_$RANDOM$RANDOM_$(id -un 2>/dev/null)
1235
+ this.send(`stty -echo -ixon cols 500 rows 50; export PS1= PS2=; echo PTYRDY_$$_$(id -un 2>/dev/null)
1236
1236
  `);
1237
1237
  try {
1238
1238
  await new Promise((resolve, reject) => {
@@ -2612,6 +2612,8 @@ class RuntimeFactory {
2612
2612
  }
2613
2613
  // src/runtime-stack.ts
2614
2614
  import { spawn as spawn5 } from "node:child_process";
2615
+ import * as os from "node:os";
2616
+ init_ssh_helpers();
2615
2617
  class RuntimeStack {
2616
2618
  static driverCache = new Map;
2617
2619
  events = new EventBus;
@@ -2631,12 +2633,15 @@ class RuntimeStack {
2631
2633
  cwd: d.cwd,
2632
2634
  capabilities: d.capabilities,
2633
2635
  cfg: {
2636
+ pty: c.pty,
2637
+ connParams: c.connParams,
2638
+ sshHops: c.sshHops,
2639
+ enters: enterStepsOf(c).map((s) => s.cmd),
2634
2640
  launchCmd: c.launchCmd,
2635
2641
  runtime: c.runtime,
2636
2642
  shell: c.shell,
2637
2643
  namespace: c.namespace,
2638
- sshOptions: c.sshOptions,
2639
- connParams: c.connParams
2644
+ sshOptions: c.sshOptions
2640
2645
  }
2641
2646
  });
2642
2647
  }
@@ -2663,6 +2668,185 @@ class RuntimeStack {
2663
2668
  this.factory.registerDriver(type, f);
2664
2669
  this.featuresCache = null;
2665
2670
  }
2671
+ namedLayers = new Map;
2672
+ activeName;
2673
+ get layers() {
2674
+ return this.namedLayers;
2675
+ }
2676
+ get active() {
2677
+ return this.activeName ? this.namedLayers.get(this.activeName)?.driver : undefined;
2678
+ }
2679
+ get activeLayer() {
2680
+ return this.activeName ? this.namedLayers.get(this.activeName) : undefined;
2681
+ }
2682
+ async addLayer(name, descriptorOrDriver) {
2683
+ this.assertNotDupName(name);
2684
+ if (this.isDriver(descriptorOrDriver)) {
2685
+ const driver = descriptorOrDriver;
2686
+ const handle = { name, descriptor: driver.descriptor, driver };
2687
+ this.namedLayers.set(name, handle);
2688
+ this.activeName = name;
2689
+ return { handle, reused: false };
2690
+ }
2691
+ const descriptor = descriptorOrDriver;
2692
+ const hit = await this.findIdentityHit(descriptor);
2693
+ if (hit) {
2694
+ const reused = hit.terminal || await this.verifyLanding(hit.handle, hopHost(layerPathIdentities(descriptor).at(-1) ?? ""));
2695
+ if (reused) {
2696
+ this.log.info(`[Filaxis] 层 '${name}' 落点已由 '${hit.handle.name}' 持有——幂等复用(不重复建连)`);
2697
+ return { handle: hit.handle, reused: true, reusedFrom: hit.handle.name };
2698
+ }
2699
+ throw new Error(`层名 '${name}' 的落点 identity 命中既有层 '${hit.handle.name}' 的途经节点,且落点校验不符` + `(期望 ${hopHost(layerPathIdentities(descriptor).at(-1) ?? "?")},实测 ${hit.handle.actualHostname ?? "?"})` + `(宁重连不错主机):请检查连接参数后重试`);
2700
+ }
2701
+ const driver = await this.connect(descriptor);
2702
+ const handle = { name, descriptor: driver.descriptor, driver };
2703
+ this.namedLayers.set(name, handle);
2704
+ this.activeName = name;
2705
+ return { handle, reused: false };
2706
+ }
2707
+ async layerInto(from, name, enterCmd, opts = {}) {
2708
+ this.assertNotDupName(name);
2709
+ const src = this.requireLayer(from);
2710
+ const pushInto = src.driver.pushInto;
2711
+ if (typeof pushInto !== "function") {
2712
+ throw new Error(`层 '${from}'(${src.descriptor.driver})不支持派生进入——需 ssh PTY 形态(pushInto 缺席)`);
2713
+ }
2714
+ const driver = await pushInto.call(src.driver, enterCmd, opts.label ?? enterCmd.slice(0, 30));
2715
+ const handle = { name, descriptor: driver.descriptor, driver };
2716
+ this.namedLayers.set(name, handle);
2717
+ this.activeName = name;
2718
+ return handle;
2719
+ }
2720
+ async sshInto(from, name, target, connParams) {
2721
+ this.assertNotDupName(name);
2722
+ const src = this.requireLayer(from);
2723
+ const hop = connParams ? { host: target, conn: connParams } : target;
2724
+ const hit = await this.findIdentityHopHit(hop);
2725
+ if (hit) {
2726
+ const reused = hit.terminal || await this.verifyLanding(hit.handle, hopHost(hop));
2727
+ if (reused) {
2728
+ this.log.info(`[Filaxis] 层 '${name}' 落点已由 '${hit.handle.name}' 持有——幂等复用(不重复建连)`);
2729
+ return { handle: hit.handle, reused: true, reusedFrom: hit.handle.name };
2730
+ }
2731
+ throw new Error(`层名 '${name}' 的落点 identity 命中既有层 '${hit.handle.name}' 的途经节点,且落点校验不符` + `(期望 ${hopHost(hop)},实测 ${hit.handle.actualHostname ?? "?"})——宁重连不错主机:请检查连接参数后重试`);
2732
+ }
2733
+ const pushChild = src.driver.pushChild;
2734
+ if (typeof pushChild !== "function") {
2735
+ throw new Error(`层 '${from}'(${src.descriptor.driver})不支持链式 ssh 派生——需 ssh PTY 形态(pushChild 缺席)`);
2736
+ }
2737
+ const driver = await pushChild.call(src.driver, hop);
2738
+ const handle = { name, descriptor: driver.descriptor, driver };
2739
+ this.namedLayers.set(name, handle);
2740
+ this.activeName = name;
2741
+ return { handle, reused: false };
2742
+ }
2743
+ layer(name) {
2744
+ return this.requireLayer(name);
2745
+ }
2746
+ setActive(name) {
2747
+ const h = this.requireLayer(name);
2748
+ this.activeName = name;
2749
+ return h;
2750
+ }
2751
+ async removeLayer(name, opts = {}) {
2752
+ const h = this.requireLayer(name);
2753
+ this.namedLayers.delete(name);
2754
+ if (opts.destroy) {
2755
+ RuntimeStack.evictDriver(h.descriptor);
2756
+ await h.driver.destroy().catch(() => {
2757
+ return;
2758
+ });
2759
+ }
2760
+ if (this.activeName === name) {
2761
+ this.activeName = [...this.namedLayers.keys()].at(-1);
2762
+ }
2763
+ }
2764
+ layerText() {
2765
+ if (this.namedLayers.size === 0)
2766
+ return "Filaxis 命名层:(空——addLayer/layerInto 注册)";
2767
+ const lines = [];
2768
+ for (const h of this.namedLayers.values()) {
2769
+ const d = h.driver;
2770
+ const base = h.descriptor.driver === "local" ? `${os.hostname()}[local]` : layerDisplayName(h.descriptor);
2771
+ const state = d.reconnecting === true ? " ↻连中" : d.layerShellGone ? " ⚠退化(内层 shell 已退出,勿在此层执行)" : d.alive === false ? " ⚠死(removeLayer 后按原参数重建)" : "";
2772
+ const user = d.effectiveUser ? ` · ${d.effectiveUser}$` : "";
2773
+ const mark = h.name === this.activeName ? " ◀ 默认" : "";
2774
+ lines.push(` ${h.name}: ${base}${user}${state}${mark}`);
2775
+ }
2776
+ return ["Filaxis 命名层(工具缺省落【默认层】;显式按名寻址任意层):", ...lines].join(`
2777
+ `);
2778
+ }
2779
+ snapshot() {
2780
+ return {
2781
+ layers: [...this.namedLayers.values()].map((h) => ({ name: h.name, descriptor: h.descriptor })),
2782
+ active: this.activeName
2783
+ };
2784
+ }
2785
+ async restore(snap) {
2786
+ for (const { name, descriptor } of snap.layers) {
2787
+ if (this.namedLayers.has(name))
2788
+ continue;
2789
+ const driver = await this.connect(descriptor);
2790
+ this.namedLayers.set(name, { name, descriptor: driver.descriptor, driver });
2791
+ }
2792
+ if (snap.active && this.namedLayers.has(snap.active))
2793
+ this.activeName = snap.active;
2794
+ }
2795
+ assertNotDupName(name) {
2796
+ if (this.namedLayers.has(name)) {
2797
+ throw new Error(`层名 '${name}' 已占用(既有:${[...this.namedLayers.keys()].join(", ")})——换名或先 removeLayer`);
2798
+ }
2799
+ }
2800
+ requireLayer(name) {
2801
+ const h = this.namedLayers.get(name);
2802
+ if (!h) {
2803
+ throw new Error(`层 '${name}' 不存在(既有:${[...this.namedLayers.keys()].join(", ") || "空"})`);
2804
+ }
2805
+ return h;
2806
+ }
2807
+ isDriver(x) {
2808
+ return typeof x.exec === "function";
2809
+ }
2810
+ async findIdentityHit(descriptor) {
2811
+ if (enterStepsOf(descriptor.config ?? {}).length > 0)
2812
+ return;
2813
+ const dest = layerPathIdentities(descriptor).at(-1);
2814
+ if (!dest)
2815
+ return;
2816
+ return this.findIdentityHopHit(dest);
2817
+ }
2818
+ async findIdentityHopHit(dest) {
2819
+ const destId = await resolveSshIdentity(dest);
2820
+ if (!destId || destId.startsWith("unresolved:")) {
2821
+ return;
2822
+ }
2823
+ for (const h of this.namedLayers.values()) {
2824
+ if (enterStepsOf(h.descriptor.config ?? {}).length > 0)
2825
+ continue;
2826
+ const path = layerPathIdentities(h.descriptor);
2827
+ for (let i = 0;i < path.length; i++) {
2828
+ if (await resolveSshIdentity(path[i]) === destId) {
2829
+ return { handle: h, terminal: i === path.length - 1 };
2830
+ }
2831
+ }
2832
+ }
2833
+ return;
2834
+ }
2835
+ async verifyLanding(hit, expectedHost) {
2836
+ if (!expectedHost)
2837
+ return true;
2838
+ const d = hit.driver;
2839
+ if (typeof d.exec !== "function")
2840
+ return true;
2841
+ try {
2842
+ hit.actualHostname ??= ((await d.exec("hostname")).stdout || "").trim().split(`
2843
+ `).pop() || "";
2844
+ } catch {
2845
+ return false;
2846
+ }
2847
+ const actual = hit.actualHostname;
2848
+ return Boolean(actual) && (actual.includes(expectedHost) || expectedHost.includes(actual));
2849
+ }
2666
2850
  async features() {
2667
2851
  if (this.featuresCache)
2668
2852
  return this.featuresCache;
@@ -3573,31 +3757,12 @@ var q5 = (s) => `'${s.replace(/'/g, "'\\''")}'`;
3573
3757
  var shellSafe = (s) => !s.includes("'") && !s.includes(`
3574
3758
  `);
3575
3759
  var statCmdOf = (f) => `stat -c '%Y %s' ${f} 2>/dev/null || stat -f '%m %z' ${f} 2>/dev/null`;
3576
- var dialectCache = new Map;
3577
- async function dialectOf(drv) {
3578
- const key = JSON.stringify(drv.descriptor);
3579
- const hit = dialectCache.get(key);
3580
- if (hit)
3581
- return hit;
3582
- const probe = await drv.exec("echo FX2=$(printf filaxis | base64 -w 0 2>/dev/null | head -c 8);" + "echo FX3=$(printf ZmlsYXhpcw== | base64 -d 2>/dev/null | head -c 7);" + "echo FX4=$(printf ZmlsYXhpcw== | base64 -D 2>/dev/null | head -c 7)", { timeoutMs: 15000 });
3583
- const out = probe.stdout || "";
3584
- const b64Gnu = /FX2=ZmlsYXhp/.test(out);
3585
- const decD = /FX4=filaxis/.test(out);
3586
- const d = {
3587
- statCmd: "",
3588
- enc: b64Gnu ? "base64 -w 0" : "base64",
3589
- dec: decD ? "base64 -D" : "base64 -d",
3590
- shaCmd: `{ sha256sum ${"{f}"} 2>/dev/null || sha256 -q ${"{f}"} 2>/dev/null || openssl dgst -sha256 ${"{f}"} 2>/dev/null | tail -1; } | head -c 12`
3591
- };
3592
- dialectCache.set(key, d);
3593
- return d;
3594
- }
3760
+ var shaCmdOf = (f) => `{ sha256sum ${f} 2>/dev/null || sha256 -q ${f} 2>/dev/null || openssl dgst -sha256 ${f} 2>/dev/null | tail -1; } | head -c 12`;
3595
3761
  async function editFile(drv, path, oldStr, newStr, opts = {}) {
3596
- const dialect = await dialectOf(drv);
3597
3762
  const qp = q5(path);
3598
- const readCmd = `${statCmdOf(qp)}; ${dialect.shaCmd.replace(/\{f\}/g, qp)}; echo __FXS__; ${dialect.enc} ${qp} 2>/dev/null`;
3763
+ const readCmd = `${statCmdOf(qp)}; ${shaCmdOf(qp)}; echo __FXS__; ` + `printf ZmlsYXhpcw== | base64 -d 2>/dev/null; echo __FXD__; base64 < ${qp} 2>/dev/null`;
3599
3764
  const r = await drv.exec(readCmd, { timeoutMs: 30000 });
3600
- const [stampPart, b64Part] = (r.stdout || "").split("__FXS__");
3765
+ const [stampPart, restPart] = (r.stdout || "").split("__FXS__");
3601
3766
  const originStampRaw = (stampPart ?? "").trim();
3602
3767
  const originLines = originStampRaw.split(/\n/);
3603
3768
  const statStamp = (originLines[0] ?? "").trim();
@@ -3612,7 +3777,9 @@ async function editFile(drv, path, oldStr, newStr, opts = {}) {
3612
3777
  };
3613
3778
  if (!origin.size || !shaStamp)
3614
3779
  throw new Error(`文件不存在或指纹异常: ${path}(${originStampRaw})`);
3615
- const content0 = Buffer.from((b64Part ?? "").replace(/\s+/g, ""), "base64").toString("utf-8");
3780
+ const [decPart, b64Part = ""] = (restPart ?? "").split("__FXD__");
3781
+ const dec = (decPart ?? "").trim() === "filaxis" ? "base64 -d" : "base64 -D";
3782
+ const content0 = Buffer.from(b64Part.replace(/\s+/g, ""), "base64").toString("utf-8");
3616
3783
  const occurrences = content0.split(oldStr).length - 1;
3617
3784
  if (occurrences === 0) {
3618
3785
  throw new Error(`未命中待替换片段(0 处): ${path}(oldStr 长度 ${oldStr.length},文件 ${content0.length} 字符)`);
@@ -3625,7 +3792,7 @@ async function editFile(drv, path, oldStr, newStr, opts = {}) {
3625
3792
  if (newB64.length > 30000) {
3626
3793
  return editFileViaFs(drv, path, edited, occurrences);
3627
3794
  }
3628
- const cmd = `if [ "$(${statCmdOf(qp)})" = "${statStamp}" ] && ` + `[ "$(${dialect.shaCmd.replace(/\{f\}/g, qp)})" = "${shaStamp}" ]; then ` + `printf %s ${q5(newB64)} | ${dialect.dec} > ${qp}.fx-tmp && mv ${qp}.fx-tmp ${qp} && ` + `echo "FX_OK $(stat -c '%Y %s' ${qp} 2>/dev/null || stat -f '%m %z' ${qp} 2>/dev/null)"; ` + `else echo FX_CAS_FAIL; fi`;
3795
+ const cmd = `if [ "$(${statCmdOf(qp)})" = "${statStamp}" ] && ` + `[ "$(${shaCmdOf(qp)})" = "${shaStamp}" ]; then ` + `printf %s ${q5(newB64)} | ${dec} > ${qp}.fx-tmp && mv ${qp}.fx-tmp ${qp} && ` + `echo "FX_OK $(stat -c '%Y %s' ${qp} 2>/dev/null || stat -f '%m %z' ${qp} 2>/dev/null)"; ` + `else echo FX_CAS_FAIL; fi`;
3629
3796
  const cas = await drv.exec(cmd, { timeoutMs: 30000 });
3630
3797
  const out = (cas.stdout || "").trim();
3631
3798
  if (out.startsWith("FX_CAS_FAIL")) {
@@ -3680,7 +3847,8 @@ var TOOL_DEFS = [
3680
3847
  type: "object",
3681
3848
  properties: {
3682
3849
  command: { type: "string", description: "shell 命令" },
3683
- timeout_ms: { type: "number", description: "超时毫秒(缺省 120000)" }
3850
+ timeout_ms: { type: "number", description: "超时毫秒(缺省 120000)" },
3851
+ layer: { type: "string", description: "目标层名(缺省=默认层——最近 push_ssh/push_into 的层)" }
3684
3852
  },
3685
3853
  required: ["command"]
3686
3854
  }
@@ -3697,7 +3865,8 @@ var TOOL_DEFS = [
3697
3865
  command: { type: "string", description: "长跑命令" },
3698
3866
  ready_marker: { type: "string", description: "就绪标记(如 'listening on')" },
3699
3867
  error_pattern: { type: "string", description: "错误模式(命中即收摊)" },
3700
- name: { type: "string", description: "会话名(人读前缀)" }
3868
+ name: { type: "string", description: "会话名(人读前缀)" },
3869
+ layer: { type: "string", description: "目标层名(缺省=默认层)" }
3701
3870
  },
3702
3871
  required: ["command"]
3703
3872
  }
@@ -3725,7 +3894,8 @@ var TOOL_DEFS = [
3725
3894
  parameters: {
3726
3895
  type: "object",
3727
3896
  properties: {
3728
- path: { type: "string" }
3897
+ path: { type: "string" },
3898
+ layer: { type: "string", description: "目标层名(缺省=默认层)" }
3729
3899
  },
3730
3900
  required: ["path"]
3731
3901
  }
@@ -3742,7 +3912,8 @@ var TOOL_DEFS = [
3742
3912
  path: { type: "string" },
3743
3913
  old: { type: "string", description: "待替换片段(含足够上下文唯一)" },
3744
3914
  new: { type: "string" },
3745
- all: { type: "boolean", description: "多处命中全替换(缺省报错)" }
3915
+ all: { type: "boolean", description: "多处命中全替换(缺省报错)" },
3916
+ layer: { type: "string", description: "目标层名(缺省=默认层)" }
3746
3917
  },
3747
3918
  required: ["path", "old", "new"]
3748
3919
  }
@@ -3759,22 +3930,87 @@ var TOOL_DEFS = [
3759
3930
  command: { type: "string" },
3760
3931
  duration_ms: { type: "number", description: "观察时长(缺省 8000)" },
3761
3932
  stream: { type: "string", enum: ["stdout", "stderr", "both"], description: "流过滤(缺省 both)" },
3762
- name: { type: "string" }
3933
+ name: { type: "string" },
3934
+ layer: { type: "string", description: "目标层名(缺省=默认层)" }
3763
3935
  },
3764
3936
  required: ["command"]
3765
3937
  }
3766
3938
  }
3939
+ },
3940
+ {
3941
+ type: "function",
3942
+ function: {
3943
+ name: "push_ssh",
3944
+ description: "建立命名 ssh 层(成为默认层——后续工具缺省落它)。同落点幂等复用(reused=true 返回既有层," + "不重复建连)+ 落点校验(宁重连不错主机)。跳板传 jump(-J 不占层)。",
3945
+ parameters: {
3946
+ type: "object",
3947
+ properties: {
3948
+ host: { type: "string", description: "目标主机(user@host 或 ssh config 别名)" },
3949
+ port: { type: "number", description: "SSH 端口(非 22 时指定)" },
3950
+ user: { type: "string", description: "登录用户名" },
3951
+ key: { type: "string", description: "私钥文件路径(-i)" },
3952
+ jump: { type: "string", description: "跳板(-J:途径机不占层)" },
3953
+ name: { type: "string", description: "层名(缺省=host;注册表内唯一,显式寻址用)" }
3954
+ },
3955
+ required: ["host"]
3956
+ }
3957
+ }
3958
+ },
3959
+ {
3960
+ type: "function",
3961
+ function: {
3962
+ name: "push_into",
3963
+ description: "从默认层(或 from 指定层)进入内层 shell(docker exec -i <ctr> sh / kubectl exec 等)——" + "enter 管线全量继承追加(父层 docker→再 su 不丢)。新层成为默认层。需 ssh 层为源。",
3964
+ parameters: {
3965
+ type: "object",
3966
+ properties: {
3967
+ enter_cmd: { type: "string", description: '进入命令,如 "docker exec -i web sh"' },
3968
+ label: { type: "string", description: "层显示标签" },
3969
+ from: { type: "string", description: "源层名(缺省=默认层)" },
3970
+ name: { type: "string", description: "层名(缺省=label)" }
3971
+ },
3972
+ required: ["enter_cmd"]
3973
+ }
3974
+ }
3975
+ },
3976
+ {
3977
+ type: "function",
3978
+ function: {
3979
+ name: "list_layers",
3980
+ description: "列出全部命名层(链路/状态/有效用户/默认层标记)。死层/退化层有警示标注(removeLayer 后按原参数重建)。",
3981
+ parameters: { type: "object", properties: {}, required: [] }
3982
+ }
3983
+ },
3984
+ {
3985
+ type: "function",
3986
+ function: {
3987
+ name: "remove_layer",
3988
+ description: "移出命名层(缺省连接保留在指纹缓存——再建同层零成本;destroy=true 才真销毁连接)。",
3989
+ parameters: {
3990
+ type: "object",
3991
+ properties: {
3992
+ name: { type: "string", description: "层名" },
3993
+ destroy: { type: "boolean", description: "真销毁连接(缺省 false=保留可复用)" }
3994
+ },
3995
+ required: ["name"]
3996
+ }
3997
+ }
3767
3998
  }
3768
3999
  ];
3769
4000
  function createAgentToolkit(stack, opts = {}) {
3770
4001
  const layer = opts.driver ? Promise.resolve(opts.driver) : stack.connect(opts.descriptor ?? { driver: "local" });
3771
4002
  const readyTimeoutMs = opts.readyTimeoutMs ?? 15000;
4003
+ const resolveDrv = async (layerName) => {
4004
+ if (layerName)
4005
+ return stack.layer(layerName).driver;
4006
+ return stack.active ?? await layer;
4007
+ };
3772
4008
  return {
3773
4009
  tools: TOOL_DEFS,
3774
4010
  layer,
3775
4011
  handlers: {
3776
- async run_command({ command, timeout_ms }) {
3777
- const drv = await layer;
4012
+ async run_command({ command, timeout_ms, layer: layerName }) {
4013
+ const drv = await resolveDrv(layerName);
3778
4014
  const r = await drv.exec(command, { timeoutMs: timeout_ms });
3779
4015
  return {
3780
4016
  stdout: r.stdout,
@@ -3783,8 +4019,8 @@ function createAgentToolkit(stack, opts = {}) {
3783
4019
  effective_user: r.effectiveUser
3784
4020
  };
3785
4021
  },
3786
- async spawn_and_observe({ command, ready_marker, error_pattern, name }) {
3787
- const drv = await layer;
4022
+ async spawn_and_observe({ command, ready_marker, error_pattern, name, layer: layerName }) {
4023
+ const drv = await resolveDrv(layerName);
3788
4024
  const h = await drv.execSession(command, name ? { name } : undefined);
3789
4025
  if (error_pattern) {
3790
4026
  drv.watchSession(h.sessionId, { pattern: error_pattern, once: false, thenStop: true });
@@ -3809,8 +4045,8 @@ function createAgentToolkit(stack, opts = {}) {
3809
4045
  }
3810
4046
  return { session_id: h.sessionId, log_base: h.logBase, note };
3811
4047
  },
3812
- async read_file({ path }) {
3813
- const drv = await layer;
4048
+ async read_file({ path, layer: layerName }) {
4049
+ const drv = await resolveDrv(layerName);
3814
4050
  if (!drv.fs.stat)
3815
4051
  throw new Error("该层不支持 read_file 的元数据(fs.stat 缺席)");
3816
4052
  const st = await drv.fs.stat(path);
@@ -3819,13 +4055,13 @@ function createAgentToolkit(stack, opts = {}) {
3819
4055
  const content = await drv.fs.readFile(path);
3820
4056
  return { path, content, size: st.size, mtime_ms: st.mtimeMs };
3821
4057
  },
3822
- async edit_file({ path, old, new: newStr, all }) {
3823
- const drv = await layer;
4058
+ async edit_file({ path, old, new: newStr, all, layer: layerName }) {
4059
+ const drv = await resolveDrv(layerName);
3824
4060
  const r = await editFile(drv, path, old, newStr, { all });
3825
4061
  return { path: r.path, occurrences: r.occurrences, bytes_before: r.bytesBefore, bytes_after: r.bytesAfter };
3826
4062
  },
3827
- async observe_stream({ command, duration_ms, stream, name }) {
3828
- const drv = await layer;
4063
+ async observe_stream({ command, duration_ms, stream, name, layer: layerName }) {
4064
+ const drv = await resolveDrv(layerName);
3829
4065
  const h = await drv.execSession(command, name ? { name } : undefined);
3830
4066
  const dur = duration_ms ?? 8000;
3831
4067
  await new Promise((r) => setTimeout(r, dur));
@@ -3837,6 +4073,49 @@ function createAgentToolkit(stack, opts = {}) {
3837
4073
  });
3838
4074
  return { session_id: h.sessionId, lines: filtered };
3839
4075
  },
4076
+ async push_ssh({ host, port, user, key, jump, name }) {
4077
+ const connParams = {};
4078
+ if (port != null)
4079
+ connParams.port = Number(port);
4080
+ if (user)
4081
+ connParams.user = String(user);
4082
+ if (key)
4083
+ connParams.keyFile = String(key);
4084
+ if (jump)
4085
+ connParams.jump = String(jump);
4086
+ const r = await stack.addLayer(name ?? String(host), {
4087
+ driver: "ssh",
4088
+ target: String(host),
4089
+ config: Object.keys(connParams).length ? { connParams } : undefined
4090
+ });
4091
+ return {
4092
+ layer: r.handle.name,
4093
+ reused: r.reused,
4094
+ reused_from: r.reusedFrom,
4095
+ note: r.reused ? "落点已由既有层持有——幂等复用(未重复建连)" : "新层已建并成为默认层"
4096
+ };
4097
+ },
4098
+ async push_into({ enter_cmd, label, from, name }) {
4099
+ const srcName = from ?? stack.activeLayer?.name;
4100
+ if (!srcName)
4101
+ throw new Error("无源层——先 push_ssh 建层,或显式传 from");
4102
+ const h = await stack.layerInto(srcName, name ?? label ?? enter_cmd.slice(0, 30), enter_cmd, { label });
4103
+ return { layer: h.name, target: h.descriptor.target };
4104
+ },
4105
+ async list_layers(_args) {
4106
+ return {
4107
+ text: stack.layerText(),
4108
+ layers: [...stack.layers.values()].map((h) => ({
4109
+ name: h.name,
4110
+ driver: h.descriptor.driver,
4111
+ target: h.descriptor.target
4112
+ }))
4113
+ };
4114
+ },
4115
+ async remove_layer({ name, destroy }) {
4116
+ await stack.removeLayer(name, { destroy });
4117
+ return { removed: name };
4118
+ },
3840
4119
  async stop_and_verify({ session_id }) {
3841
4120
  const drv = await layer;
3842
4121
  const r = await drv.stopSession(session_id);