@syrkos/filaxis 0.1.0-alpha.10

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.
Files changed (85) hide show
  1. package/AGENTS.md +46 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +10 -0
  4. package/README.md +61 -0
  5. package/bin/filaxis.ts +112 -0
  6. package/dist/agent-toolkit.d.ts +99 -0
  7. package/dist/dialect.d.ts +31 -0
  8. package/dist/driver.d.ts +130 -0
  9. package/dist/events.d.ts +26 -0
  10. package/dist/factory.d.ts +16 -0
  11. package/dist/file-edit.d.ts +43 -0
  12. package/dist/file-transfer.d.ts +44 -0
  13. package/dist/index.d.ts +38 -0
  14. package/dist/index.js +4200 -0
  15. package/dist/layer-discovery.d.ts +30 -0
  16. package/dist/layer-env.d.ts +95 -0
  17. package/dist/local-driver.d.ts +47 -0
  18. package/dist/local-session-manager.d.ts +98 -0
  19. package/dist/logger.d.ts +12 -0
  20. package/dist/remote-session-manager.d.ts +64 -0
  21. package/dist/runtime-stack.d.ts +52 -0
  22. package/dist/session-buffer.d.ts +73 -0
  23. package/dist/session-types.d.ts +74 -0
  24. package/dist/shell-discovery.d.ts +4 -0
  25. package/dist/ssh-helpers.d.ts +88 -0
  26. package/dist/ssh-pty-driver.d.ts +74 -0
  27. package/dist/stream-session-manager.d.ts +83 -0
  28. package/dist/stream-window.d.ts +124 -0
  29. package/dist/terminal/command-executer.d.ts +73 -0
  30. package/dist/terminal/output-buffer.d.ts +49 -0
  31. package/dist/terminal/session-launcher.d.ts +28 -0
  32. package/dist/terminal/session-registry.d.ts +29 -0
  33. package/dist/terminal/tmux-invoker.d.ts +8 -0
  34. package/dist/terminal/types.d.ts +56 -0
  35. package/dist/terminal-manager.d.ts +30 -0
  36. package/dist/tmux-driver.d.ts +49 -0
  37. package/dist/vt100-screen.d.ts +46 -0
  38. package/docs/DEVELOPMENT.md +38 -0
  39. package/docs/KNOWN-ISSUES.md +35 -0
  40. package/docs/implementation-notes.md +57 -0
  41. package/docs/skills/cross-host-diagnosis.md +21 -0
  42. package/docs/skills/file-sync.md +22 -0
  43. package/docs/skills/longrun-observe.md +20 -0
  44. package/docs/skills/stream-window.md +28 -0
  45. package/docs//345/256/214/346/225/264/345/267/241/346/243/200/346/212/245/345/221/212.md +141 -0
  46. package/docs//346/265/213/350/257/2252.md +112 -0
  47. package/docs//346/265/213/350/257/2253.md +73 -0
  48. package/docs//346/265/213/350/257/2254.md +138 -0
  49. package/docs//346/265/213/350/257/2255.md +124 -0
  50. package/docs//347/254/254/344/272/214/344/273/275/345/267/241/346/243/200/346/212/245/345/221/212.md +103 -0
  51. package/examples/agent-tools.ts +74 -0
  52. package/examples/cross-host-diagnosis.ts +49 -0
  53. package/examples/longrun-observe.ts +46 -0
  54. package/package.json +47 -0
  55. package/src/agent-toolkit.ts +247 -0
  56. package/src/dialect.ts +100 -0
  57. package/src/driver.ts +142 -0
  58. package/src/events.ts +96 -0
  59. package/src/factory.ts +51 -0
  60. package/src/file-edit.ts +195 -0
  61. package/src/file-transfer.ts +196 -0
  62. package/src/index.ts +38 -0
  63. package/src/layer-discovery.ts +79 -0
  64. package/src/layer-env.ts +331 -0
  65. package/src/local-driver.ts +325 -0
  66. package/src/local-session-manager.ts +397 -0
  67. package/src/logger.ts +19 -0
  68. package/src/remote-session-manager.ts +286 -0
  69. package/src/runtime-stack.ts +128 -0
  70. package/src/session-buffer.ts +183 -0
  71. package/src/session-types.ts +78 -0
  72. package/src/shell-discovery.ts +52 -0
  73. package/src/ssh-helpers.ts +225 -0
  74. package/src/ssh-pty-driver.ts +655 -0
  75. package/src/stream-session-manager.ts +296 -0
  76. package/src/stream-window.ts +309 -0
  77. package/src/terminal/command-executer.ts +374 -0
  78. package/src/terminal/output-buffer.ts +126 -0
  79. package/src/terminal/session-launcher.ts +146 -0
  80. package/src/terminal/session-registry.ts +98 -0
  81. package/src/terminal/tmux-invoker.ts +65 -0
  82. package/src/terminal/types.ts +85 -0
  83. package/src/terminal-manager.ts +83 -0
  84. package/src/tmux-driver.ts +167 -0
  85. package/src/vt100-screen.ts +166 -0
