@botiverse/k-carrier 0.1.7 → 0.2.0

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 (63) hide show
  1. package/NOTICE +5 -2
  2. package/README.md +72 -26
  3. package/core/src/artifact/download.ts +23 -1
  4. package/core/src/artifact/gzip.ts +26 -0
  5. package/core/src/artifact/source.ts +2 -0
  6. package/core/src/{createUpgrader.ts → createRunner.ts} +15 -14
  7. package/core/src/index.ts +12 -3
  8. package/core/src/launcher/launch.ts +15 -0
  9. package/core/src/launcher/supervise.ts +170 -0
  10. package/core/src/lifecycle/commandHost.ts +111 -0
  11. package/core/src/lifecycle/hostAdapter.ts +28 -16
  12. package/core/src/operation.ts +47 -25
  13. package/core/src/operationLifecycle.ts +2 -7
  14. package/core/src/platform/ops.ts +7 -0
  15. package/core/src/platform/posix.ts +26 -6
  16. package/core/src/platform/windows.ts +8 -2
  17. package/core/src/protocol/runner.ts +81 -0
  18. package/core/src/provenance/journal.ts +1 -1
  19. package/core/src/quarantine.ts +167 -0
  20. package/core/src/runner/cli.ts +27 -0
  21. package/core/src/runner/execute.ts +68 -0
  22. package/core/src/txn/engine.ts +41 -85
  23. package/core/src/txn/fileEffects.ts +15 -1
  24. package/core/src/txn/hostCallBudget.ts +4 -1
  25. package/core/src/txn/hostCallUncertain.ts +2 -0
  26. package/core/src/txn/lock.ts +81 -37
  27. package/core/src/txn/state.ts +1 -1
  28. package/core/src/upgrade/drive.ts +31 -2
  29. package/core/src/upgrade/outcome.ts +1 -1
  30. package/core/src/upgrade/recover.ts +22 -1
  31. package/core/src/upgrade/retire.ts +1 -1
  32. package/core/src/upgrader.ts +7 -8
  33. package/docs/design.md +173 -0
  34. package/docs/guide.md +196 -0
  35. package/docs/harness-design.md +75 -170
  36. package/docs/integration.md +221 -354
  37. package/docs/prior-art/design-influences.md +26 -0
  38. package/docs/prior-art/external-runner-research.md +49 -0
  39. package/docs/reference.md +209 -0
  40. package/docs/test-plan.md +89 -92
  41. package/harness/src/adapter/releaseKnob.ts +1 -1
  42. package/harness/src/adapter/serviceChecks.ts +5 -5
  43. package/harness/src/artifact/m1.ts +8 -8
  44. package/harness/src/artifact/m1Resume.ts +2 -2
  45. package/harness/src/artifact/m3.ts +25 -104
  46. package/harness/src/artifact/m3Hosts.ts +9 -61
  47. package/harness/src/artifact/m4.ts +3 -3
  48. package/harness/src/artifact/m5.ts +5 -5
  49. package/harness/src/artifact/m6.ts +6 -6
  50. package/harness/src/artifact/m6Status.ts +1 -1
  51. package/harness/src/examples/checks.ts +10 -13
  52. package/harness/src/fixtures/cliToolSource.ts +166 -0
  53. package/harness/src/fixtures/externalCrashAdapter.ts +19 -0
  54. package/harness/src/fixtures/managedHost.ts +100 -0
  55. package/harness/src/fixtures/serviceSource.ts +181 -0
  56. package/harness/src/fixtures/supervisedAdapter.ts +57 -0
  57. package/harness/src/scenario/processScan.ts +3 -1
  58. package/harness/src/scenario/sandbox.ts +2 -2
  59. package/harness/src/teeth/artifact.ts +3 -3
  60. package/harness/src/teeth/examples.ts +1 -1
  61. package/package.json +5 -3
  62. package/docs/design-v1.md +0 -246
  63. package/docs/prior-art.md +0 -150
