roswire 0.1.0

JSON-first RouterOS CLI bridge for AI agents and automation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
# RosWire 开发计划

本文是 `roswire` 的实现规格与阶段计划。用户入口、安装方式和使用示例见 [`../README.md`](../README.md)。

## 1. 项目目标

`roswire` 是面向 AI Agent 与自动化脚本的 RouterOS CLI 桥接工具。它不追求人类交互体验,而是优先保证输出稳定、错误可解析、调用可组合。

### 目标

- 提供非交互、JSON 优先(JSON-first)、可脚本化的 RouterOS 操作入口。
- 支持 RouterOS 原生 API 的 v6/v7 方言,以及 RouterOS v7 REST API。
- 严格隔离 `stdout``stderr`,便于 Agent 运行时捕获结果与错误。
- 产出稳定错误结构,帮助 Agent 根据错误上下文自我修正。
- 编译为单个原生二进制文件,不依赖外部运行时。

### 非目标

- 不替代 WinBox、WebFig 或 RouterOS 交互式终端。
- 不在 CLI 内保存长期会话或连接池。
- 不默认输出彩色文本、加载动画、进度条或分页器(pager)。
- 不在命令行交互式询问密码、确认操作或二次输入。

## 2. 核心设计原则

### 无状态执行

每次执行都是一次独立的 TCP/TLS/HTTP 生命周期。CLI 不假设上一次调用留下的本地状态。

### 确定性输出

在相同输入与相同 RouterOS 设备状态下,默认输出应尽量保持字节级稳定。

实现约束:

- 默认错误载荷不包含时间戳(timestamp)、随机 ID、耗时等运行时变化字段。
- JSON 对象中需要稳定顺序的字段使用结构体或 `BTreeMap`,避免直接序列化 `HashMap`- 调试信息必须显式打开,例如 `--debug`;默认不能污染 `stdout`
### 流隔离

- `stdout`:只输出成功结果载荷。
- `stderr`:只输出诊断、调试信息与错误载荷。
- 所有错误都以单个 JSON 对象写入 `stderr`,并以非零状态码退出。

### 零交互

如果缺少必要参数,立即输出结构化错误并退出。禁止出现 `Are you sure? [y/n]`、密码提示或任何等待用户输入的流程。

## 3. CLI 与配置约定

### 命令格式

```text
roswire [global-options] <path...> <action> [key=value ...]
```

示例:

```bash
roswire ip address print --json
roswire ip address add address=192.168.88.2/24 interface=bridge --json
roswire ip address remove .id=*1 --json
```

### 全局选项

设备、连接和传输字段不再从 `ROS_*` 环境变量读取;统一优先级为命令行参数 > 本地配置 profile > 默认值。`ROSWIRE_HOME`、`ROSWIRE_DEBUG`、`ROSWIRE_MASTER_KEY` 以及 profile secret `type=env` 指向的自定义变量仍是进程级/secret 后端变量。

| 选项 | profile / secret | 说明 |
| --- | --- | --- |
| `--profile <name>` | `default_profile` / 单 profile | 选择 `config.toml` 中的 profile |
| `--host <host>` | `host` | RouterOS IP 地址或 DNS 主机名;不支持 MAC 地址直连 |
| `--user <user>` | `user` | RouterOS 用户名 |
| `--password <password>` | secret `password` | RouterOS 密码;不推荐在 shell history 中直接传入 |
| `--protocol <mode>` | `protocol` | `auto``api``api-ssl``rest` |
| `--routeros-version <mode>` | `routeros_version` | `auto``v6``v7`;用于选择原生 API 方言 |
| `--transfer <mode>` | `transfer` | 文件传输后端;当前唯一支持值为 `ssh` |
| `--ssh-port <port>` | `ssh_port` | RouterOS SSH 服务端口,默认 `22` |
| `--ssh-user <user>` | `ssh_user` | SSH 文件传输用户名;默认复用 API user |
| `--ssh-password <password>` | secret `ssh_password` / `password` | SSH 文件传输密码;默认复用 API password |
| `--ssh-key <path>` | `ssh_key` | SSH 私钥路径;设置后优先使用 key auth |
| - | secret `ssh_key_passphrase` | 加密 SSH 私钥 passphrase;通过 profile secret 非交互提供 |
| `--ssh-host-key <fingerprint>` | `ssh_host_key` | RouterOS SSH host key 指纹;用于非交互校验服务器身份 |
| `--allow-from <cidr>` | `allow_from` | 允许访问 SSH 服务的客户端来源 CIDR,用于 `/ip service ssh address` |
| `--ensure-ssh` | - | 允许 `roswire` 通过 API/REST 启用 SSH 服务并设置白名单 |
| `--restore-ssh` | - | 文件传输结束后恢复进入任务前的 SSH 服务配置 |
| `--port <port>` | `port` | 覆盖显式协议的默认端口;`auto` 模式下不接受单一端口覆盖 |
| `--json` | - | 输出机器可解析 JSON |
| `--debug` | `ROSWIRE_DEBUG` |`stderr` 输出调试信息,必须脱敏 |

### 本地客户端目录

`roswire` 在用户目录下维护一个本地工作目录,用于配置、日志和少量运行状态:

```text
~/.roswire/
├── config.toml        # 默认配置文件,保存 profile、非敏感选项和 secret 引用
├── logs/              # 本地 JSONL 日志,最多保留 30 天
├── state/             # 运行状态;不得保存长期会话或 RouterOS 凭据明文
└── cache/             # 可删除缓存,例如远端 schema、能力探测和短期调试材料
```

实现约束:

- 首次运行可通过 `roswire config init` 创建目录和默认 `config.toml`- 目录权限必须尽量收敛:Unix/macOS 目标为 `0700``config.toml` 目标为 `0600`- 如果配置文件权限过宽,且包含任何 secret 或 secret 引用,返回 `CONFIG_INSECURE_PERMISSIONS`- 不在 `~/.roswire/` 中保存 RouterOS 长期会话、API token、明文日志或未脱敏错误上下文。
- `ROSWIRE_HOME` 可覆盖默认目录,仅用于测试、CI 或便携环境;错误上下文中只记录脱敏后的路径尾段。

