@syrkos/filaxis 0.1.0-alpha.11 → 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.

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);
@@ -1,6 +1,7 @@
1
1
  import type { RuntimeDriver, RuntimeDescriptor } from "./driver.js";
2
2
  import { RuntimeFactory } from "./factory.js";
3
3
  import { EventBus } from "./events.js";
4
+ import { type SshConnParams } from "./ssh-helpers.js";
4
5
  import type { SessionEvent } from "./session-types.js";
5
6
  import { type FilaxisLogger } from "./logger.js";
6
7
  export type FilaxisEvents = SessionEvent | {
@@ -24,6 +25,23 @@ export interface RuntimeStackOptions {
24
25
  logger?: FilaxisLogger;
25
26
  factory?: RuntimeFactory;
26
27
  }
28
+ /** 命名层句柄(Paramiko 式「连接即句柄」的持有与寻址单元) */
29
+ export interface LayerHandle {
30
+ /** 层名(注册表内唯一——显式寻址零歧义) */
31
+ name: string;
32
+ descriptor: RuntimeDescriptor;
33
+ driver: RuntimeDriver;
34
+ /** 落点校验缓存(hostname 实测——宁重连不错主机) */
35
+ actualHostname?: string;
36
+ }
37
+ /** 层注册表快照(descriptor 即可序列化层身份——可 JSON 落盘跨进程恢复) */
38
+ export interface LayerSnapshot {
39
+ layers: Array<{
40
+ name: string;
41
+ descriptor: RuntimeDescriptor;
42
+ }>;
43
+ active?: string;
44
+ }
27
45
  export declare class RuntimeStack {
28
46
  /** 进程级驱动缓存:同连接拓扑指纹复用既有连接(杜绝孤儿/重复建连) */
29
47
  private static driverCache;
@@ -35,7 +53,10 @@ export declare class RuntimeStack {
35
53
  /**
36
54
  * 连接拓扑指纹(净室移植自源版 driverKey,身份白名单投影):
37
55
  * 显示标签/运行态缓存等非身份字段不进键——不把既有连接挤出缓存、不误建重复连接。
38
- * 注:ssh 层的 enterSteps 管线属 ssh Transport 批次,届时并入 cfg 投影。
56
+ * 环境身份三列(源版同款):pty 标志 + sshHops 逐跳链 + enters 进入管线(归一化——
57
+ * legacy enterCmd 与等价 enterPipeline 单步同键)。缺列后果:同 target/connParams
58
+ * 但进不同容器的两层指纹相同 → connect 复用错连接 → B 容器的命令跑进 A 容器
59
+ * (「执行环境即 Layer」的身份底线——平移时曾丢列,第十季检查补齐)。
39
60
  */
40
61
  static driverKey(d: RuntimeDescriptor): string;
41
62
  /** 建立层连接:指纹命中即复用(AC-07 连接计数语义) */
@@ -44,6 +65,67 @@ export declare class RuntimeStack {
44
65
  static evictDriver(d: RuntimeDescriptor): RuntimeDriver | undefined;
45
66
  /** 开放注册(透传 factory;AC-09 第三方 Transport 同权) */
46
67
  registerDriver(type: string, f: (descriptor: RuntimeDescriptor) => RuntimeDriver): void;
68
+ private namedLayers;
69
+ private activeName?;
70
+ /** 层注册表只读视图 */
71
+ get layers(): ReadonlyMap<string, LayerHandle>;
72
+ /** 默认层 driver(最近注册/setActive 的层;无层时 undefined——消费方自兜底) */
73
+ get active(): RuntimeDriver | undefined;
74
+ /** 默认层句柄(含层名;无层时 undefined) */
75
+ get activeLayer(): LayerHandle | undefined;
76
+ /**
77
+ * 注册命名层。descriptor 形态走 connect(指纹复用)+ 回环检测幂等;
78
+ * driver 形态直接登记现成句柄(pushChild/pushInto 派生产物、消费方自建连接)。
79
+ * 新层自动成为默认层。
80
+ */
81
+ addLayer(name: string, descriptorOrDriver: RuntimeDescriptor | RuntimeDriver): Promise<{
82
+ handle: LayerHandle;
83
+ reused: boolean;
84
+ reusedFrom?: string;
85
+ }>;
86
+ /** 从命名层派生进入层(docker exec/kubectl 等——enter 管线全量继承追加) */
87
+ layerInto(from: string, name: string, enterCmd: string, opts?: {
88
+ label?: string;
89
+ }): Promise<LayerHandle>;
90
+ /** 从命名层链式 ssh 派生子层(本层路径为前缀——途经继承;含回环检测幂等) */
91
+ sshInto(from: string, name: string, target: string, connParams?: SshConnParams): Promise<{
92
+ handle: LayerHandle;
93
+ reused: boolean;
94
+ reusedFrom?: string;
95
+ }>;
96
+ /** 按名取层(不存在时报错并附候选清单) */
97
+ layer(name: string): LayerHandle;
98
+ /** 设默认层(显式声明「接下来都在这层干」——无全局焦点指针,最近使用 + 显式声明双轨) */
99
+ setActive(name: string): LayerHandle;
100
+ /** 移出注册表(缺省连接保留在指纹缓存可复用;destroy=true 才真销毁) */
101
+ removeLayer(name: string, opts?: {
102
+ destroy?: boolean;
103
+ }): Promise<void>;
104
+ /** 层清单纯文本(零 ANSI/零表格——npm 零依赖红线;彩色表格留消费方) */
105
+ layerText(): string;
106
+ /** 层身份快照(可 JSON 落盘——跨进程恢复的凭证) */
107
+ snapshot(): LayerSnapshot;
108
+ /**
109
+ * 按快照重建(幂等:已在层的名字跳过):tmux 层 terminalId 命中=零成本 re-attach
110
+ * (现场全保);ssh 层=重握手 + enterPipeline 逐条重放(位置确定性恢复)。
111
+ */
112
+ restore(snap: LayerSnapshot): Promise<void>;
113
+ private assertNotDupName;
114
+ private requireLayer;
115
+ private isDriver;
116
+ /**
117
+ * descriptor 落点 identity(路径末节点)比对既有层全部路径节点。
118
+ * 判重边界(原版 push_ssh 语义忠实平移):**只在无 enter 管线的纯 ssh 落点层之间**——
119
+ * enter 层(容器/内层 shell)的落点是容器而非宿主机,hop identity 管不到(enter 管线
120
+ * 不进 layerPathIdentities):同宿主机两个不同容器层会撞 identity + hostname 同为宿主机
121
+ * → 误幂等返回错容器。enter 层的身份与复用归 driverKey(含 enters 三列)。
122
+ */
123
+ private findIdentityHit;
124
+ /** terminal=true:identity 命中在该层路径**末节点**(同落点构造性成立,免 hostname 探测);
125
+ * false:命中途经节点(identity 撞了中间跳——hostname 落点校验把关) */
126
+ private findIdentityHopHit;
127
+ /** 落点校验(宁重连不错主机):命中层实测 hostname vs 期望 host——不符=identity 撞途经节点 */
128
+ private verifyLanding;
47
129
  /**
48
130
  * 能力探测(AC-02:降级不靠猜)。结果缓存至注册表变化。
49
131
  * Windows 无 ControlMaster → mux=false(自动退化直连为既定行为)。