@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.

@@ -0,0 +1,179 @@
1
+ Ran download and extract @syrkos/filaxis@0.1.0-alpha.11 · Exited with code 1
2
+ Ran download and extract @syrkos/filaxis@0.1.0-alpha.11 via curl
3
+ Analyzed filaxis.ts:L1-168
4
+ Analyzed agent-toolkit.ts:L1-254
5
+ Analyzed index.ts:L1-38
6
+ Analyzed runtime-stack.ts:L1-128
7
+ Analyzed ssh-helpers.ts:L1-225
8
+ Analyzed ssh-pty-driver.ts:L1-655
9
+ Ran check local SSH config
10
+ Ran test SSH jump via vm-ai-server to vm-homelab and check docker
11
+ Ran list existing docker images on vm-homelab
12
+ Ran check available utilities in alpine:latest container on vm-homelab · Exited with code 1
13
+ Ran check utilities in alpine:latest container
14
+ Created test_alpha11_docker.ts
15
+ Canceled test SshPtyDriver with jump to vm-homelab
16
+ Killed task Test SshPtyDriver with jump to vm-homelab
17
+ Edited test_alpha11_docker.ts
18
+ Ran test SshPtyDriver with jump vm-ai-server to vm-homelab
19
+ Killing task Test SshPtyDriver with jump vm-ai-server to vm-homelab
20
+ Edited test_alpha11_docker.ts
21
+ Ran test SshPtyDriver jump + docker enterCmd on vm-homelab
22
+
23
+ 已下载并解压 `@syrkos/filaxis@0.1.0-alpha.11`,正在通过 `vm-ai-server` 跳板机在 `vm-homelab` 的 Docker 容器层进行预检测试,稍候任务返回后继续部署新考题。
24
+
25
+ Analyzed remote-session-manager.ts:L1-286
26
+ Ran list files in filaxis 0.1.0-alpha.11 package
27
+ Edited test_alpha11_docker.ts
28
+ Ran test execSession and tailLines inside Alpine container via jump host
29
+
30
+ 正在验证 `SshPtyDriver` 在 `vm-ai-server` 跳板机 + `vm-homelab` Docker 容器内的异步会话观察(`execSession` + `tailLines` + `stopSession`)表现,稍候完成后即刻构建第四轮对比考题。
31
+
32
+ Created setup_exam3_docker.ts
33
+ Ran setup exam3-edge-a and exam3-edge-b containers on vm-homelab via vm-ai-server
34
+
35
+ 正在通过 `vm-ai-server` 跳板机在 `vm-homelab` 上初始化第四轮盲测的双容器沙箱(`exam3-edge-a` 与 `exam3-edge-b`,基于无 Python/jq 的纯净 `alpine:latest` 镜像),待环境就绪后立即并发启动两组 Gemini 3.8 Flash 子代理进行对决。
36
+
37
+ Used tool: invoke_subagent
38
+ Used tool: invoke_subagent
39
+
40
+ 已部署第四轮(`0.1.0-alpha.11`)三层穿透容器盲测考题,并同时启动两组 **Gemini 3.8 Flash** 子代理并发执行:
41
+
42
+ 1. **拓扑与环境**:`Windows 本地` $\rightarrow$ 跳板机 `vm-ai-server (10.0.20.2)` $\rightarrow$ 宿主机 `vm-homelab (10.0.20.1:3322)` $\rightarrow$ `alpine:latest` 纯净容器(**容器内无 `python3`、无 `jq`、无 `node`、无 `bash`**,仅 BusyBox `sh`)。
43
+ 2. **三项复合故障**:
44
+ - **故障 1(残留进程锁 + `stderr` 诊断流)**:清理 `/usr/local/bin/edge-lock-holder` 进程与 `/var/run/edge-dispatcher.lock`,并捕获 `/usr/local/bin/edge-dispatcher --diagnose` 输出在 `stderr` 上的故障规范;
45
+ - **故障 2(无 Python/jq 容器内的多块同构 JSON 修改)**:修改 `/etc/edge-dispatcher/webhooks.json` 中 `id="billing-sync"` 路由块的 3 个字段,严禁误伤 `global`、`order-created`、`inventory-audit` 三个同构块;
46
+ - **故障 3(含 `$2a`、`!`、`#`、`&` 特殊字符的密钥配置)**:将 `/etc/edge-dispatcher/credentials.conf` 中的 `BILLING_HMAC_SECRET` 更新为 `"whsec_9f$2a!Kp#88_Live&Key"`(含触发 Shell 变量展开的 `$2a` 和触发 `sed` 替换展开的 `&`)。
47
+ 3. **分组对决**:
48
+ - **A4 组(裸 SSH + Docker 组,`flash`)**:[`4ceee8c3-a385-49e4-8cf1-505e40db46cf`](conversation://4ceee8c3-a385-49e4-8cf1-505e40db46cf)(操作容器 `exam3-edge-a`)
49
+ - **B4 组(Filaxis `0.1.0-alpha.11` 组,`flash`)**:[`4e018b84-90c3-47d3-bcf8-28c8bea7f1d4`](conversation://4e018b84-90c3-47d3-bcf8-28c8bea7f1d4)(操作容器 `exam3-edge-b`)
50
+
51
+ 正在等待两个子代理完成任务回报,随后为您解析完整的 `transcript_full.jsonl` Token 账单与轨迹对比。
52
+
53
+ Found 8 subagents
54
+
55
+ A4 组(裸 SSH + Docker 组,`4ceee8c3-a385-49e4-8cf1-505e40db46cf`)已完成汇报;B4 组(Filaxis `0.1.0-alpha.11` 组,`4e018b84-90c3-47d3-bcf8-28c8bea7f1d4`)正在执行最后的 `--self-test` 校验,稍候其返回后立即为您输出第四轮完整 Token 与执行轨迹对比分析。
56
+
57
+ Created analyze_r4_docker.ts
58
+ Ran analyze Round 4 subagent transcripts and token usage
59
+ Analyzed analyze_r3_flash.ts:L1-65
60
+ Edited analyze_r4_docker.ts
61
+ Ran run full Round 4 transcript and token analysis
62
+ Analyzed output.txt:L35-227
63
+ Analyzed test_connect.ts:L1-27
64
+ Analyzed remediate.ts:L1-70
65
+ Ran clean up exam3-edge-a and exam3-edge-b containers on vm-homelab
66
+
67
+ 第四轮(`0.1.0-alpha.11`)**三层穿透(Windows $\rightarrow$ `vm-ai-server` 跳板机 $\rightarrow$ `vm-homelab` 宿主机 $\rightarrow$ `alpine:latest` 纯净容器)** 的 Gemini 3.8 Flash 同模型盲测已全部跑完,宿主机上的两个测试容器 `exam3-edge-a` / `exam3-edge-b` 也已清理完毕。
68
+
69
+ 这轮实验出现了一个**非常关键的转折**:
70
+ - 上一轮(`alpha.10`)卡死 Filaxis 组 32 轮(303 秒)的**多行 JSON 与特殊字符编辑,在这一轮实现了 `0` 报错、`1` 次命中过关**!
71
+ - 与此同时,由于容器换成了**无 `python3`、无 `jq`、无 `node` 的 `alpine:latest`**,裸 SSH 组再也无法用 Python 逃课,被迫在本地手搓 Base64 整文件覆盖;而 Filaxis 组则暴露出了 **CLI 入口(`bin/filaxis.ts`)尚未透传 `--jump` 与 `--enter` 参数** 的新摩擦点。
72
+
73
+ ---
74
+
75
+ ### 一、 第四轮核心指标总表(Gemini 3.8 Flash 盲测)
76
+
77
+ > 原始轨迹日志:
78
+ > - **A4 组(裸 SSH + Docker)**:[`4ceee8c3-a385-49e4-8cf1-505e40db46cf`](conversation://4ceee8c3-a385-49e4-8cf1-505e40db46cf)
79
+ > - **B4 组(Filaxis `0.1.0-alpha.11` + Docker)**:[`4e018b84-90c3-47d3-bcf8-28c8bea7f1d4`](conversation://4e018b84-90c3-47d3-bcf8-28c8bea7f1d4)
80
+ > - 完整逐轮对账输出:[output.txt](file:///C:/Users/dream/.gemini/antigravity/brain/f7c26020-3bdd-4477-b804-f050d731bc85/.system_generated/steps/9174/output.txt)
81
+
82
+ | 评测维度 | 对照 A4 组(Flash + 裸 SSH + Docker) | 实验 B4 组(Flash + Filaxis `alpha.11` + Docker) | 跨版本演进(对比第三轮 `alpha.10` B3 组) |
83
+ | :--- | :--- | :--- | :--- |
84
+ | **推理轮次 (Planner Turns)** | **23 轮** | **43 轮** | 从 53 轮降至 **43 轮**(**-10 轮**) |
85
+ | **工具调用次数 (Tool Calls)** | **22 次** | **42 次** | 从 52 次降至 **42 次**(**-10 次**) |
86
+ | **总耗时 (Wall-Clock Time)** | **234.0 秒** | **347.0 秒** | 从 540.0 秒降至 **347.0 秒**(**快了 193 秒 / -35.7%**) |
87
+ | **编辑重试 / 引号翻车轮次** | 0 次(退化为手搓 Base64 全量覆盖) | **0 次(`editFile` 两次调用均 100% 一次命中)** | 从 **32 轮卡死(303 秒)** 降至 **0 轮(0 秒)** |
88
+ | **杀进程 / Shell 踩坑次数** | **2 次**(`pkill -f` 自杀 + PowerShell 吞 `$?`) | **0 次**(`stdin` 管道注入,零 Shell 展开污染) | 保持 **0 事故** |
89
+ | **计费新增输入 (New Input)** | `85,885` | `160,682` | 稳定(`162.5K` $\rightarrow$ `160.7K`) |
90
+ | **缓存读取输入 (Cache Read)** | `506,795` | `1,825,715` | 因前 19 轮读源码垫高了前期上下文基数 |
91
+ | **总输入上下文 (Total Input)** | **`592,680` (592.7K)** | **`1,986,397` (1,986.4K)** | 源码阅读(19 轮)+ 写脚本执行(22 轮)占大头 |
92
+ | **总输出 Token (Output)** | **`13,779`** | **`22,873`** | 含编写 6 个 `.ts` 脚本的代码输出 |
93
+ | **最终 `--self-test` 结果** | `ALL_CHECKS_PASSED (exit=0)` | `ALL_CHECKS_PASSED (exit=0)` | 均一次通过验收 |
94
+
95
+ ---
96
+
97
+ ### 二、 两组精彩现场复盘:当 Alpine 容器里没有 Python 时发生了什么?
98
+
99
+ #### 1. 裸 SSH 组(A4):没了 Python,只能退化为「无锁整文件 Base64 覆盖」+ 两处多层 Shell 翻车
100
+ 在第三轮中,裸 SSH 组靠远端宿主机的 `python3` 5 秒改完了 JSON。但这轮进入纯净 `alpine:latest` 容器(无 `python3`、无 `jq`、无 `node`)后,裸 SSH 组的轨迹暴露了三层 Shell 嵌套(`PowerShell` $\rightarrow$ `ssh -J` $\rightarrow$ `docker exec sh -c`)的真实痛点:
101
+ - **踩坑 ①(Turn 9,`pkill -f` 自杀导致命令异常退出)**:
102
+ A4 执行 `ssh -J vm-ai-server vm-homelab "docker exec exam3-edge-a sh -c 'pkill -9 -f edge-lock-holder; rm -f /var/run/edge-dispatcher.lock; ps aux'"`。
103
+ 因为外层 `sh -c '...'` 的命令行参数本身就包含字符串 `edge-lock-holder`,BusyBox 的 `pkill -9 -f edge-lock-holder` **当场把正在执行命令的 `sh -c` 自己给 `SIGKILL` 了**(返回 `exit code 1`,后面的 `rm -f` 和 `ps aux` 根本没执行)!A4 被迫花了 4 轮(Turn 9~12)改用手动查 PID `kill -9 106` 才杀掉。
104
+ - **踩坑 ②(Turn 12,宿主 PowerShell 吞掉远端 `\$?`)**:
105
+ A4 执行 `ssh ... "docker exec exam3-edge-a sh -c 'ps aux ...; echo EXIT: \$?'"`,结果宿主机的 PowerShell 抢先将双引号里的 `$?` 展开成了 PowerShell 布尔值 `True`,打印出 `EXIT: True`!
106
+ - **手搓无锁 Base64(Turn 14~19,耗时 61 秒)**:
107
+ 面对含 `$2a!Kp#88_Live&Key` 的配置文件和同构 JSON,A4 发现容器里没有 Python,但有 `/bin/base64`(Turn 14),于是**在本地 PowerShell 里把整个文件全文硬编码敲了一遍**(Turn 15、16),算出 Base64 字符串后通过 `echo '<b64>' | docker exec -i exam3-edge-a sh -c 'base64 -d > ...'` 全量覆盖写回(Turn 17、19)。
108
+ - *隐患*:这种做法等于 Agent 在本地脑内重写整份文件再无锁覆盖(无 TOCTOU 校验,且一旦文件有几百行,Output Token 会成倍爆炸)。
109
+
110
+ #### 2. Filaxis `0.1.0-alpha.11` 组(B4):多层穿透与文件受控编辑“丝滑一次过”,但被 CLI 缺参数逼回写 TS 脚本
111
+ B4 组在 [`remediate.ts`](file:///C:/Users/dream/.gemini/antigravity/brain/4e018b84-90c3-47d3-bcf8-28c8bea7f1d4/scratch/remediate.ts#L4-L55) 中展现了 `SshPtyDriver` + `editFile` 在三层容器穿透下的威力:
112
+ ```ts
113
+ const drv = new SshPtyDriver({
114
+ driver: "ssh",
115
+ target: "vm-homelab",
116
+ config: {
117
+ connParams: { jump: "vm-ai-server" },
118
+ enterCmd: "docker exec -i exam3-edge-b sh",
119
+ },
120
+ });
121
+ ```
122
+ - **零嵌套转义**:通过 `renderExecChannel` 的 `docker exec -i exam3-edge-b sh -s` 标准输入管道注入,无论命令里有什么引号或 `$` 符号,宿主 PowerShell 与中间跳板机完全不解析。
123
+ - **`editFile` 100% 一次命中**:
124
+ - 修改 `credentials.conf` 中的 `BILLING_HMAC_SECRET="whsec_9f$2a!Kp#88_Live&Key"`(含 `$2a`、`!`、`#`、`&`):**1 次成功**(160B $\rightarrow$ 166B)。
125
+ - 修改 `webhooks.json` 中的 `billing-sync` 多行对象块:**1 次成功**(636B $\rightarrow$ 635B),其余三个同构块毫发无损。
126
+
127
+ ---
128
+
129
+ ### 三、 为什么 B4 组还是花了 43 轮(347 秒)?—— 罪魁祸首只有 6 行代码
130
+
131
+ 既然 `editFile` 0 翻车一次过,为什么 B4 组还是跑了 43 轮?
132
+ 拆解 [output.txt](file:///C:/Users/dream/.gemini/antigravity/brain/f7c26020-3bdd-4477-b804-f050d731bc85/.system_generated/steps/9174/output.txt) 的时间轴,原因一目了然:
133
+
134
+ | 阶段 | 轮次范围 | 消耗轮次 | 耗时 | 发生了什么? |
135
+ | :--- | :--- | :--- | :--- | :--- |
136
+ | **阶段 1:读源码找跳板与容器接口** | Turn 1 ~ 19 | **19 轮** | `93 秒` | Flash 在 Turn 5 读完 [`bin/filaxis.ts`](file:///C:/Users/dream/.gemini/antigravity/brain/f7c26020-3bdd-4477-b804-f050d731bc85/scratch/filaxis_alpha/package/bin/filaxis.ts#L22-L57) 发现:**CLI 只有 `--host --user --port --key`,没有 `--jump` 和 `--enter`!** 也就是说,`alpha.11` 在 `bin/filaxis.ts` 里刚加好的 `--stdin-json` / `--request-file`,在面对跳板机+容器时**根本用不上**!Flash 被迫连读 14 轮 `ssh-helpers.ts` 和 `ssh-pty-driver.ts` 源码去查 SDK 怎么配 `jump` 和 `enterCmd`。 |
137
+ | **阶段 2:写 6 个 `.ts` 脚本并执行** | Turn 20 ~ 41 | **14 轮** | `199 秒` | 因为不能用 CLI 一行流,Flash 每做一步操作都要先 `write_file(xxx.ts)` 再 `run_command(bun run xxx.ts)`(共写了 `test_connect.ts`、`step1_diagnose.ts`、`inspect_files.ts`、`run_self_test.ts`、`remediate.ts`、`final_check.ts` 6 个脚本)。 |
138
+ | **阶段 3:单脚本串行多次 SSH 触发后台轮询** | Turn 27~28, 34~38, 41 | **8 轮** | `40 秒` | 因为在单个 `.ts` 脚本里串行调了 4~6 次 `drv.exec` / `editFile`(每次跨跳板机+容器约 2.2s),单脚本总耗时约 12~15s,刚好超过 `run_command` 的 10s 同步等待阈值,触发了 8 次 `manage_task(status)` 轮询。 |
139
+
140
+ ---
141
+
142
+ ### 四、 给 `0.1.0-alpha.12` 的「最后一公里」建议:打通 CLI 的多层穿透参数
143
+
144
+ 目前 Filaxis 的**内核层(`SshPtyDriver` + `renderExecChannel` + `editFile`)已经完全支持跳板机(`jump`)和容器/内层 Shell 穿透(`enterPipeline` / `enterCmd`)**,而 `alpha.11` 新增的 `--request-file` / `--stdin-json` 也已经在 [`bin/filaxis.ts`](file:///C:/Users/dream/.gemini/antigravity/brain/f7c26020-3bdd-4477-b804-f050d731bc85/scratch/filaxis_alpha/package/bin/filaxis.ts#L98-L120) 就位了。
145
+
146
+ 唯一断掉的一环,就是 [`bin/filaxis.ts` (第 22-57 行)](file:///C:/Users/dream/.gemini/antigravity/brain/f7c26020-3bdd-4477-b804-f050d731bc85/scratch/filaxis_alpha/package/bin/filaxis.ts#L22-L57) 还没把 `jump` 和 `enter` 暴露成命令行参数!只需要在 `bin/filaxis.ts` 里加上 **6 行代码**:
147
+
148
+ ```ts
149
+ // package/bin/filaxis.ts
150
+ function parseOpts(argv: string[]): {
151
+ host?: string; user?: string; port?: number; key?: string;
152
+ jump?: string; enter?: string[];
153
+ ...
154
+ } {
155
+ const o: ReturnType<typeof parseOpts> = { positional: [], enter: [] };
156
+ for (let i = 0; i < argv.length; i++) {
157
+ ...
158
+ case "--jump": case "-J": o.jump = argv[++i]; break;
159
+ case "--enter": o.enter!.push(argv[++i]!); break;
160
+ case "--docker": o.enter!.push(`docker exec -i ${argv[++i]!} sh`); break;
161
+ ```
162
+ 并在 `driverFor(opts)` 中透传:
163
+ ```ts
164
+ if (opts.host) {
165
+ return new SshPtyDriver({
166
+ driver: "ssh", target: opts.host,
167
+ config: {
168
+ connParams: { user: opts.user, port: opts.port, keyFile: opts.key, jump: opts.jump },
169
+ enterPipeline: opts.enter?.length ? opts.enter.map((cmd) => ({ cmd })) : undefined,
170
+ },
171
+ });
172
+ }
173
+ ```
174
+
175
+ 一旦补上这几个参数:
176
+ 1. Agent 看到 `filaxis --help` 就能直接写:
177
+ `filaxis edit --host vm-homelab -J vm-ai-server --docker exam3-edge-b /etc/edge-dispatcher/webhooks.json --request-file req.json`
178
+ 2. **前 19 轮读源码(93 秒)直接归零**;
179
+ 3. **6 次 `write_file(*.ts)` + 8 次后台任务轮询(14 轮)直接归零**,因为单次 CLI 调用都是 2~4 秒内同步返回的原子操作!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syrkos/filaxis",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.13",
4
4
  "description": "牵丝 Filaxis — 面向自动化与 Agent 的远程执行运行时 SDK(Syrkos · 漩核)。千里一线,执行有迹。",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -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 { editFile } from "./file-edit.js";
@@ -27,15 +30,26 @@ export interface AgentToolHandlers {
27
30
  edit_file(args: { path: string; old: string; new: string; all?: boolean; layer?: string }): Promise<{
28
31
  path: string; occurrences: number; bytes_before: number; bytes_after: number;
29
32
  }>;
30
- observe_stream(args: { command: string; duration_ms?: number; stream?: "stdout" | "stderr" | "both"; name?: string }): Promise<{
33
+ observe_stream(args: { command: string; duration_ms?: number; stream?: "stdout" | "stderr" | "both"; name?: string; layer?: string }): Promise<{
31
34
  session_id: string; lines: Array<{ stream: string; text: string }>;
32
35
  }>;
33
- spawn_and_observe(args: { command: string; ready_marker?: string; error_pattern?: string; name?: string }): Promise<{
36
+ spawn_and_observe(args: { command: string; ready_marker?: string; error_pattern?: string; name?: string; layer?: string }): Promise<{
34
37
  session_id: string; log_base: string; note?: string;
35
38
  }>;
36
39
  stop_and_verify(args: { session_id: string }): Promise<{
37
40
  status: "exited" | "stopped" | "unknown"; exit_code?: number | null; note?: string;
38
41
  }>;
42
+ // ── 层族(第十季命名层注册表——Paramiko 式句柄寻址)──
43
+ push_ssh(args: { host: string; port?: number; user?: string; key?: string; jump?: string; name?: string }): Promise<{
44
+ layer: string; reused: boolean; reused_from?: string; note?: string;
45
+ }>;
46
+ push_into(args: { enter_cmd: string; label?: string; from?: string; name?: string }): Promise<{
47
+ layer: string; target?: string;
48
+ }>;
49
+ list_layers(args: Record<string, never>): Promise<{
50
+ text: string; layers: Array<{ name: string; driver: string; target?: string }>;
51
+ }>;
52
+ remove_layer(args: { name: string; destroy?: boolean }): Promise<{ removed: string }>;
39
53
  }
40
54
 
41
55
  export interface AgentToolkit {
@@ -66,6 +80,7 @@ const TOOL_DEFS: AgentToolDef[] = [
66
80
  properties: {
67
81
  command: { type: "string", description: "shell 命令" },
68
82
  timeout_ms: { type: "number", description: "超时毫秒(缺省 120000)" },
83
+ layer: { type: "string", description: "目标层名(缺省=默认层——最近 push_ssh/push_into 的层)" },
69
84
  },
70
85
  required: ["command"],
71
86
  },
@@ -79,13 +94,14 @@ const TOOL_DEFS: AgentToolDef[] = [
79
94
  "启动长跑任务(立即返回句柄不阻塞)。可选就绪标记(ready_marker——回溯热窗口," +
80
95
  "快输出进程竞态安全)与错误模式(error_pattern——命中即自动收摊防遗忘泄漏)。" +
81
96
  "后续用 stop_and_verify 停止并核实终态。",
82
- parameters: {
97
+ parameters: {
83
98
  type: "object",
84
99
  properties: {
85
100
  command: { type: "string", description: "长跑命令" },
86
101
  ready_marker: { type: "string", description: "就绪标记(如 'listening on')" },
87
102
  error_pattern: { type: "string", description: "错误模式(命中即收摊)" },
88
103
  name: { type: "string", description: "会话名(人读前缀)" },
104
+ layer: { type: "string", description: "目标层名(缺省=默认层)" },
89
105
  },
90
106
  required: ["command"],
91
107
  },
@@ -116,6 +132,7 @@ const TOOL_DEFS: AgentToolDef[] = [
116
132
  type: "object",
117
133
  properties: {
118
134
  path: { type: "string" },
135
+ layer: { type: "string", description: "目标层名(缺省=默认层)" },
119
136
  },
120
137
  required: ["path"],
121
138
  },
@@ -135,6 +152,7 @@ const TOOL_DEFS: AgentToolDef[] = [
135
152
  old: { type: "string", description: "待替换片段(含足够上下文唯一)" },
136
153
  new: { type: "string" },
137
154
  all: { type: "boolean", description: "多处命中全替换(缺省报错)" },
155
+ layer: { type: "string", description: "目标层名(缺省=默认层)" },
138
156
  },
139
157
  required: ["path", "old", "new"],
140
158
  },
@@ -154,11 +172,77 @@ const TOOL_DEFS: AgentToolDef[] = [
154
172
  duration_ms: { type: "number", description: "观察时长(缺省 8000)" },
155
173
  stream: { type: "string", enum: ["stdout", "stderr", "both"], description: "流过滤(缺省 both)" },
156
174
  name: { type: "string" },
175
+ layer: { type: "string", description: "目标层名(缺省=默认层)" },
157
176
  },
158
177
  required: ["command"],
159
178
  },
160
179
  },
161
180
  },
181
+ {
182
+ type: "function",
183
+ function: {
184
+ name: "push_ssh",
185
+ description:
186
+ "建立命名 ssh 层(成为默认层——后续工具缺省落它)。同落点幂等复用(reused=true 返回既有层," +
187
+ "不重复建连)+ 落点校验(宁重连不错主机)。跳板传 jump(-J 不占层)。",
188
+ parameters: {
189
+ type: "object",
190
+ properties: {
191
+ host: { type: "string", description: "目标主机(user@host 或 ssh config 别名)" },
192
+ port: { type: "number", description: "SSH 端口(非 22 时指定)" },
193
+ user: { type: "string", description: "登录用户名" },
194
+ key: { type: "string", description: "私钥文件路径(-i)" },
195
+ jump: { type: "string", description: "跳板(-J:途径机不占层)" },
196
+ name: { type: "string", description: "层名(缺省=host;注册表内唯一,显式寻址用)" },
197
+ },
198
+ required: ["host"],
199
+ },
200
+ },
201
+ },
202
+ {
203
+ type: "function",
204
+ function: {
205
+ name: "push_into",
206
+ description:
207
+ "从默认层(或 from 指定层)进入内层 shell(docker exec -i <ctr> sh / kubectl exec 等)——" +
208
+ "enter 管线全量继承追加(父层 docker→再 su 不丢)。新层成为默认层。需 ssh 层为源。",
209
+ parameters: {
210
+ type: "object",
211
+ properties: {
212
+ enter_cmd: { type: "string", description: "进入命令,如 \"docker exec -i web sh\"" },
213
+ label: { type: "string", description: "层显示标签" },
214
+ from: { type: "string", description: "源层名(缺省=默认层)" },
215
+ name: { type: "string", description: "层名(缺省=label)" },
216
+ },
217
+ required: ["enter_cmd"],
218
+ },
219
+ },
220
+ },
221
+ {
222
+ type: "function",
223
+ function: {
224
+ name: "list_layers",
225
+ description:
226
+ "列出全部命名层(链路/状态/有效用户/默认层标记)。死层/退化层有警示标注(removeLayer 后按原参数重建)。",
227
+ parameters: { type: "object", properties: {}, required: [] },
228
+ },
229
+ },
230
+ {
231
+ type: "function",
232
+ function: {
233
+ name: "remove_layer",
234
+ description:
235
+ "移出命名层(缺省连接保留在指纹缓存——再建同层零成本;destroy=true 才真销毁连接)。",
236
+ parameters: {
237
+ type: "object",
238
+ properties: {
239
+ name: { type: "string", description: "层名" },
240
+ destroy: { type: "boolean", description: "真销毁连接(缺省 false=保留可复用)" },
241
+ },
242
+ required: ["name"],
243
+ },
244
+ },
245
+ },
162
246
  ];
163
247
 
164
248
  /** 生成 Agent 工具集(定义层 + 执行层;宿主桥接到任意 LLM 框架) */
@@ -167,21 +251,27 @@ export function createAgentToolkit(stack: RuntimeStack, opts: AgentToolkitOption
167
251
  ? Promise.resolve(opts.driver)
168
252
  : stack.connect(opts.descriptor ?? { driver: "local" });
169
253
  const readyTimeoutMs = opts.readyTimeoutMs ?? 15_000;
254
+ /** 层寻址(第十季注册表):显式层名 > 注册表默认层(最近 push)> 初始层——
255
+ * 未用层族时与旧行为逐字节一致(零破坏),push 后六工具自动落新默认层 */
256
+ const resolveDrv = async (layerName?: string): Promise<RuntimeDriver> => {
257
+ if (layerName) return stack.layer(layerName).driver;
258
+ return stack.active ?? await layer;
259
+ };
170
260
 
171
261
  return {
172
262
  tools: TOOL_DEFS,
173
263
  layer,
174
264
  handlers: {
175
- async run_command({ command, timeout_ms }) {
176
- const drv = await layer;
265
+ async run_command({ command, timeout_ms, layer: layerName }) {
266
+ const drv = await resolveDrv(layerName);
177
267
  const r = await drv.exec(command, { timeoutMs: timeout_ms });
178
268
  return {
179
269
  stdout: r.stdout, stderr: r.stderr, exit_code: r.exitCode,
180
270
  effective_user: r.effectiveUser,
181
271
  };
182
272
  },
183
- async spawn_and_observe({ command, ready_marker, error_pattern, name }) {
184
- const drv = await layer;
273
+ async spawn_and_observe({ command, ready_marker, error_pattern, name, layer: layerName }) {
274
+ const drv = await resolveDrv(layerName);
185
275
  const h = await drv.execSession(command, name ? { name } : undefined);
186
276
  if (error_pattern) {
187
277
  drv.watchSession(h.sessionId, { pattern: error_pattern, once: false, thenStop: true });
@@ -202,21 +292,21 @@ export function createAgentToolkit(stack: RuntimeStack, opts: AgentToolkitOption
202
292
  }
203
293
  return { session_id: h.sessionId, log_base: h.logBase, note };
204
294
  },
205
- async read_file({ path }) {
206
- const drv = await layer;
295
+ async read_file({ path, layer: layerName }) {
296
+ const drv = await resolveDrv(layerName);
207
297
  if (!drv.fs.stat) throw new Error("该层不支持 read_file 的元数据(fs.stat 缺席)");
208
298
  const st = await drv.fs.stat(path);
209
299
  if (!st) throw new Error(`文件不存在: ${path}`);
210
300
  const content = await drv.fs.readFile(path);
211
301
  return { path, content, size: st.size, mtime_ms: st.mtimeMs };
212
302
  },
213
- async edit_file({ path, old, new: newStr, all }) {
214
- const drv = await layer;
303
+ async edit_file({ path, old, new: newStr, all, layer: layerName }) {
304
+ const drv = await resolveDrv(layerName);
215
305
  const r = await editFile(drv, path, old, newStr, { all });
216
306
  return { path: r.path, occurrences: r.occurrences, bytes_before: r.bytesBefore, bytes_after: r.bytesAfter };
217
307
  },
218
- async observe_stream({ command, duration_ms, stream, name }) {
219
- const drv = await layer;
308
+ async observe_stream({ command, duration_ms, stream, name, layer: layerName }) {
309
+ const drv = await resolveDrv(layerName);
220
310
  const h = await drv.execSession(command, name ? { name } : undefined);
221
311
  const dur = duration_ms ?? 8000;
222
312
  await new Promise((r) => setTimeout(r, dur));
@@ -238,6 +328,41 @@ export function createAgentToolkit(stack: RuntimeStack, opts: AgentToolkitOption
238
328
  await drv.stopSession(h.sessionId).catch(() => undefined);
239
329
  return { session_id: h.sessionId, lines: filtered };
240
330
  },
331
+ // ── 层族(第十季命名层注册表)──
332
+ async push_ssh({ host, port, user, key, jump, name }) {
333
+ const connParams: Record<string, unknown> = {};
334
+ if (port != null) connParams.port = Number(port);
335
+ if (user) connParams.user = String(user);
336
+ if (key) connParams.keyFile = String(key);
337
+ if (jump) connParams.jump = String(jump);
338
+ const r = await stack.addLayer(name ?? String(host), {
339
+ driver: "ssh", target: String(host),
340
+ config: Object.keys(connParams).length ? { connParams } : undefined,
341
+ });
342
+ return {
343
+ layer: r.handle.name, reused: r.reused, reused_from: r.reusedFrom,
344
+ note: r.reused ? "落点已由既有层持有——幂等复用(未重复建连)" : "新层已建并成为默认层",
345
+ };
346
+ },
347
+ async push_into({ enter_cmd, label, from, name }) {
348
+ const srcName = from ?? stack.activeLayer?.name;
349
+ if (!srcName) throw new Error("无源层——先 push_ssh 建层,或显式传 from");
350
+ const h = await stack.layerInto(srcName, name ?? label ?? enter_cmd.slice(0, 30), enter_cmd, { label });
351
+ return { layer: h.name, target: h.descriptor.target };
352
+ },
353
+ async list_layers(_args) {
354
+ void _args;
355
+ return {
356
+ text: stack.layerText(),
357
+ layers: [...stack.layers.values()].map((h) => ({
358
+ name: h.name, driver: h.descriptor.driver, target: h.descriptor.target,
359
+ })),
360
+ };
361
+ },
362
+ async remove_layer({ name, destroy }) {
363
+ await stack.removeLayer(name, { destroy });
364
+ return { removed: name };
365
+ },
241
366
  async stop_and_verify({ session_id }) {
242
367
  const drv = await layer;
243
368
  const r = (await drv.stopSession(session_id)) as { status?: string; exitCode?: number | null };
package/src/file-edit.ts CHANGED
@@ -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)。
@@ -42,51 +48,24 @@ const q = (s: string) => `'${s.replace(/'/g, "'\\''")}'`;
42
48
  /** 单引号安全检查(base64/stamp 等嵌入 shell 引号前的红线) */
43
49
  const shellSafe = (s: string) => !s.includes("'") && !s.includes("\n");
44
50
 
45
- interface ShellDialect {
46
- statCmd: string; // 戳命令(输出 "mtime size")
47
- enc: string; // base64 编码 argv(无换行)
48
- dec: string; // base64 解码 argv
49
- /** 内容指纹命令模板({f}=文件——三族 fallback 链:sha256sum → sha256 -q → openssl;
50
- * CAS 戳的内容分量:stat %Y 秒级粒度有同秒同 size 盲区——内容指纹封死) */
51
- shaCmd: string;
52
- }
53
-
54
51
  /**
55
52
  * stat 戳命令模板({f}=文件——行为 fallback 链,不探版本:GNU -c 优先、
56
53
  * 失败自动 BSD -f——CI 实证版本探测在 Ubuntu 误判走 BSD 分支)。
57
54
  */
58
55
  const statCmdOf = (f: string) => `stat -c '%Y %s' ${f} 2>/dev/null || stat -f '%m %z' ${f} 2>/dev/null`;
59
56
 
60
- /** 每 driver 的方言缓存(key=driver 描述符投影——仅 base64 族) */
61
- const dialectCache = new Map<string, ShellDialect>();
62
-
63
- async function dialectOf(drv: RuntimeDriver): Promise<ShellDialect> {
64
- const key = JSON.stringify(drv.descriptor);
65
- const hit = dialectCache.get(key);
66
- if (hit) return hit;
67
- // 一次往返探测 base64 族(-w 0 GNU / 解码 -d vs -D)
68
- const probe = await drv.exec(
69
- "echo FX2=$(printf filaxis | base64 -w 0 2>/dev/null | head -c 8);" +
70
- "echo FX3=$(printf ZmlsYXhpcw== | base64 -d 2>/dev/null | head -c 7);" +
71
- "echo FX4=$(printf ZmlsYXhpcw== | base64 -D 2>/dev/null | head -c 7)",
72
- { timeoutMs: 15_000 },
73
- );
74
- const out = probe.stdout || "";
75
- const b64Gnu = /FX2=ZmlsYXhp/.test(out);
76
- const decD = /FX4=filaxis/.test(out);
77
- const d: ShellDialect = {
78
- statCmd: "", // 行为 fallback(statCmdOf)——不走探测
79
- enc: b64Gnu ? "base64 -w 0" : "base64",
80
- dec: decD ? "base64 -D" : "base64 -d",
81
- shaCmd: `{ sha256sum ${"{f}"} 2>/dev/null || sha256 -q ${"{f}"} 2>/dev/null || openssl dgst -sha256 ${"{f}"} 2>/dev/null | tail -1; } | head -c 12`,
82
- };
83
- dialectCache.set(key, d);
84
- return d;
85
- }
57
+ /**
58
+ * 内容指纹命令(三族 fallback 链:sha256sum → sha256 -q → openssl;
59
+ * CAS 戳的内容分量:stat %Y 秒级粒度有同秒同 size 盲区——内容指纹封死)。
60
+ */
61
+ const shaCmdOf = (f: string) =>
62
+ `{ sha256sum ${f} 2>/dev/null || sha256 -q ${f} 2>/dev/null || openssl dgst -sha256 ${f} 2>/dev/null | tail -1; } | head -c 12`;
86
63
 
87
64
  /**
88
- * 受控编辑(合成 2 次往返 + 远端 CAS 原子写回):
89
- * ① 读+戳(1 次往返):`stat 戳; __FXS__; base64 读` —— 拿 (mtime,size)+全文
65
+ * 受控编辑(真 2 次往返 + 远端 CAS 原子写回):
66
+ * ① 读+戳+方言探测(1 次往返):`stat 戳; sha 戳; __FXS__; base64 -d 探测;
67
+ * __FXD__; base64 < file` —— (mtime,size)+全文+解码 argv 一次带回
68
+ * (__FXD__ 含下划线不在 base64 字母表——与 __FXS__ 同款防撞)
90
69
  * ② 精确替换(本地):0 命中/多处命中(未开 all)拒绝
91
70
  * ③ CAS 原子写回(1 次往返):远端一条命令内完成「戳比对 + tmp 写 + mv 原子替换 + 新戳回执」
92
71
  * —— 戳不符(他人先写)→ EditConflictError;写后回执新戳
@@ -101,12 +80,16 @@ export async function editFile(
101
80
  newStr: string,
102
81
  opts: EditFileOptions = {},
103
82
  ): Promise<EditFileResult> {
104
- const dialect = await dialectOf(drv);
105
- // 读+戳 合成一次往返(读时戳与读到的内容同窗——读后他人写 → CAS 必不符)
83
+ // 读+戳+探测 合成一次往返(读时戳与读到的内容同窗——读后他人写 → CAS 必不符)。
84
+ // 编码用 base64 < file(stdin 全方言通吃,折行由解析侧 \s+ 归一消化——无需 -w 0 探测);
85
+ // 解码探测 printf | base64 -d:GNU/busybox 出 filaxis → -d;BSD 失败 → -D(行为判定
86
+ // 与旧探测等价)。CAS 写回用本次确定的 argv——不赌 || fallback 的 stdin 双消费。
106
87
  const qp = q(path);
107
- const readCmd = `${statCmdOf(qp)}; ${dialect.shaCmd.replace(/\{f\}/g, qp)}; echo __FXS__; ${dialect.enc} ${qp} 2>/dev/null`;
88
+ const readCmd =
89
+ `${statCmdOf(qp)}; ${shaCmdOf(qp)}; echo __FXS__; ` +
90
+ `printf ZmlsYXhpcw== | base64 -d 2>/dev/null; echo __FXD__; base64 < ${qp} 2>/dev/null`;
108
91
  const r = await drv.exec(readCmd, { timeoutMs: 30_000 });
109
- const [stampPart, b64Part] = (r.stdout || "").split("__FXS__");
92
+ const [stampPart, restPart] = (r.stdout || "").split("__FXS__");
110
93
  const originStampRaw = (stampPart ?? "").trim();
111
94
  const originLines = originStampRaw.split(/\n/);
112
95
  const statStamp = (originLines[0] ?? "").trim();
@@ -120,7 +103,9 @@ export async function editFile(
120
103
  size: parseInt(originParts[1] ?? "0", 10),
121
104
  };
122
105
  if (!origin.size || !shaStamp) throw new Error(`文件不存在或指纹异常: ${path}(${originStampRaw})`);
123
- const content0 = Buffer.from((b64Part ?? "").replace(/\s+/g, ""), "base64").toString("utf-8");
106
+ const [decPart, b64Part = ""] = (restPart ?? "").split("__FXD__");
107
+ const dec = (decPart ?? "").trim() === "filaxis" ? "base64 -d" : "base64 -D";
108
+ const content0 = Buffer.from(b64Part.replace(/\s+/g, ""), "base64").toString("utf-8");
124
109
 
125
110
  const occurrences = content0.split(oldStr).length - 1;
126
111
  if (occurrences === 0) {
@@ -140,8 +125,8 @@ export async function editFile(
140
125
 
141
126
  const cmd =
142
127
  `if [ "$(${statCmdOf(qp)})" = "${statStamp}" ] && ` +
143
- `[ "$(${dialect.shaCmd.replace(/\{f\}/g, qp)})" = "${shaStamp}" ]; then ` +
144
- `printf %s ${q(newB64)} | ${dialect.dec} > ${qp}.fx-tmp && mv ${qp}.fx-tmp ${qp} && ` +
128
+ `[ "$(${shaCmdOf(qp)})" = "${shaStamp}" ]; then ` +
129
+ `printf %s ${q(newB64)} | ${dec} > ${qp}.fx-tmp && mv ${qp}.fx-tmp ${qp} && ` +
145
130
  `echo "FX_OK $(stat -c '%Y %s' ${qp} 2>/dev/null || stat -f '%m %z' ${qp} 2>/dev/null)"; ` +
146
131
  `else echo FX_CAS_FAIL; fi`;
147
132