### `config.toml` 结构

配置文件支持多个 profile。默认 profile 用于未显式传入 `--profile` 的调用。

```toml
version = 1
default_profile = "home"

[profiles.home]
host = "192.168.88.1"
user = "admin"
protocol = "auto"
routeros_version = "auto"
transfer = "ssh"
ssh_port = 22
ssh_host_key = "SHA256:replace-with-routeros-host-key"
allow_from = "203.0.113.10/32"

[profiles.home.secrets.password]
type = "keychain"
service = "roswire"
account = "profiles/home/password"

[profiles.home.secrets.ssh_password]
type = "same-as"
target = "password"

[logging]
enabled = true
retention_days = 30
level = "info"
```

配置解析规则:

- `--profile <name>` 选择 profile;未设置时使用 `default_profile`- 命令行参数覆盖环境变量;环境变量覆盖 profile;profile 覆盖默认值。
- `config.toml` 只保存非敏感字段和 secret 引用,不默认保存明文密码。
- 所有 profile 名称、secret account、日志路径都必须做最小化校验,避免路径穿越和日志污染。

### 密码与密钥保存策略

支持三种 secret 保存方式,但安全等级不同。

| 类型 | 配置写法 | 安全等级 | 结论 |
| --- | --- | --- | --- |
| 明文 | `type = "plain"` + `value = "..."` || 仅允许实验室、一次性测试或 CI 临时目录;必须配合 `0600` 权限和显式 `allow_plain_secrets = true` |
| 本地加密 | `type = "encrypted"` + `value = "v1:..."` || 可以降低误读配置文件的风险,但不能替代钥匙链;禁止使用编译进程序的固定密钥 |
| 本机钥匙链 | `type = "keychain"` + `service/account` || 默认推荐方案;macOS Keychain、Linux Secret Service、Windows Credential Manager |

`type = "same-as"` 只是引用另一个 secret,不是第四种保存方式;解析时必须检测循环引用。

明文示例:

```toml
[profiles.lab]
host = "192.168.88.1"
user = "admin"
allow_plain_secrets = true

[profiles.lab.secrets.password]
type = "plain"
value = "lab-only-password"
```

本地加密示例:

```toml
[profiles.edge.secrets.password]
type = "encrypted"
key_id = "default"
value = "v1:base64-nonce-and-ciphertext"
```

本机钥匙链示例:

```toml
[profiles.prod.secrets.password]
type = "keychain"
service = "roswire"
account = "profiles/prod/password"
```

评估结论:

- **默认使用本机钥匙链**。配置文件只保存 `service/account` 引用,实际密码交给操作系统凭据库保存。
- **明文密码保留为显式低安全选项**。它方便实验和离线调试,但如果文件权限不安全必须拒绝运行。
- **本地加密不能使用硬编码程序密钥**。固定密钥可以从二进制中提取,本质是可逆混淆;如果实现 `encrypted`,必须使用每机生成的 master key,并优先把 master key 存入本机钥匙链。
- 如果本机钥匙链不可用,`encrypted` 模式可以后续支持 `ROSWIRE_MASTER_KEY` 这类非交互 key source;MVP 阶段不交互式询问 passphrase。
- 所有 secret 进入内存后使用专门 secret 类型承载,日志、错误、debug、dry-run、shell 建议命令都必须脱敏。
- 写入钥匙链的命令必须支持 `--stdin``--env <VAR>` 输入 secret,避免出现在 shell history 中。

推荐命令形态:

```bash
roswire config init
roswire secret set home password --stdin
roswire secret set home ssh-password --same-as password
roswire secret set lab password --plain --stdin --allow-plain
```

### 日志与保留策略

`~/.roswire/logs/` 用于本地排障日志,不能影响 `stdout` / `stderr` 契约。

- 日志格式使用 JSON Lines(JSONL),每行一个事件。
- 日志文件按日期滚动,例如 `roswire-2026-05-16.log`- 默认保留 30 天;MVP 阶段 30 天是上限,可以配置更短或关闭日志,不能配置更长。
- 每次启动时执行轻量清理:删除超过保留期的日志文件。清理失败只写入脱敏 warning,不阻塞主命令。
- 日志中禁止记录密码、token、SSH 私钥路径全文、本地绝对路径、RouterOS backup 文件内容或完整命令行中的敏感值。
- `--debug` 只提高本次运行的日志详细度;仍必须遵守脱敏规则。

### Agent 自描述与详细帮助

`roswire` 的主要消费者是 Agent,因此必须提供稳定、机器可读、足够详细的自描述接口。Agent 不应解析人类帮助文本来理解命令。

设计目标:

- 所有帮助、配置检查、schema 输出都支持 `--json`- 自描述命令默认不访问 RouterOS,不产生设备侧副作用。
- 所有输出包含 `schema_version`,便于 Agent 做兼容判断。
- 所有配置输出必须脱敏,永不输出真实密码、token、私钥内容或本地绝对敏感路径。
- 人类 `--help` 可以简洁,但 JSON help 必须详细到足以让 Agent 生成安全命令。
- `roswire help --json` 是自定义机器可读帮助子命令,不是对 clap 人类 help 文本的二次解析;`roswire --help` 保留给人类阅读。

核心命令:

| 命令 | 作用 | 是否访问 RouterOS |
| --- | --- | --- |
| `roswire help --json` | 输出完整命令目录、全局选项和帮助 schema ||
| `roswire help <command...> --json` | 输出单个命令的详细参数结构、示例、输出和错误 ||
| `roswire commands --json` | 输出可用命令索引,适合 Agent 快速检索 ||
| `roswire commands --remote --json` | 输出本地命令目录,并叠加目标设备能力状态 ||
| `roswire schema command <command...> --json` | 输出命令参数 JSON Schema ||
| `roswire schema command <command...> --remote --json` | 输出命令参数 JSON Schema,并叠加远端字段、枚举值和支持状态 ||
| `roswire schema output <command...> --json` | 输出成功输出 JSON Schema ||
| `roswire schema discover [path...] --remote --json` | 显式连接 RouterOS,生成当前设备的能力与 schema 覆盖快照 ||
| `roswire config inspect --json` | 输出当前 profile 的解析后配置、来源和脱敏 secret 状态 ||
| `roswire config profiles --json` | 输出本地 profile 列表和默认 profile ||
| `roswire doctor --json` | 检查本地目录、配置权限、secret 后端和依赖状态 | 默认否;传 `--include-remote` 才访问 RouterOS |
| `roswire explain-error <code> --json` | 输出错误码含义、常见原因和建议下一步 ||