@@ -0,0 +1,49 @@
1
+ # External installer research
2
+
3
+ The 2026-09-06 survey asked whether install, self-update and remote upgrade could
4
+ share an external executor without introducing another permanent service.
5
+ These are source-reading observations and K design inferences, not results from
6
+ running those products. Moving upstream links are not a frozen comparison.
7
+
8
+ ## Rustup: a thin bootstrap and replaceable installer
9
+
10
+ [rustup-init.sh](https://github.com/rust-lang/rustup/blob/main/rustup-init.sh)
11
+ selects a platform download and runs the installer. The shell does not implement
12
+ Rust installation. [Self-update](https://github.com/rust-lang/rustup/blob/main/src/cli/self_update.rs)
13
+ also obtains an installer executable; the helper has a release version and may
14
+ remain until a later invocation cleans it up.
15
+
16
+ [Unix replacement](https://github.com/rust-lang/rustup/blob/main/src/cli/self_update/unix.rs)
17
+ and [Windows replacement](https://github.com/rust-lang/rustup/blob/main/src/cli/self_update/windows.rs)
18
+ handle different execution/deletion constraints. A different PID alone does not
19
+ prove that an updater can survive stopping a service or replace a running Windows
20
+ executable safely.
21
+
22
+ **K decision:** share an independently runnable installer across entrypoints.
23
+ Keep its persistent state, runtime, trust and recovery ownership explicit.
24
+
25
+ ## Tailscale: installation and service readiness are separate
26
+
27
+ The surveyed Linux binary path in
28
+ [clientupdate.go](https://github.com/tailscale/tailscale/blob/main/clientupdate/clientupdate.go)
29
+ attempts a service restart separately and reports when bytes were updated but
30
+ restart failed. Platform/package-specific paths respect the installation owner.
31
+ This observation does not establish whether other paths have rollback.
32
+
33
+ **K decision:** successful installation is insufficient for service promotion.
34
+ Probe the live process and defer installations owned by another manager.
35
+
36
+ ## Datadog: reuse slots, not a second control plane
37
+
38
+ The [earlier survey](design-influences.md) informed K's stable/experiment slots.
39
+ The published [Windows installer test interface](https://pkg.go.dev/github.com/DataDog/datadog-agent/test/new-e2e/tests/installer/windows)
40
+ distinguishes direct install from starting, promoting and stopping experiments.
41
+ The follow-up did not verify all current installer internals.
42
+
43
+ **K decision:** retain the existing transaction, lock, journal and host adapter.
44
+ A disposable runner can use them without a permanent installer service or a
45
+ parallel remote-job database. Recovery and archived receipts remain local.
46
+
47
+ The resulting [design](../design.md) specifies a built installer, a bounded
48
+ request protocol and an external controller. Product data compatibility,
49
+ platform packaging and remote authorization remain product responsibilities.
@@ -0,0 +1,209 @@
1
+ # K reference
2
+
3
+ Normative wire formats, exit codes, budgets and on-disk layout. This document
4
+ is a contract: it states what K does and requires, not why. For the
5
+ reasoning, read the [design](design.md); for a narrative, the
6
+ [guide](guide.md).
7
+
8
+ ## Protocol v1
9
+
10
+ One JSON request on stdin, one response on stdout, then exit. Logs go to
11
+ stderr. Decoded input is bounded to 16,384 JavaScript string code units.
12
+ Unknown fields, actions or protocol versions, invalid ids or targets, and a
13
+ nonboolean `consented` are rejected before the adapter factory runs.
14
+
15
+ Request ids and target strings are nonempty, trimmed strings of at most 256
16
+ code units.
17
+
18
+ ### Requests
19
+
20
+ ```json
21
+ {"protocolVersion":1,"action":"upgrade","id":"job-123","targetVersion":"2.0.0","consented":true}
22
+ {"protocolVersion":1,"action":"recover"}
23
+ {"protocolVersion":1,"action":"recover","expected":{"id":"job-123","targetVersion":"2.0.0"}}
24
+ {"protocolVersion":1,"action":"status"}
25
+ ```
26
+
27
+ `consented` records approval already obtained by an authenticated caller. It
28
+ is not authorization from an untrusted client. Ownership and compatibility
29
+ checks still apply.
30
+
31
+ `expected` binds automated recovery to one operation. The binding is checked
32
+ under the lock before any controller action. Missing or mismatched history
33
+ refuses recovery. A completed original operation replays its result even when
34
+ newer work is pending. An operator `recover` without `expected` runs once
35
+ against the current unfinished operation.
36
+
37
+ ### Actions and exit codes
38
+
39
+ | Action | Effect | Exit code |
40
+ |---|---|---|
41
+ | `upgrade` | Install the exact requested version; a mismatched source result is rejected | 0 promoted or up-to-date; 1 failure or rollback; 2 held; 3 unresolved |
42
+ | `recover` | Settle persisted work under the same lock; never starts a new upgrade | 0 successful or no recorded outcome; 1 recorded failure or rollback; 2 held receipt; 3 unresolved |
43
+ | `status` | Read the current receipt without lifecycle calls | 0 readable, including `genesis`; 1 unreadable |
44
+
45
+ ### Responses
46
+
47
+ Execution replies carry `protocolVersion`, `action`, `result`, `exitCode`,
48
+ `operation` and `error`. Input rejection or adapter-construction failure may
49
+ return only `protocolVersion`, `result`, `exitCode` and `error`. Termination
50
+ can leave no complete response; inspect persistent state and recover.
51
+
52
+ `operation.kind` is `observed` (with the operation record), `genesis` (no
53
+ operation recorded) or `unreadable`.
54
+
55
+ Rules:
56
+
57
+ - A successful upgrade completion must match the request id and target.
58
+ - Exceptions cannot manufacture a rollback receipt.
59
+ - `status` exit 0 means readable; it does not prove current health.
60
+ - `recover` can return `result: "recovered"` with exit 1 after restoring
61
+ stable and recording a rolled-back upgrade.
62
+ - A supervisor that cannot settle returns `result: "recovery-required"`, exit
63
+ 3, and a `recoveryFile` path.
64
+
65
+ ## Receipts and retries
66
+
67
+ `operation.json` holds the current operation. Before starting another, K
68
+ archives a terminal receipt at `receipts/<sha256(operation-id)>.json` under
69
+ the same lock. Archived receipts have no automatic garbage collection.
70
+
71
+ - An id binds to one target. A same-id terminal retry returns the current or
72
+ archived result without repeating lifecycle effects; a different target for
73
+ the same id is rejected.
74
+ - Recovery settles the interrupted operation, so retrying its id returns
75
+ that outcome.
76
+ - A new attempt, including one after a policy hold, needs a new id.
77
+ - Replayed results are historical, not live observations.
78
+ - Receipt retention is independent of transport delivery.
79
+ - Unreadable records refuse operations; missing history cannot be
80
+ reconstructed.
81
+ - `status` reads the current receipt only. There is no by-id status or
82
+ archive-list action.
83
+
84
+ ## Supervisor budgets
85
+
86
+ | Budget | Default | Option |
87
+ |---|---|---|
88
+ | Worker execution | 10 minutes | `executionTimeoutMs` |
89
+ | Each recovery attempt | 2 minutes | `recoveryTimeoutMs` |
90
+ | Recovery attempts | 2 | `recoveryAttempts` (0 to 10) |
91
+ | Total | execution + 2 × recovery (14 minutes) | `totalTimeoutMs` |
92
+ | Exit observation after termination | 1 second | fixed |
93
+
94
+ All budgets are positive integers. Artifact acquisition has separate transfer
95
+ budgets derived from the artifact size (`artifactTransferPolicy`). An
96
+ unconfirmed worker exit forbids takeover. Exhaustion preserves state and
97
+ returns exit 3 with a recovery file; it never reports success.
98
+
99
+ ## Engine host-call budget
100
+
101
+ Every host call, including fence, readback, resume and recovery, has a
102
+ positive budget, default 120 seconds (`hostCallBudgetMs`). A call whose
103
+ effect is uncertain raises `HostCallUncertain`; the worker retains its lock
104
+ until it exits. Bundled workers exit after flushing their response. An
105
+ in-process caller that receives `HostCallUncertain` must also exit rather
106
+ than reuse that worker.
107
+
108
+ ## Command controller protocol
109
+
110
+ `createCommandHost({stateDir, command, timeoutMs?})` runs an external
111
+ controller via argv, without a shell.
112
+
113
+ Request on stdin:
114
+
115
+ ```json
116
+ {"protocolVersion":1,"action":"fence"}
117
+ {"protocolVersion":1,"action":"quiesce"}
118
+ {"protocolVersion":1,"action":"stop","slot":"stable","artifactPath":"<stateDir>/slots/stable/artifact.bin"}
119
+ {"protocolVersion":1,"action":"start","slot":"experiment","artifactPath":"<stateDir>/slots/experiment/artifact.bin"}
120
+ {"protocolVersion":1,"action":"probe"}
121
+ {"protocolVersion":1,"action":"resume"}
122
+ ```
123
+
124
+ Response on stdout:
125
+
126
+ ```json
127
+ {"protocolVersion":1,"ok":true}
128
+ {"protocolVersion":1,"ok":true,"evidence":{"version":"2.0.0","pid":4242,"startId":"..."}}
129
+ ```
130
+
131
+ Rules:
132
+
133
+ - Output is bounded to 64 KiB. Each call has a positive timeout, default 30
134
+ seconds.
135
+ - A controller must do nothing without a complete request.
136
+ - `probe` evidence must come from one live service instance. The controller
137
+ process's own pid is invalid evidence.
138
+ - Reported errors exclude arbitrary stderr.
139
+ - Before delivering a command, the host durably records the controller pid in
140
+ a unique file under `controllers/`. Recovery waits for recorded controllers
141
+ to exit, then calls `fence`. Failure or timeout there prevents lifecycle
142
+ replay. The recorded pid is never used to kill an arbitrary process; pid
143
+ reuse or inaccessible identity yields a conservative unresolved result.
144
+ - `fence` acknowledgement is a product contract. K cannot infer it from
145
+ process exit. A fire-and-forget `stop` cannot establish termination.
146
+
147
+ ## Lock protocol
148
+
149
+ `upgrade.lock` has one live owner per state directory. Unique process-owned
150
+ contender entries serialize creation and reclamation, including the
151
+ partial-write window. Only entries of provably dead owners are reclaimed. Pid
152
+ reuse conservatively refuses acquisition; age never proves that a live owner
153
+ is dead. The protocol requires local atomic file creation and coherent
154
+ directory reads. It is not a distributed or NFS lock.
155
+
156
+ ## State directory layout
157
+
158
+ ```text
159
+ <stateDir>/
160
+ upgrade.lock live owner
161
+ upgrade.lock.claims/ contender entries
162
+ journal.jsonl append-only write-ahead phase record, fsync'd per line
163
+ operation.json current operation receipt
164
+ receipts/<sha256(id)>.json
165
+ slots/stable/artifact.bin
166
+ slots/stable/VERSION
167
+ slots/experiment/artifact.bin
168
+ slots/experiment/VERSION
169
+ incoming/ staging for verified downloads before slot placement
170
+ controllers/ recorded controller pids
171
+ ```
172
+
173
+ Each slot holds one `artifact.bin` and its `VERSION`. Package layouts and
174
+ additional install hooks need a product contract. Application data belongs
175
+ outside the slots. `status` reads `operation.json` without taking the lock.
176
+
177
+ Journal entries are `{seq, timestampMs, intent, detail}`. The `handing-over`
178
+ entry's `detail.priorStartId` records the incarnation probed before the
179
+ upgrade touched anything; it is absent when nothing was live. Renames and
180
+ journal appends fsync the containing directory on POSIX. Slot publication
181
+ also syncs the copied artifact, VERSION file and staging directory first.
182
+ A failed directory sync stops the operation. Windows directory sync is a
183
+ no-op and provides no equivalent power-loss durability guarantee.
184
+
185
+ ## Supervisor scratch layout
186
+
187
+ ```text
188
+ <scratchDir>/k-runner-<random>/
189
+ runner.mjs | runner.bin verified runner (mode 0700)
190
+ recovery.json invocation descriptor: file, interpreter, sha256, size, recover request
191
+ ```
192
+
193
+ `recovery.json` is an invocation descriptor, not a transaction log. It is
194
+ removed after settlement and retained on an unresolved result.
195
+ `resumeRunner(path)` re-verifies the runner against the recorded hash before
196
+ executing it.
197
+
198
+ ## Release metadata
199
+
200
+ ```json
201
+ {"version":"2.0.0","url":"https://.../service","sha256":"<hex>","size":123456,
202
+ "gzip":{"url":"https://.../service.gz","sha256":"<hex>","size":45678}}
203
+ ```
204
+
205
+ `sha256` and `size` always describe the installed bytes. When `gzip` is
206
+ present K downloads and verifies the compressed object, bounds
207
+ decompression, then verifies the canonical size and hash. Failure of a
208
+ selected gzip object is terminal; K does not fall back to the canonical URL.
209
+ Resume offsets refer to the compressed object.
package/docs/test-plan.md CHANGED
@@ -1,100 +1,97 @@
1
- # K (k-carrier) 测试计划 v1
1
+ # Test plan
2
2
 
3
- > 跑在 `harness-design.md` 设计的测试框架上;harness 先于功能层(executable-spec 顺序),本计划的每颗齿都进 harness teeth 注册表。
3
+ Use this plan to validate the [external runner design](design.md). The
4
+ [harness guide](harness-design.md) explains test structure and fault coverage.
4
5
 
5
- 对应 design v1.2 §3 的教义,这里是**可执行计划**:按里程碑排、每格给"测什么 / 怎么算过 / 必须会红的例子(must-red)"。规矩承自 mutation-runner 契约:**每颗齿声明时同时声明它的 must-red;全绿或全红都不发结论;先跑已知红/已知绿自验,harness 自己不合格不准验别人。**
6
+ ## Run the checks
6
7
 
7
- 里程碑与 profile 绑定:**每个里程碑的出口 = 对应 example demo 变绿**(没绿 demo 就没那档的支持 claim)。
8
+ Node 24 and the repository's pinned pnpm are required.
8
9
 
9
- ---
10
+ ```sh
11
+ pnpm install --frozen-lockfile
12
+ pnpm check
13
+ pnpm test:runner
14
+ node harness/src/cli.ts --list
15
+ node harness/src/cli.ts sim
16
+ node harness/src/cli.ts sim --seed 42 --json
17
+ ```
10
18
 
11
- ## M0 harness 自举(先于一切功能层)
19
+ `pnpm check` includes typechecking, lint, ratchets and all Node tests.
20
+ `test:runner` is a focused alternative when iterating on the external boundary,
21
+ not an extra requirement after the full suite. `--list` lists registered harness
22
+ checks; protocol and real-runner Node tests also contribute coverage.
12
23
 
13
- | 测什么 | 怎么算过 | must-red |
14
- |---|---|---|
15
- | fake-host(实现 HostAdapter 的最小假宿主,带可注入故障开关) | 五方法可被编排调用、故障开关能让任一方法定点失败 | 关掉故障开关注入 ⇒ 对应齿必须转绿(证明齿测的是故障不是常态) |
16
- | fake 静态 server(manifest+工件,认 Range,可篡改) | 正常链路可走通 | 篡改任一字节 ⇒ 下游校验齿红 |
17
- | **harness 自验**(mutation 契约 §自验承重墙) | 内置已知红/已知绿样例各≥1 + **1 个对抗样例**(结构过 fixture、违真 oracle) | 对抗样例被判 EFFECTIVE ⇒ harness 不上线 |
18
- | profile 分档执行器 | `--profile swap|daemon|managed` 只跑该档齿集 | cli 档误跑 L2 齿 ⇒ 计划红(档界齿) |
19
-
20
- ## M1 — L1 事务 + L0 工件(出口:`examples/swap-tool` 绿 = cli 档成立)
21
-
22
- | 测什么 | 怎么算过 | must-red |
23
- |---|---|---|
24
- | 状态机合法迁移 | 7 相全部合法路径逐条走通 | 非法迁移(如 idle→readback)被拒 |
25
- | **崩溃注入矩阵**(承重) | **迁移边×kill 点由脚本枚举生成**(禁手列);每点 kill -9 → 重启后 = 恢复 stable 或完成迁移 | 任一点出现双跑(两 incarnation 同活)或砖(起不来)⇒ 红;**journal 写后动作前的窗口必须被覆盖** |
26
- | journal 性质 | append-only、意图先于动作(WAL)、重放幂等 | 乱序/覆写 journal ⇒ 重放拒绝 |
27
- | 回滚对称性 | rolled-back 后 stable 完整可跑、experiment 槽清空、原因入 journal | 回滚后 experiment 残留可执行 ⇒ 红 |
28
- | config 同轨 | 配置 experiment/promote/rollback 走同一状态机 | config 绕过状态机直写 ⇒ 红 |
29
- | L0 校验+原子换 | sha256 不符拒装;换字节原子(半写不可见);Windows 运行中自替换 | 篡改工件 ⇒ 拒;swap 中途 kill ⇒ 旧字节完好 |
30
- | cli 档端到端 | swap-tool demo:升级→下次运行是新版;`held/rolled-back/up-to-date` 四态出口都可构造 | — |
31
-
32
- 已落地齿以 `k-harness --list` 为准——本文件**不手抄齿名**(手抄清单是 `--list` 的副本,只会漂向'少列一颗';ratchet 7 只能查'写下来的存在',查不了'该写的没写')。逐颗齿(层/档/must-red/定义位置)直接 `k-harness --list`。 本层判据形状:篡改工件 ⇒ 拒装;原子换(半写不可见、中途 kill 旧字节完好);未知平台/指名版本 ⇒ 拒;cli 档闭环(真升级 → 下次运行新版本 → state promoted);坏版本 ⇒ 自动回滚 + 旧可用 + experiment 清空;下载中途死 ⇒ Range 续传 + 全量验证。**下载层 8 洞**(L0 接入方挖出,每洞一齿):deadline **竞速**而非仅信号(注入不理会 AbortSignal 的 fetch 也必须超时);Rosetta 下 platform key **问硬件**(x64 Node 在 arm64 硬件选 arm64 target,探针只在 darwin+x64 被问);无 resumeDir 进度也必须动(单调收尾到全量);无 body 响应**两臂**(内存 + resume)都报 typed "no readable body"、绝不当作空前缀;静默被限界不是总时长(慢而正常存活、卡死点名 stall);主动放弃的 stall 是 typed DOWNLOAD_FAILED 点名原因;mid-body 的 stall 说 mid-body。
33
-
34
- ## M2 — L0.5 供应链:**不做**
35
-
36
- L0.5 已于 2026-08-06 移除(决定:不支持签名)。K 只验完整性(sha256 +
37
- size),不验来源真实性;原两级签名链、`m2.*` 四颗齿与 harness 的测试密钥链一并
38
- 删除。留一个没人接的签名接口比没有更糟——接入方会以为来路已经有人管了。理由,
39
- 以及它与 OS 代码签名的区别,见 `docs/design-v1.md` §L0.5。
40
-
41
- **防回滚不在这层**:manifest 版本低于当前且非 pinned ⇒ 默认拒,这是 L0 的
42
- `source-fails-closed` 管的,与签名无关。
43
-
44
- ## M3 — L2 生命周期 + L3 收敛(出口:`examples/service-daemon` 绿 = daemon 档成立)
45
-
46
- | 测什么 | 怎么算过 | must-red |
47
- |---|---|---|
48
- | **HostAdapter 契约一致性套件**(对任意 adapter 可跑,接入方复用) | quiesce↔resume 状态等价(含 **rolled-back 后 resume**);probe 证据同进程(pid+startId) | resume 后负载状态漂移 ⇒ 红;probe 返回缓存/文件拼的证据 ⇒ 红(换 pid 不换 startId 的假 probe 必须被抓) |
49
- | 交接顺序 | journal 意图→交接→新进程自证→才清旧,顺序断言 | 颠倒任两步 ⇒ 红 |
50
- | `binary_at_target` | same-PID probe:version+startId 绑同一 incarnation | 旧进程活着报新版本号 ⇒ 不绿(same-PID 就绪核反假绿) |
51
- | `host_lifecycle_converged` | 点名面读回一致;**面在 allowlist 才可作证** | 用不可读面(模拟 System Events 类)自称 same-source ⇒ 拒 |
52
- | **禁投影齿** | version/channel/升级次数灌真值、谓词面造假 ⇒ 必须不绿 | 任一元数据字段能把谓词转绿 ⇒ 红(version⊥state 实测教训) |
53
- | fail-closed 退役序 | 未过 host_lifecycle_converged 前退旧管理器 ⇒ 拒 + typed HOLD | 强行退役路径存在 ⇒ 红 |
54
- | ownership 检测 | 受管标记存在 ⇒ `held: managed-elsewhere`(typed、指向管理者) | 受管副本完成自升 ⇒ 红 |
55
-
56
- 已落地齿以 `k-harness --list` 为准——本文件**不手抄齿名**(手抄清单是 `--list` 的副本,只会漂向'少列一颗';ratchet 7 只能查'写下来的存在',查不了'该写的没写')。逐颗齿(层/档/must-red/定义位置)直接 `k-harness --list`。 本层判据形状:quiesce↔resume 账本逐字节等价(含回滚后 resume);probe 证据**绑定活化身**(探针说谎/报旧 startId ⇒ 红);每开关故障关掉齿必须绿;service 升级两种宿主形状(spawn 自起 / respawn 交给 owner)——真停旧、真起新、旧 pid 验证死、新化身 fresh startId;坏版本 ⇒ 旧版**真的拉回来在跑**(不是槽位回退);卡死 driver ⇒ 宿主调用预算超时 → 锁释放 → successor 凭**证据**(v2 + fresh startId)判交接完成,凭标志不恢复。`host_lifecycle_converged` / 禁投影 / 退役序 → M5 齿。
57
-
58
- ## M4 — L4 同意与通知
59
-
60
- | 测什么 | 怎么算过 | must-red |
61
- |---|---|---|
62
- | 策略门 | confirm 未答 ⇒ 零副作用;notify-only ⇒ 只通知不动 | confirm 前有任何盘面写 ⇒ 红 |
63
- | **通知可验齿**(Hipp 判据原样) | 构造真实失败(迁移写失败/readback 不一致)⇒ sink **真收到**结构化事件 | 删通知调用 ⇒ 此齿必须红;"代码调用了通知"但 sink 没收到 ⇒ 红 |
64
-
65
- 已落地齿以 `k-harness --list` 为准——本文件**不手抄齿名**(手抄清单是 `--list` 的副本,只会漂向'少列一颗';ratchet 7 只能查'写下来的存在',查不了'该写的没写')。逐颗齿(层/档/must-red/定义位置)直接 `k-harness --list`。 本层判据形状:confirm 未答 ⇒ **磁盘零副作用**(无 journal/slots/incoming,不是"没 promote"是"没 staged");同意只装**当初同意的那个版本**(中途服务器换版 ⇒ 拒装,不装"当前版");notify-only 通知带**真能装的那个版本** + 零副作用。
66
-
67
- ## M5 — platform 适配器 + managed 档(出口:`examples/hosted-service` 绿)
68
-
69
- | 测什么 | 怎么算过 | must-red |
70
- |---|---|---|
71
- | mac/linux/windows 适配器 | 各平台读回面 allowlist 注册齐 + CI 矩阵跑(linux 真跑;mac/win 至少接口级+Testbed 真机轮) | 未注册面被引用 ⇒ 拒 |
72
- | managed 端到端 | hosted-service demo:带活"会话"的完整升级→会话保留断言→回滚路径同样保留 | 升级后会话丢失/回滚后会话丢失 ⇒ 红 |
73
- | ownership 迁移场景 | **DEFERRED(v0 只假设官方 installer 安装,不做 deb/RPM 接管)**——PM 装的副本走 ownership 检测 → `held: managed-elsewhere` 即为正确终态(有齿,M3);接管(adopt)留给将来需要时再立项 | —(deferred) |
74
-
75
- 已落地齿以 `k-harness --list` 为准——本文件**不手抄齿名**(手抄清单是 `--list` 的副本,只会漂向'少列一颗';ratchet 7 只能查'写下来的存在',查不了'该写的没写')。逐颗齿(层/档/must-red/定义位置)直接 `k-harness --list`。 本层判据形状:**面在 allowlist 才可作证**(未注册面被引用 ⇒ typed UNREGISTERED_SURFACE 拒);读回新工件路径才 promote(读回旧路径仍 promote ⇒ 红);**禁投影**——版本串/元数据永远不能绿收敛谓词;**退役序**——未过收敛前 `retireLegacyManager()` 是 typed HOLD(无条件退役 ⇒ 红)。`ConvergenceReport.hostLifecycleConverged` 为 `PredicateResult | null`——**未声明面 = null = 从未被观测 = 不等于通过**(沉默不能当证据花)。
76
-
77
- ## M6 — L5 drive(可选层,最后)
24
+ ## Acceptance criteria
78
25
 
79
- | 测什么 | 怎么算过 | must-red |
26
+ | Area | Required behavior | Failure that must be detected |
80
27
  |---|---|---|
81
- | 远程命令过策略门 | drive stage/promote/rollback 全部经 L4 | drive 绕过 confirm 直接动 红(设备主人永远赢) |
82
- | 状态上报 | {stable, experiment, 两谓词, 策略} 与本地读回一致 | 上报值可与本地不一致 |
83
- | provenance journal | forward-only reconcile 来路;**"已记录"与 NOT_OBSERVED 机制上不可合并** | 存量机被计入"已记录" |
84
-
85
- **已落地齿**(三包全合,main 296 绿 / 56 齿):以 `k-harness --list` 为准,本文件不手抄(手抄是 `--list` 的副本,只会漂向少列一颗)。本层判据形状:
86
- - provenance:journal **三态** genesis/observed/unreadable(只有 ENOENT genesis;unreadable append 拒——截断视图绝不能重发 seq);记录 {who, carrier, when, version} **写前**(回滚的 reconcile 也留痕,证明写前);聚合把 genesis NOT_OBSERVED 机械分离("没数据"≠"没记录")。
87
- - status:机器自报是**读回不是发明**;谓词带**版本戳 join key**(真结论贴错版本比造假更难看出);跨重启持久化("观测过、只是我重启了"≠"从没观测过");读不了 从没有(第三态)。
88
- - drive + 政策门:服务器下发的命令和本地升级走**同一套门**;**ownership 门画在动作性质上**——settle 在飞事务永远允许(在飞 + ownership 翻转必须收敛,不许 held——held 在开了头的机器上是砖),只有"休息态 + managed-elsewhere"的新改装才 typed held(三个终态 idle/promoted/rolled-back 都断言);已 promote 版本的 push-rollback confirm 下必须 HOLD(安全方向是字节安全不是权威);K 自己的 in-transaction 自动回滚**绝不问同意**(配对互相控制)。
89
-
90
- ## 跨里程碑(一直在跑)
91
-
92
- - **跨版本矩阵**:`STATE_FORMAT_VERSION` 升档后旧 core 读新状态 ⇒ fail-closed 拒 + 指引;新 core 收养旧布局 ⇒ 无损;混合窗口显式建模。
93
- - **mutation-runner**(Lincan 工具就绪即接):对本计划全部齿跑变异;换说法+整段删两变体默认;杀不掉先排除"没杀对"再删守卫。
94
- - **断言纪律标注**:每个测试文件头标 `@invariant` `@baseline(failure-condition: ...)`;CI 检查无标注的 implementation-locking 断言(OS-supervisor 退役设计 二分的机械化)。
95
- - **真机轮**(Testbed):每里程碑收口跑一轮真机抽样;个人真机仅 consent 后读回抽样。
96
- - **DST**(§1.45,已实现):`k-harness sim` PR/profile 门跑固定 smoke seeds;`DST Nightly` 从 workflow run id 起跑 50,000 个顺序 seed。每个 journal/slot/host/predicate effect 后跑共享 invariant;失败自动留下 seed + transcript hash + `k-harness sim --seed X --json` 重放命令,并进入失败语料库后转成枚举矩阵固定格。
97
- - **时间戳锚定断言**(借 Datadog):scenario receipt 的事件断言一律"标记之后发生了 X"(取 marker assert-since),防旧事件/上一场景残留冒充新证据。
98
-
99
- ## 完成定义
100
- 计划本身的验收 = **三个 example demo 全绿 + M0 harness 自验含对抗样例 + 崩溃矩阵零人工枚举**。任何"支持 X"的 README claim 若无对应绿齿,按"没绿 demo 就没 claim"规则视为未支持。
28
+ | Runner acquisition | Verify runner size/hash before execution; preserve exit result and clean scratch code | Corrupt runner executes or worker failure becomes success |
29
+ | Request boundary | Validate wire version, fields, id, target and consent before loading the adapter | Invalid input reaches trusted product code |
30
+ | Ownership and policy | New upgrades respect installation owner, approved target and compatibility | Another manager's install is changed or the selected version changes after consent |
31
+ | Product acquisition | Verify exact target bytes; bound stalled transfers and validate resumed content | Corrupt, truncated or mismatched bytes reach staging |
32
+ | Transaction | Journal intent before effects; serialize operations; retain a recoverable stable slot | Conflicting writers, illegal transitions or loss of the fallback |
33
+ | Service transition | Stop old instance, start candidate, check live identity/version and declared lifecycle surfaces | Cached evidence, stale instance or wrong target authorizes promotion |
34
+ | Failure recovery | Roll back before durable promote intent; replay commit after it | A live candidate alone is treated as a committed transaction |
35
+ | Retry and receipts | Same id/target replays the recorded result; different target is rejected | Retry repeats lifecycle effects or rewrites an earlier result |
36
+ | Controller boundary | Enforce call budgets and validate response shape | Hung/failed command or malformed probe reports success |
37
+ | Workloads and data | Test the product's promised quiesce/resume and migration behavior | Rollback restores bytes but loses promised workload state |
38
+ | Observation | Preserve unreadable/absent/observed distinctions; report actual outcome | Missing data becomes success, or historical success is presented as current health |
39
+
40
+ The real-runner suite builds a runner and controls a separate service with no K
41
+ import. It covers successful upgrade, wrong-version rollback, concurrent-worker
42
+ refusal, worker death between stop and start, offline recovery and receipt replay.
43
+ Generated crash cases and seeded simulation cover mechanism-level interleavings.
44
+ Neither establishes exhaustive OS failure or physical power-cut coverage.
45
+
46
+ ## Transaction completion release gate
47
+
48
+ The Linux process tests in `core/src/launcher/supervise.test.ts` exercise these
49
+ cases with a built worker and live service. `core/src/txn/lock.test.ts` races eight
50
+ real processes against a stale lock; engine tests cover every recovery call budget.
51
+ Whole-invocation loss is simulated by killing supervisor and worker, not by a
52
+ physical power cut. Product OS startup triggers remain separate acceptance.
53
+
54
+ | Scenario | Required result |
55
+ |---|---|
56
+ | Worker exits without a durable outcome | Supervisor invokes recovery and returns the settled result, not launch success |
57
+ | Upgrade or recovery call hangs | Bounded execution; fence outstanding effects before any replacement worker |
58
+ | Worker dies but a controller action survives | No takeover until the remaining writer is stopped or safely fenced |
59
+ | Another installer starts during recovery | One state writer; original operation stays bound across retries |
60
+ | A newer operation completes before the old supervisor resumes | Old supervisor reads/replays its own result and never recovers or mutates the newer operation |
61
+ | Recovery repeatedly fails | Finite attempts and elapsed time; explicit unresolved result, retained state and executable recovery path |
62
+ | Installer starts with unfinished work | Settle it before accepting new work; do not silently retry the failed target |
63
+ | Crash during terminal reporting or cleanup | Recorded result is replayable; cleanup cannot erase required recovery state |
64
+ | Whole machine or supervisor stops | Next installer invocation restores consistency; product OS startup trigger tested separately |
65
+
66
+ Use real worker/controller processes for timeout and takeover cases, including
67
+ late effects and competing invocations. Keep generated journal-fault tests for
68
+ transaction ordering. Run both successful settlement and deliberately failed
69
+ recovery so an implementation that always returns success cannot pass.
70
+
71
+ ## Maintaining test quality
72
+
73
+ - Pair success cases with failures that exercise the intended boundary.
74
+ - Keep check registrations and their known-green/known-red tests aligned.
75
+ - Test process identity and termination using real processes where those are the claim.
76
+ - Keep fixtures isolated and clean up servers, child processes and temporary state.
77
+ - Use `pnpm ratchet` to check prohibited core shortcuts, assertion declarations,
78
+ registered-check coverage and cited source paths.
79
+
80
+ For larger deterministic runs, use
81
+ `node harness/src/cli.ts sim --start-seed 1 --seeds 50000`.
82
+ The [nightly workflow](../.github/workflows/dst-nightly.yml) runs an expanded seed
83
+ set and retains failures in `.k-harness/sim-failures.json`. Replay the reported
84
+ seed before turning a discovered failure into a fixed regression case.
85
+
86
+ ## Product and platform acceptance
87
+
88
+ The [CI workflow](../.github/workflows/ci.yml) gates Linux and macOS checks;
89
+ Windows is informational while the harness port remains incomplete. A green
90
+ framework suite does not certify a product's platform support.
91
+
92
+ Before shipping an installer, test its actual packaging, installation ownership,
93
+ first-install baseline, running-service upgrade, bad-candidate rollback, recovery
94
+ while offline, workload/data preservation, and survival outside the application's
95
+ service unit. Verify cloud reconnection separately if the product promises it.
96
+ K does not supply a universal fresh-install procedure, a signed distribution
97
+ service, reversible data migrations or automatic recovery after a machine reboot.
@@ -9,7 +9,7 @@
9
9
  import assert from "node:assert/strict";
10
10
  import { type ToothContext } from "../teeth/registry.ts";
11
11
  import { serveRelease } from "../artifact/m1.ts";
12
- import { PLAIN_DAEMON_SOURCE } from "../../../examples/service-daemon/source.ts";
12
+ import { PLAIN_DAEMON_SOURCE } from "../fixtures/serviceSource.ts";
13
13
  import {
14
14
  makeUpgrader,
15
15
  releaseSourceFor,
@@ -6,7 +6,7 @@
6
6
  * HostAdapter responsibilities + the app-declared lifecycle surfaces — no
7
7
  * test backdoors (transparency §1.8).
8
8
  *
9
- * These drive createUpgrader in-process (the library plane) against the
9
+ * These drive createRunner in-process (the library plane) against the
10
10
  * adapter's host + a real fake-server release.
11
11
  *
12
12
  * The release is the ADOPTER'S app when they supply `releaseSource`, and the
@@ -21,7 +21,7 @@
21
21
  import assert from "node:assert/strict";
22
22
  import * as path from "node:path";
23
23
  import { type ToothContext } from "../teeth/registry.ts";
24
- import { createUpgrader } from "../../../core/src/createUpgrader.ts";
24
+ import { createRunner } from "../../../core/src/createRunner.ts";
25
25
  import { staticManifestSource } from "../../../core/src/artifact/staticManifestSource.ts";
26
26
  import type { HostAdapter } from "../../../core/src/lifecycle/hostAdapter.ts";
27
27
  import type { HostDriver } from "../fake-host/inproc.ts";
@@ -29,7 +29,7 @@ import type { ReadbackSurface } from "../../../core/src/converge/predicates.ts";
29
29
  import { slotArtifactPath } from "../../../core/src/txn/fileEffects.ts";
30
30
  import { processAlive } from "../fake-host/daemon.ts";
31
31
  import { serveRelease } from "../artifact/m1.ts";
32
- import { PLAIN_DAEMON_SOURCE } from "../../../examples/service-daemon/source.ts";
32
+ import { PLAIN_DAEMON_SOURCE } from "../fixtures/serviceSource.ts";
33
33
 
34
34
  /** The adopter module's factory contract for the service tier. */
35
35
  export type ServiceAdapterFactory = (stateDir: string) => HostDriver & {
@@ -59,7 +59,7 @@ export function releaseSourceFor(adapter: { releaseSource?: () => string }): str
59
59
  }
60
60
 
61
61
  export function makeUpgrader(ctx: ToothContext, adapter: HostAdapter, baseUrl: string, surfaces?: ReadbackSurface[]) {
62
- const opts: import("../../../core/src/createUpgrader.ts").CreateUpgraderOptions = {
62
+ const opts: import("../../../core/src/createRunner.ts").RunnerOptions = {
63
63
  host: adapter,
64
64
  source: staticManifestSource({ baseUrl }),
65
65
  policy: "auto",
@@ -67,7 +67,7 @@ export function makeUpgrader(ctx: ToothContext, adapter: HostAdapter, baseUrl: s
67
67
  stateDir: stateDir(ctx),
68
68
  };
69
69
  if (surfaces !== undefined) opts.lifecycleSurfaces = surfaces;
70
- return createUpgrader(opts);
70
+ return createRunner(opts);
71
71
  }
72
72
 
73
73
  /** The running successor's evidence, read through the adapter's probe. */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * M1 exit checks — the swap-tool demo runs a REAL upgrade through core's
3
- * Upgrader (createUpgrader facade): end-to-end promotion with state
3
+ * Upgrader (createRunner facade): end-to-end promotion with state
4
4
  * assertions read via upgrader.state() (never internal files), and
5
5
  * automatic rollback of a version that fails to start.
6
6
  */
@@ -14,18 +14,18 @@ import { currentPlatformKey } from "../../../core/src/artifact/staticManifestSou
14
14
  import { FakeServer } from "../fake-server/server.ts";
15
15
  import { ArtifactFactory } from "../artifact-factory/factory.ts";
16
16
  import { runCommand } from "../artifact-factory/run.ts";
17
- import { CLI_TOOL_SOURCE } from "../../../examples/swap-tool/source.ts";
17
+ import { CLI_TOOL_SOURCE } from "../fixtures/cliToolSource.ts";
18
18
 
19
- /** The swap-tool demo's @botiverse/k-carrier wiring (createUpgrader module URL). */
20
- export function coreUpgraderUrl(): string {
21
- return pathToFileURL(path.join(import.meta.dirname, "../../../core/src/createUpgrader.ts")).href;
19
+ /** The swap-tool demo's @botiverse/k-carrier wiring (createRunner module URL). */
20
+ export function runnerFactoryUrl(): string {
21
+ return pathToFileURL(path.join(import.meta.dirname, "../../../core/src/createRunner.ts")).href;
22
22
  }
23
23
 
24
24
  export function swapToolEnv(ctx: ToothContext, baseUrl: string): Record<string, string> {
25
25
  return {
26
26
  K_RELEASE_BASE: baseUrl,
27
27
  K_STATE_DIR: path.join(ctx.sandboxDir, "state"),
28
- K_CORE_UPGRADER: coreUpgraderUrl(),
28
+ K_CORE_UPGRADER: runnerFactoryUrl(),
29
29
  };
30
30
  }
31
31
 
@@ -62,9 +62,9 @@ export async function buildSwapTool(ctx: ToothContext): Promise<string> {
62
62
  export async function readState(env: Record<string, string>): Promise<TxnState> {
63
63
  const coreSrcUrl = new URL(".", env.K_CORE_UPGRADER).href;
64
64
  const script = [
65
- `const { createUpgrader } = await import(${JSON.stringify(env.K_CORE_UPGRADER)});`,
65
+ `const { createRunner } = await import(${JSON.stringify(env.K_CORE_UPGRADER)});`,
66
66
  `const { staticManifestSource } = await import(${JSON.stringify(new URL("artifact/staticManifestSource.ts", coreSrcUrl).href)});`,
67
- `const u = createUpgrader({`,
67
+ `const u = createRunner({`,
68
68
  ` host: { quiesce: async () => {}, stop: async () => {}, start: async () => {}, healthProbe: async () => ({ version: "x", pid: 0, startId: "x" }), resume: async () => {} },`,
69
69
  ` source: staticManifestSource({ baseUrl: ${JSON.stringify("http://127.0.0.1:1")} }),`,
70
70
  ` policy: "auto", notificationSink: async () => {},`,
@@ -13,7 +13,7 @@ import { currentPlatformKey, parseManifest } from "../../../core/src/artifact/st
13
13
  import { downloadVerified, partialPathFor } from "../../../core/src/artifact/download.ts";
14
14
  import { FakeServer } from "../fake-server/server.ts";
15
15
  import { MANIFEST_FILE, sha256Hex } from "../fake-server/manifest.ts";
16
- import { coreUpgraderUrl } from "./m1.ts";
16
+ import { runnerFactoryUrl } from "./m1.ts";
17
17
 
18
18
  // ---------------------------------------------------------------------------
19
19
  // m1.download-resumes-after-kill (断点续传)
@@ -59,7 +59,7 @@ export async function checkDownloadResumesAfterKill(
59
59
  };
60
60
 
61
61
  const resumeDir = path.join(ctx.sandboxDir, "incoming");
62
- const coreUrl = coreUpgraderUrl();
62
+ const coreUrl = runnerFactoryUrl();
63
63
  const coreSrc = new URL(".", coreUrl).href;
64
64
  const dlScript = [
65
65
  `const { downloadVerified, partialPathFor } = await import(${JSON.stringify(new URL("artifact/download.ts", coreSrc).href)});`,