@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
@@ -4,6 +4,7 @@ import type { ConvergenceReport } from "./converge/predicates.js";
4
4
  import type { StatusReport } from "./status/report.js";
5
5
  import type { ReleaseSource } from "./artifact/source.js";
6
6
  import type { OperationDescriptor, OperationRead } from "./operation.js";
7
+ import type { QuarantineOptions, QuarantineResult } from "./quarantine.ts";
7
8
 
8
9
  /**
9
10
  * Who drove a reconcile, recorded in the provenance journal (M6, L5).
@@ -18,11 +19,9 @@ export interface ProvenanceIdentity {
18
19
  }
19
20
 
20
21
  /**
21
- * Upgrader is the single facade an application calls. Every entrypoint the
22
- * app exposes (daemon-internal auto-update, `myapp self upgrade`, install
23
- * script, remote drive) constructs the SAME Upgrader one canonical
24
- * executor, so there is no entrypoint that "swaps bytes but skips
25
- * convergence" (the class of bug this framework exists to kill).
22
+ * Upgrader is the transaction facade constructed by createRunner
23
+ * inside the disposable runner. Application entry points submit runner
24
+ * requests; they do not construct an in-process upgrade engine.
26
25
  */
27
26
  export interface Upgrader {
28
27
  /**
@@ -34,7 +33,7 @@ export interface Upgrader {
34
33
  * recorded by K. Hosts should run this from a coordinator that survives
35
34
  * service replacement, because recovery may stop and restart the service.
36
35
  */
37
- recover(): Promise<void>;
36
+ recover(expected?: { id: string; targetVersion: string }): Promise<void>;
38
37
 
39
38
  /**
40
39
  * Ask the release source whether this install should move, without moving
@@ -104,8 +103,8 @@ export interface Upgrader {
104
103
  /** K's single durable operation receipt; hosts project it, never mirror it. */
105
104
  operation(): Promise<OperationRead>;
106
105
 
107
- /** Mark one exact terminal operation delivered by the host transport. */
108
- acknowledgeOperation(operationId: string): Promise<"acknowledged" | "not-terminal" | "not-found" | "changed">;
106
+ /** Atomically move quiesced K state to an audit-only fresh-install backup. */
107
+ quarantineState(options: QuarantineOptions): Promise<QuarantineResult>;
109
108
  }
110
109
 
111
110
  export type UpgradeOutcome =