`config inspect` 输出必须包含字段来源,便于 Agent 判断应修改 CLI 参数、环境变量还是 profile:

```json
{
    "schema_version": "roswire.config.inspect.v1",
    "active_profile": "home",
    "paths": {
        "home": "~/.roswire",
        "config": "~/.roswire/config.toml",
        "logs": "~/.roswire/logs"
    },
    "resolved": {
        "host": { "value": "192.168.88.1", "source": "profile" },
        "user": { "value": "admin", "source": "profile" },
        "protocol": { "value": "auto", "source": "default" },
        "transfer": { "value": "ssh", "source": "profile" },
        "password": { "status": "available", "type": "keychain", "source": "profile", "redacted": true },
        "ssh_host_key": { "value": "SHA256:...", "source": "profile" }
    },
    "logging": {
        "enabled": true,
        "retention_days": 30,
        "level": "info"
    },
    "warnings": []
}
```

命令帮助 JSON 必须包含以下结构:

```json
{
    "schema_version": "roswire.command.help.v1",
    "name": "ip address add",
    "summary": "添加 RouterOS IP address 记录。",
    "stability": "planned",
    "kind": "routeros-command",
    "syntax": "roswire ip address add address=<cidr> interface=<name> [disabled=<bool>] --json",
    "routeros_path": "/ip/address/add",
    "protocol_support": {
        "classic_api_v6": true,
        "classic_api_v7": true,
        "rest_v7": true,
        "rest_method": "PUT",
        "rest_path": "/rest/ip/address"
    },
    "side_effects": ["creates-routeros-record"],
    "idempotency": "not-idempotent",
    "requires_confirmation": false,
    "arguments": [
        {
            "name": "address",
            "style": "key-value",
            "required": true,
            "type": "cidr",
            "example": "192.168.88.2/24",
            "description": "要添加到接口上的 IP 地址。"
        },
        {
            "name": "interface",
            "style": "key-value",
            "required": true,
            "type": "string",
            "example": "bridge",
            "discovery_hint": "可先运行 roswire interface print --json 获取可用接口。"
        }
    ],
    "examples": [
        {
            "title": "给 bridge 添加地址",
            "command": "roswire ip address add address=192.168.88.2/24 interface=bridge --json",
            "expected_stdout_shape": "object",
            "notes": ["失败时检查 interface 是否存在。"]
        }
    ],
    "success_output": {
        "content_type": "application/json",
        "schema_ref": "roswire.output.routeros-record.v1"
    },
    "errors": ["USAGE_ERROR", "AUTH_FAILED", "NETWORK_ERROR", "ROS_API_FAILURE"],
    "self_correction": [
        {
            "when_error_contains": "no such interface",
            "next_command": "roswire interface print --json"
        }
    ]
}
```

命令目录规范:

- 每个命令必须登记 `kind``routeros-command``workflow``config``secret``introspection`- 每个会修改设备状态的命令必须声明 `side_effects``idempotency`- 每个参数必须声明 `style``positional``key-value``flag``option`- 每个参数必须声明 `required``type``description`,并尽量提供 `example`- 文件工作流必须声明本地路径、远端路径、临时文件、清理策略和 SSH 前置条件。
- 帮助示例中的命令不得包含真实密码、token、私钥路径或公网真实敏感地址。
- `doctor --include-remote --json` 才允许访问 RouterOS;普通 `help/schema/config inspect` 必须纯本地。

### 动态能力与 schema 发现

RouterOS 的菜单、字段和命令会随版本、package、硬件能力和授权状态变化。把所有 schema 都硬编码进 `roswire` 会很快过期;但反过来,完全依赖设备动态返回也不可靠,因为 RouterOS 官方公开文档中没有承诺提供完整、稳定、可直接生成 OpenAPI/JSON Schema 的元数据端点。REST API 本质上是 console API 的 JSON wrapper,支持通过 `POST` 调用任意 console command,但不等于提供完整 schema 注册表。

设计结论:采用“静态目录 + 远端覆盖 + 缓存”的混合模型。

| 层级 | 来源 | 用途 | 限制 |
| --- | --- | --- | --- |
| 静态目录 | `roswire` 随版本内置的命令目录、参数说明、示例、错误码和安全策略 | Agent 离线理解命令、生成基础调用、做稳定 snapshot 测试 | 需要随 `roswire` 发布更新,可能落后于最新 RouterOS |
| 远端覆盖 | 显式连接设备后,通过只读探测、版本信息、package 信息、协议能力和可观测字段生成 | 判断当前设备是否支持某命令、字段、协议和枚举候选值 | 只能作为 capability overlay,不应覆盖安全策略和副作用声明 |
| 运行时值发现 | 通过 `interface print``routing table print``system resource print` 等只读命令获取 | 为 Agent 提供接口名、表名、bridge 名称等候选值 | 这是当前设备数据,不是通用 schema;必须标记来源和时间 |
| 本地缓存 | `~/.roswire/cache/schema/` 下的可删除 JSON 快照 | 减少重复探测成本,便于 Agent 快速启动 | 缓存可失效、可删除、不可含 secret;不能作为唯一真相 |

动态发现命令必须显式要求访问远端:

```bash
roswire schema discover --remote --json
roswire schema discover ip address --remote --json
roswire schema command ip address add --remote --json
roswire commands --remote --json
```

默认的 `help --json`、`commands --json`、`schema command ... --json` 仍然只读取本地静态目录,不访问 RouterOS。这样可以保持自描述命令无副作用、低延迟、可离线,也避免 Agent 在“只是想查帮助”时误触发网络访问。

