ek-station 0.3.1

Station for the ek ecosystem: headless plugin host bootstrap - assembles ek-core MainEngine from a compile-time plugin factory table, runs the svctl supervisor service (list/start/remove over bus ask) and a headless main (readiness marker + graceful close).
Documentation
# ek-station DESIGN


设计与决策记录(docs/ek-ui-station-plan.md 的 S1a 落地面;扎钉后不放)。

## BD(边界钉)


- **BD-U1 svctl.stop ≅ svctl.remove**:ek-core 0.3.0 无"停而不摘表"导出
  (`close_one` 是私有方法,公开面只有 `close_all`/`remove_plugin`)。
  运行期下线 = remove(五步闭停 + 摘表 + `ePlugDown` 广播);重启用 start
  (宿主 `add_plugin` 在 start 后即时 `launch_one` 上线 + `ePlugUp` 广播)。
  测试 c0 全链钉死。
- **BD-U2 工厂 build 用 `Arc<dyn Fn>` 而非 `fn` 指针**:测试探针需要捕获
  共享槽;S3 参数化配置也吃这个签名。行动面不变(零参调用)。
- **BD-U3 headless 的 stdin 退出策略**:Windows 控制台无 std 信号面,
  `SetConsoleCtrlHandler` 需三方/unsafe → v1 记录:`q`/`quit` 行 = 优雅退出。
  ~~管道 EOF = 优雅退出~~ **0.1.4 文档纠错(T2 t2g 实测推翻)**:EOF 与 stdin
  不可用(detached)同归**常驻 keep-alive**——launcher 的 spawn 用 stdin=null,
  子进程起手即 EOF,EOF 若走优雅退则 launcher 协议即碎。五步闭停在 `q` 与
  窗口关闭(ek-ui 侧)路径生效;常驻进程由进程外 kill 终止。
  **0.2.12 补两件事(审核 P12 的出口,不改默认)**:① 这一档从此有测试钉 ——
  `headless_exit.rs` 的 t4 钉「EOF 之后进程必须还活着」、t5 钉「`--exit-on-eof`
  走优雅闭停」。此前四个测试全喂 `q\n`,EOF 分支从未被跑过:谁把它改成优雅退,
  四个测试照绿而 launcher 协议当场碎,0.1.4 那条实测纠正从此没有守卫。
  ② 新增 `--exit-on-eof` 开关(缺省**不开**,常驻仍是默认):手跑 `ek-station < NUL`
  或在管道里跑时,缺省留下的是持有 CTP 会话的孤儿进程,这条给一个优雅出口。
  常驻本身仍无超时位、无退出条件 —— 0.2.11 审核把它记为按裁定保留,不加 `--keep-alive`。
- **BD-U4 `--from` 清单延迟校验**:行格式错即时报(exit 2),未知工厂名
  延迟到 boot 单一硬报错点(exit 1)——避免两套重名/存在性校验的重复面。
- **BD-U5 构造期身份(多账号面已改写)**:原 v1 口径"工厂名 == 插件注册名
  (dm/oms/ctp 各一实例)"**已废**。`StartReq.name` 非空 = 以该注册名建实例
  (工厂须登记 `PlugKind.build_named`;ctp 已登记,dm/oms 未登记 → 显式回拒,
  不静默退回零参构造);空 = 旧行为(工厂名即注册名,单实例零回归)。
  `StartRes.Started.name` 一律回带**插件自报的 `name()`**,不再假设工厂名。
  网关注册名由 (broker, 账号) 派生(`ek_ctp::derived_gateway_name`),
  **不进配置文件**。

- **BD-U6 管制面不可自摘(0.2.11,应用级审核 P1)**:`svctl.remove(name = "svctl")`
  一律回 `RemoveRes::Rejected`。两条理由:① `Supervisor` 是全系统唯一持
  `Arc<MainEngine>` 的插件,而工厂表(`default_kinds`/ek-ui 的 `ui_kinds`)都不登记
  "svctl" 这一项 —— 摘掉之后 `svctl.start` 也只会回 `unknown factory: svctl`,
  启停面**没有恢复入口**(单向门);② 请求在本插件自己的 worker 里处理,摘自己
  = `close_one` 等自己排空,只能走到 ek-core 那 2 秒超时的 detach 分支,回包也发不出。
  钉在测试 c0 step5/6(拒 + 拒后 svctl 仍在场)。观测面 `ui` 的同一道闸长在 ek-ui 侧
  (0.4.3):那条指令正是 ui 自己的 worker 在执行,运维窗不给它画移除钮。
- **BD-U7 回执 `name` 栏只放原请求名(0.2.11,审核 P2)**:0.2.10 之前非
  `UnknownName` 的宿主错被塞进 `NoSuchName { name: format!("{e:?}") }`,而该栏文档
  写明是「原请求名」—— 消费侧(ek-ui 日志)会把错误文本当名字打,原请求名丢失。
  现:新增 `Rejected { name, reason }`,名字一律照请求回填,错因只进 `reason`。

## 契约面(wire.rs)


`PlugRow { name, version, state(Debug 形状), deps, spilled, panics }`;
`StartRes/RemoveRes` 用**业务回执枚举**(Started/Rejected、Removed/NoSuchName/Rejected)
而非 `ReplyErr` 承载业务错 —— 回执在总线/测试里可读可断言。

## BD-CFG 配置托底钉清单(2026-10-25;用户裁定「软集中」,单源全文)


> 配套修订:ek-ui DESIGN 的 BD-UI-19 升第三版「零配置**文件**」——宿主/界面
> 不代读、不代转发、不代解释配置值;宿主唯一配置动作 = 载入两步直调(搬运)。
> 版本锚:ek-core 0.4.0(契约) / ek-cfg 0.1.0(库) / 本 crate 0.3.0(加载单点+登记)。