package/docs/design.md ADDED
@@ -0,0 +1,173 @@
1
+ # K design
2
+
3
+ This document is the normative contract for K's execution model, transaction,
4
+ supervision and controller boundary. It states obligations; it does not
5
+ explain them. Read [how an upgrade works](guide.md) first for the narrative
6
+ and vocabulary, and the [reference](reference.md) for wire formats, exit
7
+ codes, budgets and file layout.
8
+
9
+ ## Execution and trust
10
+
11
+ ```mermaid
12
+ flowchart LR
13
+ Entry[Bootstrap / CLI / remote control] --> Supervisor[Verify and launch runner]
14
+ Supervisor --> Worker[K + product adapter]
15
+ Worker --> State[Lock / journal / slots / receipts]
16
+ Worker --> Controller[Product lifecycle controller]
17
+ Controller --> App[Application]
18
+ ```
19
+
20
+ - The runner and its runtime live outside the application slots and service
21
+ unit. Stopping the application must leave the worker alive. A child
22
+ process can still belong to a systemd cgroup or Windows job; the publisher
23
+ must arrange isolation.
24
+ - The supervisor retains recovery code until settlement. Installation state
25
+ persists. An OS hook or operator must restart installation after reboot.
26
+ - The adapter is fixed at build time. Requests select an action and a target
27
+ version, never code, commands or download URLs.
28
+ - The publisher authenticates distribution metadata and caller authority. K
29
+ checks artifact SHA-256 and size; these checks do not establish publisher
30
+ identity. See [packaging](integration.md#distribute-a-built-installer).
31
+
32
+ ## Components
33
+
34
+ Paths are relative to `core/src/`.
35
+
36
+ | Component | Responsibility |
37
+ |---|---|
38
+ | `launcher/` | Acquire, verify, execute and clean runner code; supervise recovery |
39
+ | `protocol/` | Validate the bounded request/response contract |
40
+ | `runner/` | Serve stdin/stdout, execute requests and map outcomes to exit codes |
41
+ | `createRunner.ts` | Assemble source, host, policy and transaction state |
42
+ | `lifecycle/` | Stop/start/probe the application, including command-based control |
43
+ | `artifact/`, `txn/`, `converge/` | Verified acquisition, durable transactions and readiness predicates |
44
+
45
+ `createRunner(options)` constructs the transaction interface. `serveRunner(factory)`
46
+ serves it in the runner process. `launchRunner({release, request, scratchDir,
47
+ interpreter?})` verifies and supervises a worker, writes one final response and
48
+ returns its exit code. `superviseRunner` returns the same result as data,
49
+ including any retained `recoveryFile`. `resumeRunner(recoveryFile)` verifies
50
+ that retained runner and performs operation-bound recovery without
51
+ distribution access.
52
+
53
+ ## Transaction and recovery
54
+
55
+ Each installation has one lock, stable and experiment executable slots, and a
56
+ journal. K stages verified candidate bytes, quiesces work, stops the old
57
+ service, starts the candidate and evaluates live readiness. Passing candidates
58
+ are promoted; failed candidates restore stable. Application data belongs
59
+ outside the slots.
60
+
61
+ The phases are `idle`, `staged`, `handing-over`, `running-experiment`,
62
+ `readback`, `promoted` and `rolled-back`. Intent is journaled before effects.
63
+ Recovery takes the same lock and uses existing slot bytes without release
64
+ lookup:
65
+
66
+ - Before durable promote intent: restore stable.
67
+ - After durable promote intent: replay commit idempotently.
68
+
69
+ A running candidate alone does not authorize commit. Corrupt or unknown state,
70
+ and another live lock owner, prevent conflicting operations. Filesystem
71
+ durability and controller behavior determine the real platform guarantees.
72
+
73
+ ## Transaction completion
74
+
75
+ **Required for the initial release.** Every started operation has an owner
76
+ that waits for a durable terminal result or explicitly reports unresolved
77
+ recovery. An installer invocation must first settle unfinished work before
78
+ admitting a new upgrade. Recovering an interrupted operation does not retry
79
+ its requested upgrade.
80
+
81
+ The supervisor:
82
+
83
+ - runs outside the application service unit;
84
+ - retains the verified runner while the operation is active;
85
+ - enforces execution and recovery budgets ([values](reference.md#supervisor-budgets));
86
+ - starts a recovery worker after an abnormal exit;
87
+ - stops supervising after completion or an explicit unresolved result.
88
+
89
+ Recovery attempts and total elapsed time are bounded. Exhaustion preserves
90
+ state and provides a recovery command rather than reporting success.
91
+
92
+ Before starting a successor, the supervisor must observe the prior worker
93
+ exit. The recovery worker then takes the lock and fences outstanding
94
+ controller effects before replaying lifecycle actions. An expired deadline
95
+ alone is insufficient; an unconfirmed exit forbids takeover.
96
+
97
+ Recovery must bind to the original operation id under K's transaction lock.
98
+ If another operation has since run, recovery inspects or replays the original
99
+ result without modifying the newer operation. Recovery reuses the existing
100
+ journal and receipts; supervision does not create another transaction log.
101
+
102
+ Cleanup follows settlement: persist the outcome, release owned resources,
103
+ then remove disposable code. Never delete slots, a live owner's lock, or
104
+ recovery logs to make an interrupted operation appear complete. If recovery
105
+ remains unresolved, preserve the evidence and a verified means to invoke it
106
+ again. Installer startup handles leftover work; machine reboot still requires
107
+ an OS hook or operator to start the installer.
108
+
109
+ Every engine host call has a positive budget. Uncertain effects retain the
110
+ worker's lock until it exits. Bundled workers exit after flushing their
111
+ response; custom in-process callers must also exit on `HostCallUncertain`
112
+ rather than reuse that worker.
113
+
114
+ The lock is a local filesystem protocol requiring atomic creation and
115
+ coherent directory reads ([details](reference.md#lock-protocol)). It is not a
116
+ distributed or NFS lock. Operation ids, receipt archival and replay rules are
117
+ in the [reference](reference.md#receipts-and-retries).
118
+
119
+ ## Host control contract
120
+
121
+ `createRunner` requires a HostAdapter. Its operations and the controller's
122
+ obligations:
123
+
124
+ | Operation | Controller obligation |
125
+ |---|---|
126
+ | fence | Drain or fence previous controller effects; required when effects can outlive the worker |
127
+ | quiesce | Stop admission and durably park promised workloads; repeated calls safe |
128
+ | stop | Stop the specified slot's service and confirm termination |
129
+ | start | Start the selected artifact idempotently, without creating duplicate residents |
130
+ | healthProbe | Return version, pid and startId from one ready live instance |
131
+ | resume | Restore parked work on either candidate or rolled-back stable |
132
+
133
+ - The controller must work while the old application is down.
134
+ - `start` returning does not establish readiness; the probe does.
135
+ - K probes once before handover and records that incarnation's `startId`
136
+ with the handing-over intent. Readback evidence carrying the same
137
+ `startId` is refused and rolls back: the old service was not replaced.
138
+ - `start` must not execute the artifact in place inside the slot on
139
+ Windows; promotion renames slot directories and a running executable locks
140
+ its directory. Copy or hard-link to a runtime path outside the slots.
141
+ - A stateless service satisfies `quiesce` and `resume` by acknowledging. The
142
+ obligations apply to workloads the product promises to preserve.
143
+ - Adapters with no effects surviving their worker may omit `fence`. All
144
+ other adapters must supply it and test it against their real service
145
+ manager. `createCommandHost` always delivers `fence`; a command controller
146
+ that queues nothing acknowledges it.
147
+ - If OS lifecycle surfaces are declared, K also requires them to reference
148
+ the promoted artifact before retiring their previous manager. Undeclared
149
+ surfaces are not observed.
150
+
151
+ `createCommandHost` runs an external controller via argv without a shell,
152
+ records the controller pid durably before each call, and drains recorded
153
+ controllers before `fence` during recovery. Wire format, bounds and pid
154
+ handling are in the [reference](reference.md#command-controller-protocol).
155
+ The recorded pid is never used to kill an arbitrary process. `fence`
156
+ acknowledgement is a product contract, not something K infers from process
157
+ exit. Fire-and-forget stop cannot establish termination.
158
+
159
+ ## Product responsibilities
160
+
161
+ The adapter defines release lookup, installation ownership, consent,
162
+ notification and compatibility policy. Package-manager-owned installations
163
+ defer to their owner. K restores executables; products provide data-migration
164
+ compatibility, backup and restore, and any promised workload continuity.
165
+ Service upgrades can interrupt availability. Remote authorization,
166
+ distribution channels and cloud reconnection belong to the product
167
+ integration.
168
+
169
+ Use the [integration guide](integration.md) and
170
+ [service example](../examples/external-service/README.md) to build an
171
+ installer. The [test plan](test-plan.md) describes framework and product
172
+ acceptance; [prior art](prior-art/design-influences.md) records design
173
+ influences.
package/docs/guide.md ADDED
@@ -0,0 +1,196 @@
1
+ # How an upgrade works
2
+
3
+ This is the narrative walkthrough of K's mechanism: which processes exist,
4
+ what happens in what order, what breaks, and how to read the result. It is
5
+ written to be read once, top to bottom. It is **not** the contract; where it
6
+ and the [design](design.md) or [reference](reference.md) disagree, they win.
7
+
8
+ For what K is for and whether you need it, read the [README](../README.md).
9
+ For how to build and ship an installer, read the
10
+ [integration guide](integration.md). Terms are defined where they first
11
+ appear in bold; this document is the vocabulary the other documents use.
12
+
13
+ ## The pieces
14
+
15
+ ```mermaid
16
+ flowchart LR
17
+ B[Bootstrap<br/>install.sh / self upgrade] -->|request| S[Supervisor<br/>launchRunner]
18
+ S -->|verify, spawn| W[Worker<br/>K + adapter]
19
+ W --> D[(State directory<br/>lock · journal · slots · receipts)]
20
+ W -->|fence · quiesce · stop<br/>start · probe · resume| C[Controller]
21
+ C --> A[Application]
22
+ ```
23
+
24
+ - The **bootstrap** is your `install.sh`, or the `self upgrade` command inside
25
+ your application. It downloads and verifies the installer, launches it,
26
+ and relays the exit code. It contains no upgrade logic.
27
+ - The **installer** is what you ship. It has two halves. The **supervisor**
28
+ is a temporary process that acquires and verifies the runner, executes it
29
+ and, if it dies, runs bounded recovery. The **runner** is K plus your
30
+ adapter bundled into one executable; it serves one request and exits. A
31
+ running runner is a **worker**.
32
+ - The **adapter** is your trusted code, fixed at build time: where releases
33
+ come from, whether an upgrade may proceed, and how to reach the controller.
34
+ - The **controller** is your program that actually stops, starts and probes
35
+ the application. The adapter can call it directly or through `createCommandHost`.
36
+ - The **state directory** holds the lock, the journal, two **slots** and the
37
+ receipts. Recovery uses this state together with a compatible runner, its
38
+ runtime and the product controller; it does not need release distribution.
39
+
40
+ The runner and the controller live outside the application's service unit.
41
+ That is the whole point: stopping the application must not stop the thing
42
+ that is upgrading it.
43
+
44
+ The slots are **stable** and **experiment**; these are positions
45
+ on disk, not release channels, so a product channel also called "stable" is
46
+ unrelated. *Hands*, mentioned in the integration guide, is the release
47
+ platform K's authors publish with; K does not depend on it.
48
+
49
+ ## One upgrade, start to finish
50
+
51
+ ```mermaid
52
+ sequenceDiagram
53
+ participant B as Bootstrap
54
+ participant S as Supervisor
55
+ participant W as Worker
56
+ participant C as Controller
57
+ participant A as Application
58
+ B->>S: upgrade id=job-1 target=2.0.0
59
+ S->>S: download runner, check sha256 + size
60
+ S->>W: spawn, request on stdin
61
+ W->>W: take upgrade.lock, inspect operation identity
62
+ W->>C: fence previous controller effects
63
+ W->>W: settle leftovers, begin requested operation
64
+ W->>W: fetch release, verify bytes
65
+ Note over W: journal: staged intent
66
+ W->>W: write experiment slot
67
+ Note over W: journal: handing-over intent
68
+ W->>C: quiesce
69
+ C->>A: park work
70
+ W->>C: stop stable
71
+ C->>A: terminate, confirm exit
72
+ W->>C: start experiment
73
+ C->>A: launch candidate
74
+ W->>C: healthProbe
75
+ C-->>W: version 2.0.0, pid, startId
76
+ Note over W: journal: readback ok → promote intent
77
+ W->>W: experiment becomes stable
78
+ W->>C: resume
79
+ Note over W: persist promoted operation receipt
80
+ W-->>S: {result: promoted}, exit 0
81
+ S-->>B: exit 0
82
+ ```
83
+
84
+ In words:
85
+
86
+ 1. The bootstrap asks the supervisor for one operation: an id and a target
87
+ version. The id is what everything else binds to.
88
+ 2. The supervisor downloads the runner, checks its hash and size, writes it
89
+ to scratch space and runs it with the request on stdin.
90
+ 3. The worker takes the installation lock. If an earlier operation was left
91
+ unfinished, it settles that first. The same id replays its result; a new
92
+ id may proceed only after recovery succeeds.
93
+ 4. It asks your release source for exactly the target version, verifies the
94
+ bytes, and writes them into the *experiment* slot. Nothing running has
95
+ changed yet.
96
+ 5. It asks the controller to quiesce (park in-flight work) and to stop the
97
+ current service, and waits for confirmation that the old process is gone.
98
+ 6. It asks the controller to start the candidate from the experiment slot,
99
+ then probes it. The probe must come from one live process and report the
100
+ expected version with a pid and a **startId**. K probed once before
101
+ stopping the old service and journaled that startId; if the same one
102
+ comes back now, the old process was never replaced and the upgrade rolls
103
+ back.
104
+ 7. Only now does the worker write **promote intent** to the journal. That
105
+ line is the point of no return: before it, the safe move is always to put
106
+ the old version back; after it, the safe move is always to finish the
107
+ promotion.
108
+ 8. Experiment becomes stable. The controller resumes parked work. The
109
+ receipt is persisted in `operation.json`; it is archived before a later
110
+ operation begins. The worker prints its response and exits 0.
111
+
112
+ The transaction journal phases are, in order: `idle`,
113
+ `staged`, `handing-over`, `running-experiment`, `readback`, and then either
114
+ `promoted` or `rolled-back`. Operation receipts additionally track stages such as
115
+ `downloading` and policy outcomes such as `held`.
116
+
117
+ ## When something goes wrong
118
+
119
+ ### The candidate is bad
120
+
121
+ It does not start, it reports the wrong version, or it never passes the probe.
122
+ The worker stops it, starts stable again, resumes parked work, and records
123
+ `rolled-back`. Exit code 1. The stable bytes were never touched. This is the
124
+ routine failure and the one K is built around.
125
+
126
+ ### The worker dies
127
+
128
+ A crash, a kill, or a budget timeout. The supervisor:
129
+
130
+ 1. terminates a timed-out worker and waits for its observed exit; a deadline
131
+ alone does not permit takeover;
132
+ 2. starts a recovery worker bound to the same operation id;
133
+ 3. that worker takes the transaction lock, checks the operation identity,
134
+ and asks the controller to **fence** before replaying lifecycle effects.
135
+ Fencing ensures actions queued by the earlier worker cannot land later.
136
+ An already terminal original operation simply replays its receipt.
137
+
138
+ Recovery does not need the network and does not guess. It reads the journal:
139
+
140
+ ```mermaid
141
+ flowchart LR
142
+ idle --> staged --> ho[handing-over] --> re[running-experiment] --> rb[readback]
143
+ rb -->|promote intent| promoted
144
+ rb -.-> rolledback[rolled-back]
145
+ subgraph before [before promote intent: restore stable]
146
+ staged
147
+ ho
148
+ re
149
+ rb
150
+ end
151
+ subgraph after [after promote intent: replay commit]
152
+ promoted
153
+ end
154
+ ```
155
+
156
+ Before promote intent, recovery restores stable. After it, recovery replays
157
+ the commit. Both are idempotent, so a crash *during* recovery is handled the
158
+ same way next time. Recovery never starts a new upgrade, even if the original
159
+ request was an upgrade.
160
+
161
+ Attempts and elapsed time are bounded. If recovery does not settle within
162
+ those bounds, the supervisor exits 3, leaves the verified runner and a
163
+ `recovery.json` in scratch space, and prints where. Later, `resumeRunner` on
164
+ that file retries the same recovery offline.
165
+
166
+ ### Everything dies
167
+
168
+ Power loss, reboot, or the whole invocation killed. The state directory
169
+ survives. The next installer invocation, whether from `install.sh`, from
170
+ `self upgrade`, or from an operator, settles the unfinished operation before
171
+ it accepts new work. K does not install a permanent watchdog, so *something*
172
+ has to run the installer again: your product's OS startup hook, or a person.
173
+
174
+ ## Reading the result
175
+
176
+ Every run prints one JSON response on stdout and returns an exit code.
177
+ Inspect both. The short version:
178
+
179
+ | Exit | Meaning | What to do |
180
+ |---|---|---|
181
+ | 0 | Successful upgrade outcome; readable status or recovery with no recorded outcome | Check the action and receipt |
182
+ | 1 | Rolled back, or failed before any change | Read `operation.operation.outcome` and the reason |
183
+ | 2 | Held by policy, ownership or compatibility | Nothing changed; a new attempt needs a new id |
184
+ | 3 | Unresolved | Keep the recovery file and run recovery |
185
+
186
+ Two things surprise people. First, a `recover` action that successfully
187
+ restored stable after a bad candidate exits **1**, because the recorded
188
+ outcome is a rolled-back upgrade. That is the honest answer. Second, `status`
189
+ reads the last receipt, not the live service; it can say `genesis` (no
190
+ operation ever recorded) on a machine that is running fine.
191
+
192
+ The exact response shape and every exit code are in the
193
+ [reference](reference.md#protocol-v1).
194
+
195
+ What each step asks of your controller, and how to package and ship the
196
+ result, is the [integration guide](integration.md).