远端发现流程:

1. 按正常协议路由规则连接设备,解析 RouterOS 主版本、完整版本、build time、architecture、board-name 和 package 列表摘要。
1. 记录当前命中的协议:`rest``api-ssl``api`,以及原生 API 方言:`v6``v7`1. 对目标路径执行只读 capability probe,例如 `print``print count-only`、有限 `.proplist`、REST `GET` 或 REST `POST <path>/print`1. 从成功响应中提取可观测输出字段;从失败响应中只提取脱敏错误分类,不能把失败消息当成可靠 schema。
1. 对枚举值只做“候选值”标注,例如接口名、routing table、bridge、address-list 名称;不能把当前候选值误判为完整枚举。
1. 生成 `remote_overlay`,再与静态目录合并输出。
1. 如果启用缓存,把结果写入 `~/.roswire/cache/schema/<cache-key>.json`
远端发现必须遵守安全边界:

- 只允许执行只读命令;禁止为了发现参数而执行 `add``set``remove``enable``disable``reset``run``import``backup save` 等有副作用命令。
- 禁止通过“故意发错参数再解析错误消息”的方式构造必填字段 schema。错误文本不稳定,而且容易制造设备日志噪音。
- `remote_overlay` 不能改变静态目录中的 `side_effects``idempotency`、secret 标记、文件传输安全规则和危险命令策略。
- 如果设备账号权限不足,返回降级后的 schema,并在 `warnings` 中标记 `CAPABILITY_PROBE_FAILED`;不能要求 Agent 提权。
- 动态发现默认不修改 SSH、API、REST 或防火墙服务配置;如果发现命令需要 SSH 文件传输,只能报告前置条件缺失。

缓存键建议包含:

```text
profile-name + host-id + routeros-version + build-time + architecture + board-name + packages-hash + selected-protocol
```

其中 `host-id` 必须是脱敏哈希,不能直接写入真实主机名、公网 IP 或用户名。缓存失效规则:

- RouterOS 版本、build time、package 摘要、协议能力任一变化时失效。
- 默认 TTL 不超过 7 天;用户可以配置更短,也可以关闭动态 schema 缓存。
- `roswire schema discover --remote --refresh --json` 强制绕过缓存重新探测。
- 普通命令执行不能因为缓存缺失而自动做全量 schema 发现;最多复用首次连接阶段已经拿到的版本与协议能力。

远端 schema 输出示例:

```json
{
    "schema_version": "roswire.remote.schema.v1",
    "schema_source": ["static_catalog", "remote_overlay", "runtime_values"],
    "profile": "home",
    "device": {
        "routeros_version": "7.15.3",
        "major": "v7",
        "architecture": "arm64",
        "board_name": "RB5009UG+S+",
        "selected_protocol": "rest"
    },
    "cache": {
        "status": "miss",
        "ttl_seconds": 604800,
        "cache_key": "sha256:..."
    },
    "commands": [
        {
            "name": "ip address add",
            "support": "supported",
            "schema_source": ["static_catalog", "remote_overlay"],
            "output_fields_observed": [".id", "address", "interface", "network", "disabled"],
            "runtime_value_hints": {
                "interface": ["bridge", "ether1"]
            },
            "warnings": []
        }
    ],
    "warnings": []
}
```

Agent 使用建议:

- 生成命令前先读本地 `commands --json``help <command> --json`- 需要匹配真实设备版本、接口名、REST/API 支持状态时,再调用 `schema ... --remote --json`- 如果 `remote_overlay` 与静态目录冲突,安全策略以静态目录为准;字段存在性和协议支持以远端覆盖为准。
- 如果远端探测失败,Agent 应根据 `warnings` 降级使用静态目录,而不是猜测未知字段。

### 默认端口

| 协议 | 默认端口 | 说明 |
| --- | ---: | --- |
| `api` | `8728` | RouterOS 原生 API |
| `api-ssl` | `8729` | 基于 TLS 的 RouterOS 原生 API |
| `rest` | `443` | RouterOS v7 REST,实际端口取决于设备上的 `www-ssl` 服务配置 |

### 协议自动选择与调用优先级

`protocol=auto` 是默认模式。它用于解决 RouterOS v7 同时支持原生 API 与 REST API 时的路由选择问题。

基本原则:显式配置优先,自动探测其次。

| 场景 | 选择策略 |
| --- | --- |
| 用户显式指定 `--protocol rest` | 只走 REST;如果设备不是 v7 或 REST 不可用,返回结构化错误 |
| 用户显式指定 `--protocol api``api-ssl` | 只走原生 API,并按 `--routeros-version` 选择或探测 v6/v7 方言 |
| `auto` + RouterOS v6 | 走原生 API v6 方言 |
| `auto` + RouterOS v7 + REST 可用 + 当前动作有 REST 映射 | 优先走 REST |
| `auto` + RouterOS v7 + REST 不可用 | 回落到原生 API v7 方言 |
| `auto` + RouterOS v7 + 当前动作没有 REST 映射 | 回落到原生 API v7 方言 |

自动探测候选顺序:

1. `rest`:尝试只读请求 `/rest/system/resource`,解析版本与 REST 可用性。
1. `api-ssl`:登录后执行 `/system/resource/print`,解析版本。
1. `api`:登录后执行 `/system/resource/print`,解析版本。

错误处理规则:

- 网络不可达、端口未开放、服务未启用:记录为候选失败,继续尝试下一个协议。
- 认证失败:立即返回 `AUTH_FAILED`,不再静默尝试其它协议,避免掩盖凭据问题。
- REST 返回 404/405 或缺少目标菜单映射:视为该动作不适合 REST,允许回落到 v7 原生 API。
- `auto` 模式下如果用户同时指定 `--port`,返回 `CONFIG_ERROR`;避免一个端口被误用于多个候选协议。

首次探测结果只在当前进程内复用,不写入磁盘缓存。CLI 仍保持无状态;下一次调用会重新探测,除非用户显式指定协议和版本。

### 原生 API 方言选择