- **BD-CFG-1(依赖真相)**:各模块对托底的依赖 = Cargo 里 `ek-cfg`(库,编译期);
  `cfg` 插件实例 = 库在总线上的收件形貌,**非关卡**。apply 走宿主直调、
  秘值走库函数、save 有库级兜底。
- **BD-CFG-2(契约单份)**:`SectionMap`/`Field`/`Configurable`/`ApplyReport`
  住 ek-core,全链一份(0.2.15「两份同名类型 = 运行时静默断链」判据适用)。
  ek-core 只含纯数据与 trait,**不含格式解析**。
- **BD-CFG-3(格式钉)**:行式键值对(pickfile 同源风格):`key = value`、
  `#` 注释、列表逗号分列、首行 `rev = 1`;未知 key = 警告跳过不炸;
  解析/序列化在 ek-cfg;文件位置 `${config_dir}/ek/cfg/<ns>.ini` 经
  `dirs::config_dir()`(Win `%APPDATA%` / Linux `~/.config`,一码两平台)。
- **BD-CFG-4(落盘钉)**:原子三步写 `.new` → 旧文改 `.bak` → 新文转正 → 清
  `.bak`(std rename 在 Windows 不覆盖已存在目标,必须三步);读序
  current → `.bak` → Err 上抛。
- **BD-CFG-5(单写者)**:每 ns 一个写者;字段级并发 = 插件自身 worker 串行。
- **BD-CFG-6(生效三档)**:Live 即时 / NextUse 下次使用 / NextStart =
  `svctl.remove`→`start`(插件级重启,秒级)——**配置永不要求重启整个应用**。
  载入路径 `ApplyReport.deferred` 显形;运行期修改的重启生效提示走各插件
  自己的 configure 回执;`cfg.save` 回执只报落盘成败。
- **BD-CFG-7(文件是影子)**:运行中插件状态唯一权威在内存;手改文件不热生效;
  持久面只认「最后一次受理成功」——不认退出前状态、不做退出兜底写。
- **BD-CFG-8(save 触发)**:仅「配置面受理成功」一个触发时机,不受连接结果
  牵引(受理 ≠ 连接成功;网络失败不阻止持久化用户意图)。
- **BD-CFG-9(缺席判定)**:`ask` 超时 ≡ `NoRoute` ≡ 实例缺席,同闸单分支,
  不重试不区分。
- **BD-CFG-10(软集中/禁拉起)**:cfg 实例缺席时,调用方**直调同一库函数**
  (原子纪律/秘面分流全在库;兜底不是劣化实现);**禁止任何插件为写配置而
  临时拉起 cfg 插件**(写盘付不起一个插件生命周期,且管制面唯一在 svctl)。
- **BD-CFG-11(审计线)**:兜底成功必留一行 eLog「cfg 缺席,已直写落盘」,
  每插件每会话一次性(内部 Mutex<bool>),禁静默。
- **BD-CFG-12(秘面落地)**:明面文件**零秘值**,只存引用串
  `secret://ek/<ns>/<key>`;真值在系统凭据库或注入环境变量。
- **BD-CFG-13(秘键单点)**:`ek_cfg::secret::{set,get,delete,ref_of}` 统一收
  `(ns, key)`,条目名 `ek/<ns>/<key>` 与引用串**单源生成**,永不发散;
  ns 唯一性天然隔离多插件。
- **BD-CFG-14(受理三行序)**:配置受理成功的插件固定顺序
  `secret::set(...)` → `dump_config()`(秘面出引用)→ `ask("cfg.save", …)`。
- **BD-CFG-15(dump 口径)**:dump 只输出要持久的字段——不输出 = 不持久
  (临时态字段天然豁免);秘面字段在 dump 里只有引用形态。
- **BD-CFG-16(解析时机)**:`apply_config` 只回灌引用,不解析秘值;取秘由
  插件在**自己的时机**(典型 = on_start 后自发连接前)做;
  **宿主管理线程禁止调 `secret::get`**(keyring/DBus 可秒级阻塞)。
- **BD-CFG-17(秘库无枚举)**:keyring 不提供跨平台枚举——「已存/未存」以
  明面文件的引用串为准;孤儿秘条容忍(幂等覆盖,残留无语义影响),
  **不做全局 GC**;`cfg.delete(ns)` 只清文件可见键。
- **BD-CFG-18(脱敏)**:秘值类型 `SecretValue(Zeroizing)` Drop 即抹内存;
  含秘结构**手工 Debug**(只打 `******`),禁 `derive(Debug)`;
  eLog/总线广播/`cfg.list` 回包**永零秘值**(测试机器扫描钉死)。
- **BD-CFG-19(信任边界)**:同机同用户态攻击者(root/管理员/调试器)不在
  防御范围;承诺面 = 静态落盘 / 日志 / 总线**零明文**。
- **BD-CFG-20(cfg 自身)**:`as_configurable() = None`(托底者不吃托底);
  路径/后端 = 构造期装配参数(`CfgStore::default()`),不进回灌;
  `build_named = None`(单实例;多实例 = 自造竞态);缺省清单必含 `cfg`,
  手工摘除须显形警告。
- **后端矩阵(BD-CFG-12 的选型)**:A = keyring(Win Credential Manager /
  Linux Secret Service / macOS 钥匙串);B = env 注入
  (`EK_SECRET__<NS>__<KEY>`,headless 服务器常态);序贯探测
  A 可用→A,A 无且 env 有→B,皆无→字段空置 +「待手配」显形,不静默;
  C(DPAPI 文件)不做,D(口令加密文件)argon2id+AES-GCM 暂缓至 v3;
  E(明文+权限)拒绝。探测与取值都在插件 worker 侧(BD-CFG-16 同义)。