@@ -0,0 +1,247 @@
1
+ /**
2
+ * agent-toolkit.ts — 原生 Agent 工具集(Gemini 试用反馈 #3)
3
+ *
4
+ * 一行 `createAgentToolkit(stack)` 生成 LLM 工具面(run_command / spawn_and_observe /
5
+ * stop_and_verify)——OpenAI/Anthropic/Vercel AI SDK 通吃的 {tools, handlers} 双层结构:
6
+ * 定义层喂模型,执行层留宿主(桥到任意框架)。语义契约(退出码是数据/终态三态/
7
+ * 就绪握手)由内核承载,宿主零胶水。
8
+ */
9
+ import type { RuntimeStack } from "./runtime-stack.js";
10
+ import { editFile } from "./file-edit.js";
11
+ import type { RuntimeDriver } from "./driver.js";
12
+
13
+ export interface AgentToolDef {
14
+ type: "function";
15
+ function: {
16
+ name: string;
17
+ description: string;
18
+ parameters: Record<string, unknown>; // JSON Schema
19
+ };
20
+ }
21
+
22
+ export interface AgentToolHandlers {
23
+ run_command(args: { command: string; timeout_ms?: number; layer?: string }): Promise<{
24
+ stdout: string; stderr: string; exit_code: number; effective_user?: string;
25
+ }>;
26
+ read_file(args: { path: string; layer?: string }): Promise<{ path: string; content: string; size: number; mtime_ms: number }>;
27
+ edit_file(args: { path: string; old: string; new: string; all?: boolean; layer?: string }): Promise<{
28
+ path: string; occurrences: number; bytes_before: number; bytes_after: number;
29
+ }>;
30
+ observe_stream(args: { command: string; duration_ms?: number; stream?: "stdout" | "stderr" | "both"; name?: string }): Promise<{
31
+ session_id: string; lines: Array<{ stream: string; text: string }>;
32
+ }>;
33
+ spawn_and_observe(args: { command: string; ready_marker?: string; error_pattern?: string; name?: string }): Promise<{
34
+ session_id: string; log_base: string; note?: string;
35
+ }>;
36
+ stop_and_verify(args: { session_id: string }): Promise<{
37
+ status: "exited" | "stopped" | "unknown"; exit_code?: number | null; note?: string;
38
+ }>;
39
+ }
40
+
41
+ export interface AgentToolkit {
42
+ tools: AgentToolDef[];
43
+ handlers: AgentToolHandlers;
44
+ /** 工具面绑定的层(缺省 local;远端层经 stack.connect 后传入重绑) */
45
+ readonly layer: Promise<RuntimeDriver>;
46
+ }
47
+
48
+ export interface AgentToolkitOptions {
49
+ /** 层描述符(缺省 local);也可直接传已连接的 driver */
50
+ descriptor?: { driver: string; target?: string; config?: Record<string, unknown> };
51
+ driver?: RuntimeDriver;
52
+ /** 就绪握手与 watcher 轮询的超时上限 */
53
+ readyTimeoutMs?: number;
54
+ }
55
+
56
+ const TOOL_DEFS: AgentToolDef[] = [
57
+ {
58
+ type: "function",
59
+ function: {
60
+ name: "run_command",
61
+ description:
62
+ "执行一次性命令。退出码如实返回(非零不是异常——是数据,供条件分支);" +
63
+ "只有无法执行(超时/中断/连接死)才报错。stdout 与 stderr 分离返回。",
64
+ parameters: {
65
+ type: "object",
66
+ properties: {
67
+ command: { type: "string", description: "shell 命令" },
68
+ timeout_ms: { type: "number", description: "超时毫秒(缺省 120000)" },
69
+ },
70
+ required: ["command"],
71
+ },
72
+ },
73
+ },
74
+ {
75
+ type: "function",
76
+ function: {
77
+ name: "spawn_and_observe",
78
+ description:
79
+ "启动长跑任务(立即返回句柄不阻塞)。可选就绪标记(ready_marker——回溯热窗口," +
80
+ "快输出进程竞态安全)与错误模式(error_pattern——命中即自动收摊防遗忘泄漏)。" +
81
+ "后续用 stop_and_verify 停止并核实终态。",
82
+ parameters: {
83
+ type: "object",
84
+ properties: {
85
+ command: { type: "string", description: "长跑命令" },
86
+ ready_marker: { type: "string", description: "就绪标记(如 'listening on')" },
87
+ error_pattern: { type: "string", description: "错误模式(命中即收摊)" },
88
+ name: { type: "string", description: "会话名(人读前缀)" },
89
+ },
90
+ required: ["command"],
91
+ },
92
+ },
93
+ },
94
+ {
95
+ type: "function",
96
+ function: {
97
+ name: "stop_and_verify",
98
+ description:
99
+ "停止任务并核实终态。status 三态:exited=拿到退出码;stopped=终止已核实;" +
100
+ "unknown=核实不可达(非成功非失败——重试核实或显式上报,严禁当已清理)。",
101
+ parameters: {
102
+ type: "object",
103
+ properties: {
104
+ session_id: { type: "string", description: "spawn_and_observe 返回的 session_id" },
105
+ },
106
+ required: ["session_id"],
107
+ },
108
+ },
109
+ },
110
+ {
111
+ type: "function",
112
+ function: {
113
+ name: "read_file",
114
+ description: "读取远端层文件(UTF-8 文本)。返回内容与 (size, mtime_ms)。",
115
+ parameters: {
116
+ type: "object",
117
+ properties: {
118
+ path: { type: "string" },
119
+ },
120
+ required: ["path"],
121
+ },
122
+ },
123
+ },
124
+ {
125
+ type: "function",
126
+ function: {
127
+ name: "edit_file",
128
+ description:
129
+ "受控编辑远端文件:精确替换(唯一命中校验,all=true 全替换)+ TOCTOU 乐观锁(他人先写则拒绝)" +
130
+ "+ base64 保真写回(零 Shell 转义——绝不 sed -i)。未命中/多处命中/冲突均报错不盲写。",
131
+ parameters: {
132
+ type: "object",
133
+ properties: {
134
+ path: { type: "string" },
135
+ old: { type: "string", description: "待替换片段(含足够上下文唯一)" },
136
+ new: { type: "string" },
137
+ all: { type: "boolean", description: "多处命中全替换(缺省报错)" },
138
+ },
139
+ required: ["path", "old", "new"],
140
+ },
141
+ },
142
+ },
143
+ {
144
+ type: "function",
145
+ function: {
146
+ name: "observe_stream",
147
+ description:
148
+ "长跑观察(限时快照):启动长跑会话并观察一段时间(缺省 8s)按流过滤(stderr/stdout/both)," +
149
+ "返回行对象(带 stream 标记)。跨流序为到达序。",
150
+ parameters: {
151
+ type: "object",
152
+ properties: {
153
+ command: { type: "string" },
154
+ duration_ms: { type: "number", description: "观察时长(缺省 8000)" },
155
+ stream: { type: "string", enum: ["stdout", "stderr", "both"], description: "流过滤(缺省 both)" },
156
+ name: { type: "string" },
157
+ },
158
+ required: ["command"],
159
+ },
160
+ },
161
+ },
162
+ ];
163
+
164
+ /** 生成 Agent 工具集(定义层 + 执行层;宿主桥接到任意 LLM 框架) */
165
+ export function createAgentToolkit(stack: RuntimeStack, opts: AgentToolkitOptions = {}): AgentToolkit {
166
+ const layer: Promise<RuntimeDriver> = opts.driver
167
+ ? Promise.resolve(opts.driver)
168
+ : stack.connect(opts.descriptor ?? { driver: "local" });
169
+ const readyTimeoutMs = opts.readyTimeoutMs ?? 15_000;
170
+
171
+ return {
172
+ tools: TOOL_DEFS,
173
+ layer,
174
+ handlers: {
175
+ async run_command({ command, timeout_ms }) {
176
+ const drv = await layer;
177
+ const r = await drv.exec(command, { timeoutMs: timeout_ms });
178
+ return {
179
+ stdout: r.stdout, stderr: r.stderr, exit_code: r.exitCode,
180
+ effective_user: r.effectiveUser,
181
+ };
182
+ },
183
+ async spawn_and_observe({ command, ready_marker, error_pattern, name }) {
184
+ const drv = await layer;
185
+ const h = await drv.execSession(command, name ? { name } : undefined);
186
+ if (error_pattern) {
187
+ drv.watchSession(h.sessionId, { pattern: error_pattern, once: false, thenStop: true });
188
+ }
189
+ let note: string | undefined;
190
+ if (ready_marker) {
191
+ // 就绪握手(回溯热窗口——快输出进程竞态安全)
192
+ const t0 = Date.now();
193
+ for (;;) {
194
+ const lines = await drv.tailSession(h.sessionId, 50);
195
+ if (lines.some((l) => l.includes(ready_marker))) { note = "ready"; break; }
196
+ if (Date.now() - t0 > readyTimeoutMs) {
197
+ await drv.stopSession(h.sessionId).catch(() => undefined);
198
+ throw new Error(`ready 超时(${readyTimeoutMs}ms 未见 '${ready_marker}'——已自动停止)`);
199
+ }
200
+ await new Promise((r) => setTimeout(r, 100));
201
+ }
202
+ }
203
+ return { session_id: h.sessionId, log_base: h.logBase, note };
204
+ },
205
+ async read_file({ path }) {
206
+ const drv = await layer;
207
+ if (!drv.fs.stat) throw new Error("该层不支持 read_file 的元数据(fs.stat 缺席)");
208
+ const st = await drv.fs.stat(path);
209
+ if (!st) throw new Error(`文件不存在: ${path}`);
210
+ const content = await drv.fs.readFile(path);
211
+ return { path, content, size: st.size, mtime_ms: st.mtimeMs };
212
+ },
213
+ async edit_file({ path, old, new: newStr, all }) {
214
+ const drv = await layer;
215
+ const r = await editFile(drv, path, old, newStr, { all });
216
+ return { path: r.path, occurrences: r.occurrences, bytes_before: r.bytesBefore, bytes_after: r.bytesAfter };
217
+ },
218
+ async observe_stream({ command, duration_ms, stream, name }) {
219
+ const drv = await layer;
220
+ const h = await drv.execSession(command, name ? { name } : undefined);
221
+ const dur = duration_ms ?? 8000;
222
+ await new Promise((r) => setTimeout(r, dur));
223
+ // 双流:driver 带 sessions.tailLines(local/ssh 均有)用行对象;否则 stdout 投影兜底
224
+ const anyDrv = drv as unknown as {
225
+ sessions?: { tailLines?: (id: string, n?: number) => Array<{ stream: string; text: string }> };
226
+ };
227
+ const lines = anyDrv.sessions?.tailLines
228
+ ? anyDrv.sessions.tailLines(h.sessionId, 500)
229
+ : (await drv.tailSession(h.sessionId, 500)).map((t) => ({ stream: "stdout", text: t }));
230
+ const filtered = (stream && stream !== "both") ? lines.filter((l) => l.stream === stream) : lines;
231
+ await drv.stopSession(h.sessionId).catch(() => undefined);
232
+ return { session_id: h.sessionId, lines: filtered };
233
+ },
234
+ async stop_and_verify({ session_id }) {
235
+ const drv = await layer;
236
+ const r = (await drv.stopSession(session_id)) as { status?: string; exitCode?: number | null };
237
+ if (r.status === "exited") return { status: "exited", exit_code: r.exitCode };
238
+ if (r.status === "stopped") return { status: "stopped", note: "终止已核实(SIGKILL 升级在场)" };
239
+ if (r.status === "unknown") return { status: "unknown", note: "核实不可达——重连后核实或显式上报" };
240
+ // 无 status 的旧形态 driver:exitCode 可判 exited
241
+ return r.exitCode != null
242
+ ? { status: "exited", exit_code: r.exitCode }
243
+ : { status: "stopped", note: "已发终止(旧形态 driver——建议升级内核取三态)" };
244
+ },
245
+ },
246
+ };
247
+ }
package/src/dialect.ts ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * dialect.ts — 远端命令方言适配层(FEAT-14:GNU · BSD · busybox 三族原语探测与渲染)
3
+ *
4
+ * 动机(生产实锤):`base64 -w 0` 为纯 GNU 方言——FreeBSD/OPNsense 的 base64
5
+ * 不认 `-w`(BSD 用 `-b`,解码历史差异 `-d`/`-D`);sha256 工具 Linux=sha256sum /
6
+ * FreeBSD=/sbin/sha256 -q / 精简容器=busybox 或 openssl dgst。目标环境
7
+ * (OPNsense/Alpine)不探测必错。探测结果**按连接缓存**(探测键由调用方持有)。
8
+ */
9
+ import type { ExecResult } from "./driver.js";
10
+
11
+ export type DialectFamily = "gnu" | "bsd" | "busybox" | "openssl-fallback" | "unknown";
12
+
13
+ /** 一套已探测的远端原语(渲染为 argv 前缀,拼接数据参数后执行) */
14
+ export interface RemoteDialect {
15
+ family: DialectFamily;
16
+ /** 编码(无换行输出):["base64","-w","0"](GNU)/ ["base64"](BSD·默认不折行) */
17
+ base64Encode: string[];
18
+ /** 解码:GNU ["base64","-d"] / BSD ["base64","-D"] */
19
+ base64Decode: string[];
20
+ /** sha256 摘要:["sha256sum"] / ["sha256","-q"] / busybox ["sha256sum"] / openssl ["openssl","dgst","-sha256"] */
21
+ sha256: string[];
22
+ }
23
+
24
+ export type ExecFn = (command: string) => Promise<Pick<ExecResult, "stdout" | "exitCode">>;
25
+
26
+ /** 探测失败时的兜底:全部原语标记不可用(显式,不静默用错方言) */
27
+ export const NULL_DIALECT: RemoteDialect = {
28
+ family: "unknown",
29
+ base64Encode: [],
30
+ base64Decode: [],
31
+ sha256: [],
32
+ };
33
+
34
+ async function tryCmd(exec: ExecFn, cmd: string): Promise<{ ok: boolean; stdout: string }> {
35
+ try {
36
+ const r = await exec(cmd);
37
+ return { ok: r.exitCode === 0, stdout: r.stdout };
38
+ } catch {
39
+ return { ok: false, stdout: "" };
40
+ }
41
+ }
42
+
43
+ /** 探测远端方言(每命令独立判定;缺原语=该族标记跳过,非报错) */
44
+ export async function probeDialect(exec: ExecFn): Promise<RemoteDialect> {
45
+ // ── base64 编码:GNU 认 -w 0,BSD 报错(认 -b)──
46
+ const gnuEnc = await tryCmd(exec, "printf filaxis | base64 -w 0");
47
+ if (gnuEnc.ok && gnuEnc.stdout.includes("ZmlsYXhpcw==")) {
48
+ // GNU base64 在场;解码参数再探(GNU -d)
49
+ const gnuDec = await tryCmd(exec, "printf ZmlsYXhpcw== | base64 -d");
50
+ if (gnuDec.ok && gnuDec.stdout.includes("filaxis")) {
51
+ const sha = await detectSha256(exec);
52
+ return { family: sha.family, base64Encode: ["base64", "-w", "0"], base64Decode: ["base64", "-d"], sha256: sha.argv };
53
+ }
54
+ }
55
+ // ── BSD:无 -w 默认不折行;解码 -D(老 BSD)或 -d(新)──
56
+ const bsdEnc = await tryCmd(exec, "printf filaxis | base64");
57
+ if (bsdEnc.ok && bsdEnc.stdout.replace(/\s+/g, "").includes("ZmlsYXhpcw==")) {
58
+ const decD = await tryCmd(exec, "printf ZmlsYXhpcw== | base64 -d");
59
+ const decFlag = decD.ok && decD.stdout.includes("filaxis") ? "-d" : "-D";
60
+ const sha = await detectSha256(exec);
61
+ return { family: sha.family, base64Encode: ["base64"], base64Decode: ["base64", decFlag], sha256: sha.argv };
62
+ }
63
+ // ── base64 完全缺席(极简容器):全 NULL(调用方显式降级别的原语路径)──
64
+ return { ...NULL_DIALECT, sha256: (await detectSha256(exec)).argv };
65
+ }
66
+
67
+ const HEX64 = /[0-9a-f]{64}/;
68
+
69
+ async function detectSha256(exec: ExecFn): Promise<{ family: DialectFamily; argv: string[] }> {
70
+ const coreutils = await tryCmd(exec, "printf filaxis | sha256sum");
71
+ if (coreutils.ok && HEX64.test(coreutils.stdout)) {
72
+ return { family: "gnu", argv: ["sha256sum"] };
73
+ }
74
+ const bsd = await tryCmd(exec, "printf filaxis | sha256 -q");
75
+ if (bsd.ok && HEX64.test(bsd.stdout.trim())) {
76
+ return { family: "bsd", argv: ["sha256", "-q"] };
77
+ }
78
+ const openssl = await tryCmd(exec, "printf filaxis | openssl dgst -sha256");
79
+ if (openssl.ok && HEX64.test(openssl.stdout)) {
80
+ return { family: "openssl-fallback", argv: ["openssl", "dgst", "-sha256"] };
81
+ }
82
+ return { family: "unknown", argv: [] };
83
+ }
84
+
85
+ /** 按连接缓存:探测键(调用方用连接身份)→ 方言 */
86
+ export class DialectCache {
87
+ private cache = new Map<string, RemoteDialect>();
88
+
89
+ async get(probeKey: string, exec: ExecFn, force = false): Promise<RemoteDialect> {
90
+ if (!force && this.cache.has(probeKey)) return this.cache.get(probeKey)!;
91
+ const d = await probeDialect(exec);
92
+ this.cache.set(probeKey, d);
93
+ return d;
94
+ }
95
+
96
+ invalidate(probeKey?: string): void {
97
+ if (probeKey) this.cache.delete(probeKey);
98
+ else this.cache.clear();
99
+ }
100
+ }
package/src/driver.ts ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * driver.ts — RuntimeDriver 统一契约(多 Transport 执行内核)
3
+ *
4
+ * 净室平移自 Aethelum-Nexus `src/core/tools/runtime/driver.ts`
5
+ * (源锚点:Aethelum-Nexus dev @ 8977ff3),按 REQ-0001 OSS 化改造:
6
+ * - ExecResult 新增 stderr(FEAT-03:exec 通道 stdout/stderr 物理分离;源版无此字段)
7
+ * - ExecOptions 新增 channel(FEAT-03:通道显式化 "exec" | "pty",缺省 exec)
8
+ * - 剔除 interactionManager/sessionId(凭据交互策略属 FEAT-12,M1 后续批次设计后进契约)
9
+ * - capabilities 保留为纯数据标签(语义解释权归消费方;源注释中的策略引擎字样改为通用表述)
10
+ *
11
+ * 已知 driver 快捷方式(可自动生成 launchCmd):
12
+ * "local" — 进程内原生执行,唯一特殊值
13
+ * "ssh" — ssh [-options] <target>
14
+ * "docker" — <runtime> exec -i <target> <shell> -i (runtime 默认 docker,可换 podman/nerdctl 等)
15
+ * "k8s" — <runtime> exec -i [-n ns] <target> -- <shell> -i (runtime 默认 kubectl,可换 oc/k3s 等)
16
+ * "teleport" — tsh ssh [--cluster=c] [login@]<target>
17
+ *
18
+ * 任意字符串也合法(e.g. "nix-shell", "conda", "distrobox"),
19
+ * 但必须在 config.launchCmd 中提供完整的启动命令。
20
+ */
21
+
22
+ export type RuntimeDriverType = "local" | "browser" | "ssh" | "docker" | "k8s" | (string & {});
23
+
24
+ export interface RuntimeDescriptor {
25
+ driver: RuntimeDriverType;
26
+ /** 连接目标(ssh: user@host;container: 容器名/ID;自定义: 任意标签) */
27
+ target?: string;
28
+ /** 驱动配置:sshOptions / shell / runtime / namespace / launchCmd 等 */
29
+ config?: Record<string, unknown>;
30
+ /** 持久会话 ID(tmux 后端管理的长连接标识,跨重启恢复用) */
31
+ terminalId?: string;
32
+ /**
33
+ * 持久化的工作目录。调用方执行 cd 后异步更新,跨重启恢复时用于重建上下文。
34
+ */
35
+ cwd?: string;
36
+ /**
37
+ * 该运行时环境的能力标签列表(纯数据——消费方可据此做环境感知策略,
38
+ * 内核不解释)。内置约定示例:
39
+ * "read-only" — 禁止写操作
40
+ * "production" — 生产环境,高危操作从严
41
+ * "sandbox" — 沙箱环境,可放宽检查
42
+ */
43
+ capabilities?: string[];
44
+ }
45
+
46
+ /**
47
+ * 执行通道(FEAT-03):
48
+ * - "exec"(缺省)——非 PTY 通道:stdout/stderr 物理分离;层上下文经
49
+ * cwd/env/enterSteps 管线重放拼装(独立 channel 复现层进入路径)
50
+ * - "pty"——层内 PTY marker 协议:天然继承层内交互状态(su 身份/cd/export),
51
+ * stderr 混流为终端语义(不承诺分离)
52
+ */
53
+ export type ExecChannel = "exec" | "pty";
54
+
55
+ export interface ExecOptions {
56
+ timeoutMs?: number;
57
+ env?: Record<string, string>;
58
+ /** 临时覆盖命令执行路径 */
59
+ cwd?: string;
60
+ /** 执行通道(缺省 "exec") */
61
+ channel?: ExecChannel;
62
+ /** AbortSignal 硬中断子进程:abort 时子进程立即 SIGTERM,Promise 以 AbortError reject */
63
+ signal?: AbortSignal;
64
+ }
65
+
66
+ export interface ExecResult {
67
+ stdout: string;
68
+ /** exec 通道物理分离的 stderr;pty 通道下并入 stdout(终端语义) */
69
+ stderr: string;
70
+ exitCode: number;
71
+ /** 执行完成时该通道命令上下文的有效用户(marker 尾缀 id -un 实测——层内 su/sudo 变更可追踪) */
72
+ effectiveUser?: string;
73
+ }
74
+
75
+ export interface FileEntry {
76
+ name: string;
77
+ isDirectory: boolean;
78
+ size?: number;
79
+ mtime?: number;
80
+ }
81
+
82
+ export interface FileStat {
83
+ mtimeMs: number;
84
+ size: number;
85
+ }
86
+
87
+ export interface FileSystemDriver {
88
+ readFile(path: string): Promise<string>;
89
+ writeFile(path: string, content: string): Promise<void>;
90
+ exists(path: string): Promise<boolean>;
91
+ readdir(path: string): Promise<FileEntry[]>;
92
+ rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
93
+ mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
94
+ /** 可选 stat(mtimeMs/size)——先读后写闸的数据依赖 */
95
+ stat?(path: string): Promise<FileStat | null>;
96
+ /**
97
+ * 可选 readBase64——二进制安全读取。readFile 返回 UTF-8 字符串会破坏
98
+ * 二进制字节;本方法返回裸 base64。local 层实现;远端层无此方法时
99
+ * 调用方应显式处理「该层不支持二进制读取」。
100
+ */
101
+ readBase64?(path: string): Promise<string>;
102
+ }
103
+
104
+ import type {
105
+ SessionStartOptions,
106
+ SessionHandleInfo,
107
+ WatchOptions,
108
+ ReadLogOptions,
109
+ } from "./session-types.js";
110
+
111
+ export interface RuntimeDriver {
112
+ // 基础信息
113
+ descriptor: RuntimeDescriptor;
114
+
115
+ // 核心环境能力
116
+ exec(command: string, options?: ExecOptions): Promise<ExecResult>;
117
+
118
+ // ── 长跑会话生命周期(spawn 即返 + 落盘日志 + 热窗口 + watcher)──
119
+ /** 启动长跑会话(立即返回) */
120
+ execSession(command: string, opts?: SessionStartOptions): Promise<SessionHandleInfo>;
121
+ /** 热窗口快照:最近 n 行 */
122
+ tailSession(sessionId: string, lines?: number): Promise<string[]>;
123
+ /** 读会话日志文件(全量/大范围) */
124
+ readSessionLog(sessionId: string, opts?: ReadLogOptions): Promise<string>;
125
+ /** 注册 watcher:命中 → 会话事件(内核事件接口,见 FEAT-09) */
126
+ watchSession(sessionId: string, opts: WatchOptions): { cancel(): void };
127
+ /** 停止会话(SIGTERM→SIGKILL 升级;发出后核实终态) */
128
+ stopSession(sessionId: string): Promise<{ exitCode?: number | null }>;
129
+ /** 会话列表 */
130
+ listSessions(): Array<{ sessionId: string; command: string; running: boolean; startedAt: number }>;
131
+
132
+ // 原生路径解析与环境获取
133
+ getCwd(): Promise<string>;
134
+ getEnv(key: string): Promise<string | undefined>;
135
+
136
+ // 强隔离文件系统
137
+ fs: FileSystemDriver;
138
+
139
+ // 初始化与销毁
140
+ init(): Promise<void>;
141
+ destroy(): Promise<void>;
142
+ }
package/src/events.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * events.ts — 内核事件总线(FEAT-09:EventEmitter + AsyncIterable 双形态,零队列依赖)
3
+ *
4
+ * 消费方异常与内核生命周期隔离(AC-10:on 回调异常不杀内核与流)。
5
+ * AsyncIterable 形态为有界队列:满时丢最旧并累计 dropped(慢消费者可查证——
6
+ * 不承诺无限缓冲,与热窗口「有限历史」同一哲学)。
7
+ */
8
+ import { EventEmitter } from "node:events";
9
+
10
+ const DEFAULT_ITER_QUEUE_MAX = 1024;
11
+
12
+ export interface EventBusOptions {
13
+ /** iterable 形态的每订阅队列上限(满丢最旧并计数) */
14
+ queueMax?: number;
15
+ }
16
+
17
+ type Handler<E> = (ev: E) => void;
18
+
19
+ export class EventBus<E extends { type: string }> {
20
+ private emitter = new EventEmitter();
21
+ private readonly queueMax: number;
22
+ /** 每个迭代订阅一份独立队列:{queue, dropped, active} */
23
+ private iterables = new Set<{ queue: E[]; dropped: number; active: boolean }>();
24
+
25
+ constructor(opts: EventBusOptions = {}) {
26
+ this.queueMax = opts.queueMax ?? DEFAULT_ITER_QUEUE_MAX;
27
+ this.emitter.setMaxListeners(0); // 多订阅是常态(每会话/每流),不告警
28
+ }
29
+
30
+ /** 订阅(可按 type 过滤);返回退订函数 */
31
+ on(type: E["type"] | "*", handler: Handler<E>): () => void {
32
+ const wrapped = (ev: E) => {
33
+ try {
34
+ handler(ev);
35
+ } catch {
36
+ /* 消费方异常不外溢(AC-10) */
37
+ }
38
+ };
39
+ this.emitter.on(type === "*" ? "__all" : (type as string), wrapped as (ev: E) => void);
40
+ return () => this.emitter.off(type === "*" ? "__all" : (type as string), wrapped as (ev: E) => void);
41
+ }
42
+
43
+ once(type: E["type"], handler: Handler<E>): void {
44
+ try {
45
+ this.emitter.once(type as string, handler);
46
+ } catch { /* 同上 */ }
47
+ }
48
+
49
+ off(type: E["type"] | "*", handler: Handler<E>): void {
50
+ this.emitter.off(type === "*" ? "__all" : (type as string), handler as (ev: E) => void);
51
+ }
52
+
53
+ /** 发布:同步分发到订阅者与迭代队列 */
54
+ emit(ev: E): void {
55
+ this.emitter.emit(ev.type as string, ev);
56
+ this.emitter.emit("__all", ev);
57
+ for (const it of this.iterables) {
58
+ if (!it.active) continue;
59
+ if (it.queue.length >= this.queueMax) {
60
+ it.queue.shift();
61
+ it.dropped++;
62
+ }
63
+ it.queue.push(ev);
64
+ }
65
+ }
66
+
67
+ /** AsyncIterable 形态句柄:可 for-await 消费(可按 type 过滤),dropped() 查溢出丢弃数 */
68
+ iterable(type?: E["type"]): AsyncIterable<E> & { dropped(): number } {
69
+ const entry = { queue: [] as E[], dropped: 0, active: true };
70
+ this.iterables.add(entry);
71
+ const handle = {
72
+ [Symbol.asyncIterator]: async function* (): AsyncGenerator<E> {
73
+ try {
74
+ while (entry.active) {
75
+ if (entry.queue.length === 0) {
76
+ await new Promise((r) => setTimeout(r, 10));
77
+ continue;
78
+ }
79
+ const ev = entry.queue.shift()!;
80
+ if (type && ev.type !== type) continue;
81
+ yield ev;
82
+ }
83
+ } finally {
84
+ entry.active = false;
85
+ }
86
+ },
87
+ dropped: () => entry.dropped,
88
+ } as AsyncIterable<E> & { dropped(): number };
89
+ (handle as unknown as { __entry: typeof entry }).__entry = entry;
90
+ return handle;
91
+ }
92
+
93
+ listenerCount(type?: E["type"]): number {
94
+ return this.emitter.listenerCount(type === "*" || !type ? "__all" : (type as string));
95
+ }
96
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * factory.ts — Transport 工厂与开放注册表(FEAT-01/FEAT-09:AC-09 第三方 driver 同权接入)
3
+ *
4
+ * 净室平移自 Aethelum-Nexus `src/core/tools/runtime/factory.ts`(源锚 8977ff3),
5
+ * OSS 化:注册表对外开放(registerDriver);内置 local,其余 Transport 分批落位。
6
+ */
7
+ import type { RuntimeDriver, RuntimeDescriptor } from "./driver.js";
8
+ import { LocalRuntimeDriver } from "./local-driver.js";
9
+ import { SshPtyDriver } from "./ssh-pty-driver.js";
10
+ import { TmuxSessionDriver } from "./tmux-driver.js";
11
+
12
+ export type DriverFactory = (descriptor: RuntimeDescriptor) => RuntimeDriver;
13
+
14
+ export class RuntimeFactory {
15
+ private custom = new Map<string, DriverFactory>();
16
+
17
+ /** 开放注册:第三方 Transport 与内置同权;重复注册拒绝(FEAT-01 异常策略) */
18
+ registerDriver(type: string, factory: DriverFactory): void {
19
+ if (this.custom.has(type) || ["local", "ssh", "tmux"].includes(type)) {
20
+ throw new Error(`driver '${type}' 已注册,拒绝覆盖——先 unregister 或换类型名`);
21
+ }
22
+ this.custom.set(type, factory);
23
+ }
24
+
25
+ unregisterDriver(type: string): boolean {
26
+ return this.custom.delete(type);
27
+ }
28
+
29
+ registeredTypes(): string[] {
30
+ return ["local", "ssh", "tmux", ...this.custom.keys()];
31
+ }
32
+
33
+ create(descriptor: RuntimeDescriptor, fallbackCwd?: string): RuntimeDriver {
34
+ if (descriptor.driver === "local") {
35
+ return LocalRuntimeDriver.fromDescriptor(descriptor, fallbackCwd);
36
+ }
37
+ if (descriptor.driver === "ssh") {
38
+ return new SshPtyDriver(descriptor);
39
+ }
40
+ if (descriptor.driver === "tmux") {
41
+ return new TmuxSessionDriver(descriptor);
42
+ }
43
+ const f = this.custom.get(descriptor.driver);
44
+ if (f) return f(descriptor);
45
+ // 未知 driver 且无注册工厂:结构化错误(docker/k8s/teleport 快捷方式经
46
+ // launchCmd 走 tmux 后端,或 registerDriver 自定义——FEAT-01/FEAT-09)
47
+ throw new Error(
48
+ `未知 driver '${descriptor.driver}'——经 registerDriver() 注册,或在 config.launchCmd 提供完整启动命令`,
49
+ );
50
+ }
51
+ }