RouterOS v6 与 v7 的原生 API 共享同一套 TCP/TLS sentence 协议,但不能在实现中完全混为一个后端。差异主要体现在登录流程、菜单字段、命令可用性、`!trap` 错误内容和返回字段归一化上。

设计结论:采用“共享传输层 + 分离方言层”。

- 共享部分:TCP/TLS 连接、word/sentence 编解码、`!re`/`!done`/`!trap` 解析、超时处理、脱敏日志、统一错误输出。
- v6 方言层:兼容旧式 challenge-response 登录与 v6.43+ 的现代登录流程,维护 v6 字段归一化和命令差异。
- v7 方言层:默认使用现代登录流程,维护 v7 字段归一化、命令差异,并与 REST 映射保持语义一致。
- `auto` 模式:默认先尝试现代登录;如遇到旧式登录特征再回退到 v6 challenge-response。登录成功后通过 `/system/resource/print` 的版本信息确认方言。
- 显式模式:`--routeros-version v6``--routeros-version v7` 跳过模糊探测,适合测试、CI 和受控生产环境。

允许 v6/v7 方言模块在映射表、归一化逻辑、错误 hint 上保留适度冗余。禁止复制底层 sentence codec、TCP/TLS I/O 和统一错误输出,否则后续 bug 修复会变成双份维护。

## 4. 数据模型与协议映射

Agent 通常使用 CLI 风格的层级命令。`roswire` 需要将空格分隔路径转换为 RouterOS API 路径,并将 `key=value` 参数转换为协议载荷。

### 路径映射

| CLI | 原生 API(v6/v7 方言) | REST API |
| --- | --- | --- |
| `roswire ip address print` | `/ip/address/print` | `GET /rest/ip/address` |
| `roswire ip address add address=1.1.1.1 interface=ether1` | `/ip/address/add` + `=address=1.1.1.1` + `=interface=ether1` | `PUT /rest/ip/address` + JSON 请求体 |
| `roswire ip address set .id=*1 disabled=true` | `/ip/address/set` + `=.id=*1` + `=disabled=true` | `PATCH /rest/ip/address/*1` + JSON 请求体 |
| `roswire ip address remove .id=*1` | `/ip/address/remove` + `=.id=*1` | `DELETE /rest/ip/address/*1` |

> REST 动词映射必须在实现阶段用 RouterOS v7 文档与真实设备测试验证;不要仅根据常见 REST 习惯推断。

### 参数规则

- `key=value` 原样保留 RouterOS 字段名,例如 `.id=*1``comment=wan uplink`- 布尔值、数字和字符串在 CLI 层保持字符串;协议层按目标 API 要求转换。
- 任何密码、令牌(token)、证书内容都不能出现在错误上下文或调试日志中。
- 未知动作(action)不应静默猜测;返回 `UNSUPPORTED_ACTION`,或在后续版本中设计显式原始命令(raw command)透传模式。

### 客户端文件上传下载

这里讨论的是本机运行 `roswire` 时,如何把本机文件上传到 RouterOS,或把 RouterOS 上的文件下载到本机。它不同于 `/file print`、`/file remove` 这类设备内文件菜单操作。

设计结论:`roswire` 应该提供一等公民的文件工作流,但内部必须分成“API/REST 编排”和“SSH 字节传输”两层。

- API/REST 负责控制面:生成备份、导出配置、执行导入、查询文件是否存在、删除临时文件。
- SSH transfer 后端负责数据面:真正把文件字节从本机传到路由器,或从路由器传回本机。
- 不能把 API sentence 或 REST JSON 当成通用大文件传输通道。

目标命令:

| 命令 | 工作流 |
| --- | --- |
| `roswire file upload <local> <remote>` | 通过 SSH transfer 后端上传本机文件到 RouterOS 文件系统 |
| `roswire file download <remote> <local>` | 通过 SSH transfer 后端把 RouterOS 文件下载到本机 |
| `roswire import <local.rsc>` | 上传 `.rsc` 到临时路径,执行 `/import file-name=...`,可选清理远端临时文件 |
| `roswire export download <local.rsc>` | 执行 `/export file=...`,等待文件出现,下载到本机,可选清理远端文件 |
| `roswire backup download <local.backup>` | 执行 `/system/backup/save name=...`,等待 `.backup` 文件出现,下载到本机,可选清理远端文件 |
| `roswire script put <name> --source @<local.rsc>` | 读取本地文本,通过 API/REST 写入 `/system/script source=...`,不创建 RouterOS 文件 |

SSH 认证规则:

- 默认复用 API `user` / `password` 作为 SSH 登录凭据,减少重复配置。
- 如果提供 `--ssh-user``--ssh-password``--ssh-key`,则 SSH transfer 使用显式 SSH 凭据,不影响 API/REST 控制面登录。
- 设置 `--ssh-key` 时优先使用 key auth;加密私钥 passphrase 不得交互式询问,必须通过 profile secret `ssh_key_passphrase` 非交互提供。
- SSH host key 必须非交互校验。MVP 阶段要求用户通过 `--ssh-host-key` 或 profile `ssh_host_key` 提供期望指纹;未知或不匹配时失败。
- 所有 SSH 凭据都属于敏感信息,不能进入错误上下文、debug 日志、dry-run 输出或 shell 建议命令。

SSH 服务准备:

| 步骤 | API/REST 动作 | 说明 |
| --- | --- | --- |
| 检查 SSH 服务 | `/ip/service/print where name=ssh` | 读取 `disabled``port``address` 当前值 |
| 启用 SSH | `/ip/service/set ssh disabled=no` | 仅当用户显式传入 `--ensure-ssh` 时允许执行 |
| 设置白名单 | `/ip/service/set ssh address=<merged-cidrs>` | 只允许把 `<cidr>` 追加/合并进现有 `address` 列表,禁止无提示替换为更宽范围 |
| 设置端口 | `/ip/service/set ssh port=<port>` | 仅当用户显式传入 `--ssh-port` 或 profile `ssh_port` 时修改 |
| 恢复配置 | `/ip/service/set ssh ...` | 仅当用户传入 `--restore-ssh` 时恢复任务前快照 |

SSH 白名单安全规则:

