@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.
- package/dist/agent-toolkit.d.ts +46 -5
- package/dist/file-edit.d.ts +14 -6
- package/dist/index.js +322 -43
- package/dist/runtime-stack.d.ts +83 -1
- package/package.json +1 -1
- package/src/agent-toolkit.ts +143 -18
- package/src/file-edit.ts +33 -48
- package/src/runtime-stack.ts +290 -2
- package/src/ssh-pty-driver.ts +7 -4
package/dist/runtime-stack.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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(自动退化直连为既定行为)。
|
package/package.json
CHANGED
package/src/agent-toolkit.ts
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
*
|
|
89
|
-
* ①
|
|
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
|
-
|
|
105
|
-
//
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
-
`[ "$(${
|
|
144
|
-
`printf %s ${q(newB64)} | ${
|
|
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
|
|