- `--ensure-ssh` 需要修改 SSH 服务时,必须提供 `--allow-from` 或 profile `allow_from`;不能默认写入 `0.0.0.0/0``::/0`- 如果当前 `/ip service ssh address` 已经覆盖客户端来源,则不需要修改白名单。
- 如果需要修改白名单,只能基于任务前快照做追加/合并,不能覆盖掉管理员已有地址限制。
- 如果传入的 CIDR 明显过宽(例如 `0.0.0.0/0``::/0`),返回 `SSH_WHITELIST_UNSAFE`- 如果启用了 `--restore-ssh`,必须在成功、失败和中断路径中尽力恢复任务前 SSH 服务配置;恢复失败要写入 `stderr` 结构化错误或警告,不能静默吞掉。

不做默认承诺:

- 不默认假设 REST 支持 multipart/form-data 上传。
- 不保留 FTP、`/tool fetch`、临时 HTTP server 等其它传输后端。
- 不在未传入 `--ensure-ssh` 时自动开启 RouterOS SSH 服务。
- 不自动修改防火墙规则;本阶段只管理 `/ip service ssh address` 白名单。
- 不默认通过 API/REST 读取或写入大文件内容。RouterOS 文件菜单对 `contents``/file/read` 有明确大小/分块限制,只能作为小文本或诊断能力。
- 不在用户未请求文件工作流时探测或修改 SSH 服务;普通 RouterOS 命令路径不应触碰 SSH。

MVP 阶段可以先实现 `dry-run` 计划和清晰错误:如果 SSH 服务未启用且用户没有传入 `--ensure-ssh`,返回 `SSH_SERVICE_UNAVAILABLE`;如果缺少白名单,返回 `SSH_WHITELIST_REQUIRED`。

所有文件传输命令都必须有:超时、大小限制、校验和校验、覆盖策略、临时文件清理策略和脱敏日志。文件路径必须做最小化规范化,禁止把本地绝对路径或敏感目录写入错误 payload。

## 5. 标准错误模型

为了让 Agent 能够解析错误并进行自我修正,所有错误必须使用稳定的 JSON 结构。

```rust
use serde::Serialize;
use std::collections::BTreeMap;

#[derive(Serialize)]
pub struct RosWireError {
    pub error_code: String,
    pub message: String,
    pub hint: Option<String>,
    pub context: ErrorContext,
}

#[derive(Serialize)]
pub struct ErrorContext {
    pub command: String,
    pub requested_protocol: String,
    pub selected_protocol: String,
    pub transfer_backend: Option<String>,
    pub routeros_version: String,
    pub host: String,
    pub resolved_args: BTreeMap<String, String>,
}
```

`requested_protocol` 记录用户请求值,例如 `auto`、`rest`、`api-ssl`。`selected_protocol` 记录实际命中的协议,例如 `rest`、`api-ssl`、`api` 或 `unknown`。`transfer_backend` 仅在文件传输工作流中设置,例如 `ssh`。`routeros_version` 使用解析后的方言值,例如 `v6`、`v7` 或 `unknown`。在登录失败、版本探测失败等早期错误路径中,不能为了填充这些字段而发起额外有副作用的探测命令。

### 错误码建议

| 错误码 | 含义 | 退出码 |
| --- | --- | ---: |
| `USAGE_ERROR` | CLI 参数缺失或格式错误 | `2` |
| `CONFIG_ERROR` | 环境变量或配置不完整 | `2` |
| `PROFILE_NOT_FOUND` | 请求的本地配置 profile 不存在 | `2` |
| `CONFIG_INSECURE_PERMISSIONS` | `~/.roswire``config.toml` 权限过宽 | `2` |
| `SECRET_BACKEND_UNAVAILABLE` | 请求的 secret 后端不可用,例如系统钥匙链不可用 | `2` |
| `SECRET_NOT_FOUND` | 配置引用的 secret 不存在 | `2` |
| `SECRET_DECRYPT_FAILED` | 本地加密 secret 解密失败 | `3` |
| `AUTH_FAILED` | RouterOS 认证失败 | `3` |
| `NETWORK_ERROR` | TCP/TLS/HTTP 连接失败 | `4` |
| `ROS_API_FAILURE` | RouterOS 返回 trap/error | `1` |
| `UNSUPPORTED_ACTION` | 当前 action 无映射或未实现 | `2` |
| `SSH_SERVICE_UNAVAILABLE` | RouterOS SSH 服务未启用,且用户未允许自动开启 | `2` |
| `SSH_HOST_KEY_REQUIRED` | SSH 文件传输缺少 RouterOS host key 指纹 | `2` |
| `SSH_HOST_KEY_MISMATCH` | RouterOS SSH host key 与期望指纹不匹配 | `3` |
| `SSH_WHITELIST_REQUIRED` | SSH 文件传输缺少来源白名单 CIDR | `2` |
| `SSH_WHITELIST_UNSAFE` | SSH 白名单 CIDR 过宽或会放宽现有访问限制 | `2` |
| `SSH_RESTORE_FAILED` | 文件传输后恢复 SSH 服务配置失败 | `6` |
| `FILE_TOO_LARGE` | 文件超过当前后端或安全策略限制 | `2` |
| `FILE_TRANSFER_FAILED` | SSH 文件传输执行失败 | `6` |
| `SERIALIZATION_ERROR` | JSON 编解码失败 | `5` |
| `HELP_TOPIC_NOT_FOUND` | 请求的帮助主题或命令不存在 | `2` |
| `SCHEMA_UNAVAILABLE` | 请求的命令 schema 尚未登记 | `2` |
| `REMOTE_SCHEMA_UNAVAILABLE` | 远端 schema 或能力覆盖不可用,只能使用静态目录 | `2` |
| `CAPABILITY_PROBE_FAILED` | 远端只读能力探测失败,输出已降级 | `1` |
| `REMOTE_SCHEMA_STALE` | 本地远端 schema 缓存已过期或与设备指纹不匹配 | `2` |
| `INTERNAL_ERROR` | 未预期内部错误 | `5` |

## 6. 模块划分

```text
src/
├── main.rs            # 入口;全局错误捕获;stdout/stderr 分流
├── args.rs            # clap 定义与 CLI 解析
├── error.rs           # 统一错误类型与 JSON 输出
├── mapping.rs         # CLI path/action 到协议请求的转换
├── config/
│   ├── mod.rs         # 配置解析入口与优先级合并
│   ├── paths.rs       # ~/.roswire、ROSWIRE_HOME、权限检查
│   ├── file.rs        # config.toml 读写与 schema version
│   ├── profiles.rs    # profile 解析、继承与默认值
│   ├── secrets.rs     # plain/encrypted/keychain secret 解析与脱敏
│   └── logging.rs     # JSONL 日志、滚动与 30 天保留策略
├── introspect/
│   ├── mod.rs         # Agent 自描述入口
│   ├── catalog.rs     # 命令目录与索引
│   ├── help.rs        # 机器可读详细帮助
│   ├── schema.rs      # 参数与输出 JSON Schema
│   ├── discovery.rs   # 远端 schema/capability 只读探测与覆盖合并
│   ├── cache.rs       # ~/.roswire/cache/schema 下的动态 schema 缓存
│   ├── config.rs      # config inspect/profiles 的脱敏输出
│   └── doctor.rs      # 本地/远端诊断检查
├── workflow/          # 跨协议编排,不直接实现底层协议
│   ├── mod.rs         # 工作流入口与计划模型
│   ├── import.rs      # 上传 .rsc 并执行 /import
│   ├── export.rs      # 生成并下载 .rsc
│   ├── backup.rs      # 生成并下载 .backup
│   └── script.rs      # 小文本写入 /system/script source
├── protocol/
│   ├── mod.rs              # Protocol trait 与协议路由器
│   ├── discovery.rs        # 首次连接探测、协议优先级与能力选择
│   ├── classic/
│   │   ├── mod.rs          # 原生 API 共享入口
│   │   ├── transport.rs    # TCP/TLS 连接与超时控制
│   │   ├── sentence.rs     # word/sentence 编解码
│   │   ├── login.rs        # 现代登录与旧式 v6 登录回退
│   │   ├── dialect.rs      # v6/v7 方言 trait 与能力描述
│   │   ├── v6.rs           # RouterOS v6 方言实现
│   │   └── v7.rs           # RouterOS v7 方言实现
│   └── rest_v7.rs          # RouterOS v7 REST 实现
└── transfer/               # 客户端 ↔ RouterOS SSH 文件传输
    ├── mod.rs              # 文件传输 trait、计划模型与统一错误映射
    ├── ssh.rs              # SSH 文件上传下载实现
    └── ssh_service.rs      # 通过 API/REST 检查、启用、白名单与恢复 SSH 服务
```

## 7. 依赖策略

限制依赖是项目目标之一,但“零运行时依赖”不等于“零 Rust crate”。优先选择同步、阻塞式实现,避免为了单次 CLI 调用引入完整异步运行时。

推荐起点:

```toml
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
toml = "0.8"
directories = "6"
keyring = "3"
secrecy = "0.10"
zeroize = "1"
chacha20poly1305 = "0.10"
rand = "0.9"
schemars = "0.8"

# REST 客户端候选:实现时二选一,并禁用默认 TLS 后端。
# reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
# ureq = { version = "3", default-features = false, features = ["json", "rustls"] }

# SSH 文件传输:选用 ssh2,统一封装 SCP/SFTP 能力。
ssh2 = "0.9"
```

约束:

- 不默认使用 `tokio = { features = ["full"] }`,除非后续证明需要长连接或并发任务。
- TLS 优先使用 Rustls,减少系统 OpenSSL 依赖带来的发布复杂度。
- SSH 文件传输依赖固定为 `ssh2`。实现时优先尝试 SFTP;如果目标 RouterOS 版本不支持 SFTP,再验证是否可用 SCP。
- `ssh2` 会引入 `libssh2` 及其 native 依赖链,发布计划必须覆盖 macOS/Linux 静态或半静态打包。
- 如果 `ssh2` 在目标平台无法可靠打包,必须先记录为发布 blocker,不自动切换到其它 SSH 库。
- `keyring` 是默认 secret 后端依赖;如果目标平台没有可用钥匙链,必须返回结构化错误,不回退为明文。
- `chacha20poly1305` 只用于 `encrypted` secret;master key 不能硬编码进程序。
- `schemars` 用于生成和测试 Agent 可读 JSON Schema;命令 help 仍需要维护人工说明、示例和自愈提示。
- 所有新增依赖都必须说明用途、二进制体积影响与替代方案。

## 8. 测试计划

### 单元测试

- CLI path/action 解析与协议映射。
- `stdout`/`stderr` 分流,不允许错误进入 `stdout`- 错误 JSON 快照,字段顺序和脱敏规则必须稳定。
- 配置优先级:CLI 参数 > 本地配置 profile > 默认值。
- 本地配置目录:`ROSWIRE_HOME` 覆盖、`~/.roswire` 创建、权限过宽返回 `CONFIG_INSECURE_PERMISSIONS`- `config.toml`:profile 选择、schema version、缺失字段默认值和未知字段报错策略。
- profile 选择:`--profile``default_profile`、单 profile 推导和 profile 不存在错误。
- Secret 解析:plain 需要显式允许、encrypted 解密失败、keychain 缺失、same-as 引用循环。
- 日志保留:默认 30 天、不能配置更长、清理过期文件、日志脱敏。
- Agent 自描述:`help --json``help <command> --json``commands --json``schema command/output --json` 输出稳定 snapshot。
- 动态 schema:默认 schema 命令不访问远端;`--remote` 才探测 RouterOS;缓存键、TTL、失效和脱敏规则稳定。
- 远端覆盖合并:静态目录的安全策略不被覆盖,远端字段和协议支持可以修正 capability。
- 能力探测降级:权限不足、路径不存在、REST/API 差异返回 `CAPABILITY_PROBE_FAILED` warning,而不是崩溃或猜测 schema。
- 配置检查:`config inspect --json` 输出来源、脱敏 secret 状态、日志策略和 warnings。
- 帮助安全:帮助示例、doctor 输出和 dry-run 不包含真实 secret 或本地敏感绝对路径。
- `auto` 协议选择:v7 同时可用 REST 与原生 API 时 REST 优先。
- `auto` 回落:REST 不可用或动作无 REST 映射时回落到 v7 原生 API。
- 显式协议优先:`--protocol api``api-ssl``rest` 不被自动改道。
- 认证失败不回落:任一候选协议确认认证失败后立即返回 `AUTH_FAILED`- SSH 文件传输工作流:上传脚本、导入 `.rsc`、生成并下载 backup/export 的计划生成。
- SSH 服务准备:未启用 SSH 且缺少 `--ensure-ssh` 时返回 `SSH_SERVICE_UNAVAILABLE`- SSH host key:缺少 `--ssh-host-key` / profile `ssh_host_key` 时返回 `SSH_HOST_KEY_REQUIRED`;不匹配时返回 `SSH_HOST_KEY_MISMATCH`- SSH 白名单:需要修改白名单但缺少 `--allow-from` 或 profile `allow_from` 时返回 `SSH_WHITELIST_REQUIRED`- SSH 白名单安全:拒绝 `0.0.0.0/0``::/0`,且不会覆盖管理员已有地址限制。
- SSH 服务恢复:成功、失败和中断路径都生成恢复计划;恢复失败返回 `SSH_RESTORE_FAILED`
### 集成测试

- RouterOS CHR 或测试设备上的原生 API 登录、print、add、set、remove。
- RouterOS v6 原生 API:旧式 challenge-response 登录、v6.43+ 现代登录、典型字段差异。
- RouterOS v7 原生 API:现代登录、典型字段差异、与 REST 语义一致性。
- RouterOS v7 REST 登录、print、add、set、remove。
- RouterOS v7 双协议环境:验证 REST 优先、原生 API 回落和错误上下文中的实际协议。
- 认证失败、接口不存在、TLS 失败、REST 404/401 等错误路径。
- SSH 文件传输后端:验证上传 `.rsc` 后导入、生成 `.backup` 后下载、白名单设置、服务恢复、大小限制、超时、覆盖策略和校验和失败路径。
- SSH 凭据路径:密码复用、显式 SSH 凭据、key auth、host key 校验、加密私钥不支持路径都要有稳定错误。
- 本机钥匙链:macOS Keychain、Linux Secret Service、Windows Credential Manager 至少各有 smoke test 或 documented fallback;执行入口见 [`keychain-smoke.md`]keychain-smoke.md- Agent 远端诊断:`doctor --include-remote --json` 访问 RouterOS 时必须声明 selected protocol、routeros version、服务状态和脱敏错误。

### 发布验证

- `cargo fmt --check`
- `cargo clippy -- -D warnings`
- `cargo test`
- Linux/macOS 发布二进制冒烟测试。

## 9. 里程碑

| 阶段 | 主题 | 交付物 |
| --- | --- | --- |
| M0 | 项目初始化 | 创建 `Cargo.toml``src/main.rs`、基础 CI |
| M1 | CLI、本地配置与自描述 | 实现 `args.rs``config/``introspect/``error.rs``mapping.rs``~/.roswire/config.toml` 和静态 schema 目录 |
| M2 | 协议抽象与自动选择 | 定义 `Protocol` trait、统一请求/响应类型、首次探测与优先级策略 |
| M3 | 原生 API 共享层 | 实现 RouterOS API sentence(协议句)编解码、TCP/TLS 传输和登录流程 |
| M4 | v6/v7 方言 | 实现原生 API v6/v7 方言、字段归一化和兼容测试 |
| M5 | REST API | 实现 v7 REST 映射、HTTP 状态码归一化和 JSON 解析 |
| M6 | 动态 schema 与 SSH 文件传输 | 实现远端 schema 覆盖、缓存、SSH 服务准备、白名单合并/恢复、上传下载和文件工作流编排 |
| M7 | 测试与文档 | 完成单元测试、集成测试、README 示例校正 |
| M8 | 发布与生产级门槛 | GitHub Releases、校验和、基础安装说明、[`production-readiness.md`]production-readiness.md 中的 P0 blocker 关闭 |

## 10. Agent 自愈闭环

当 Agent 调用 `roswire` 失败时,设计闭环如下:

1. Agent 执行 `roswire ip address add interface=ether99 address=10.0.0.1/24 --json`1. RouterOS 返回 `no such interface (ether99)`1. `roswire``stderr` 写入标准 JSON 错误,退出码为 `1`1. Agent 运行时检测到非零退出码,读取并解析 `stderr`1. Agent 从 `message``hint` 中识别接口不存在。
1. Agent 自动执行 `roswire interface print --json` 获取可用接口,再生成下一步修正命令。

## 11. 给实现 Agent 的启动提示

```text
你是一名资深 Rust 工程师。请按照 docs/develop-plan.md 实现 roswire。

第一项任务:
1. 初始化 Rust 项目,创建 Cargo.toml 与 src/main.rs。
2. 实现 src/error.rs、src/args.rs、src/config/ 与 src/mapping.rs。
3. 实现 `protocol=auto` 的协议探测骨架:REST、api-ssl、api 的候选顺序和错误分类。
4. 实现 SSH 文件传输的 CLI 配置骨架:--transfer ssh、--ssh-host-key、--ensure-ssh、--allow-from、--restore-ssh。
5. 实现 Agent 自描述骨架:help --json、commands --json、config inspect --json、schema command --json;默认只读本地静态目录。
6. 预留动态 schema 骨架:schema discover --remote --json、schema command ... --remote --json、远端覆盖合并与 ~/.roswire/cache/schema 缓存接口。
7. 确保错误只以稳定 JSON 写入 stderr。
8. 默认载荷中不要包含时间戳或随机 ID。
9. 使用 BTreeMap 保证参数排序稳定。
10. 为 CLI 解析、本地配置、secret 解析、日志保留、Agent 自描述、动态 schema 缓存、协议自动选择、SSH 服务准备计划与错误序列化添加单元测试。
```