Repository navigation
designs: propose agent v2 with a pluggable node runtime - #1024
Philip Lombardi (plombardi89) wants to merge 29 commits into
Conversation
Adds a design for an experimental v2 of the unbounded agent, shipped as `unbounded-agent v2` in the existing binary. v2 defines a NodeRuntime interface with two implementations, host and nspawn-container, and an extension model (configuration, declarative extensions, exec plugins) so downstream consumers such as AKS Flex Node can use the agent binary instead of composing their own agent from pkg/agent. The document covers the external contract, internal interfaces, lifecycles with diagrams, explicit cleanup for the host runtime, reversible host preparation, parity with v1, an export policy, and the decisions and open questions for review. No code changes.
…e|after> Lifecycle hook events now follow one lowercase, dot-separated form that matches the rest of the plugin protocol, for example node.start.after and hook.node.start.after. Every state has both a before and an after event, which adds eight events to complete the pairs. Also defines when events fire (after events carry the step's result, hooks must be idempotent), the default failure policies, the order of events in each lifecycle, and whole-segment wildcard subscriptions such as node.*.after. The sequence diagrams and decision log are updated to match.
Renames the agent v2 host runtime cleanup levels from Quiesce, Release, Wipe and Remove to Terminate, Unmount, Wipe and Uninstall. The new names say what each level does, Terminate matches the machinectl term v1 already uses, and Uninstall no longer collides with the ordinary use of "remove" throughout the document. Records the change in the decision log.
Renames terms that clashed with other meanings or needed explaining: - the host runtime's slot "activation" becomes a slot change with an active-slot record, leaving "activation" to the v1 binary activation - the host preparation "journal" becomes an undo log - install "admission" becomes the install check - "orchestrator" becomes the node manager throughout - Goal.Disposition becomes a boolean delete - footprint, claims, host exposures, baseline and anchors become owned resources, the ownership record, shared host paths, preexisting mounts and original sysctl values, and systemd targets - the mutation lock is called the install lock Also rewrites smaller jargon (level-triggered, fail closed, seam, funnel, netns handles, roll forward), notes that the node bundle is unrelated to v1's offline artifact bundles, and adds a terms section (1.4).
Philip Lombardi (plombardi89)
left a comment
There was a problem hiding this comment.
AI-assisted review (Claude via OpenCode). These findings were checked against the code where cited, but please verify each one before acting on it.
Line references like L123 point to designs/agent-v2.md at da8073e4f. Code citations point to the doc's baseline (dca6c964d).
Overall: The goals are clear, the v1 research is thorough, and most of the citations are accurate. The main problems are:
- several state-machine gaps that would cause real bugs, such as a bootstrap token change triggering an unwanted wipe;
- a hook and plugin surface that is larger than any stated consumer needs;
- internal inconsistencies between sections 3, 5.6, 10 and 17.
The doc could also be much shorter. Section 10 alone is about 460 lines of implementation detail.
1. Gaps to resolve before implementation
-
Re-rendering the bundle can trigger an unwanted wipe.
- The bundle is not saved; it is rendered again when needed, including by the boot hooks (L826-827, L725).
Renderneeds aNodeSpec, andResolvetakes bootstrap credentials and a host probe (L791). The bootstrap credential may come from a plugin "called before every provision" (L586).- A new token changes the bundle's content and therefore its digest. The
hostruntime treats a changed digest atStartas a slot change and wipes node state (L1505-1508). "Same digest is a no-op" (L978) also stops holding duringRecover. - v1's pre-start hook needs only the applied config, not credentials (
pkg/agent/nspawnlifecycle/lifecycle.go:67-93). - Suggestion: save the bundle, or a secret-free manifest of it, in the slot at
Provision. Keep secrets out of the digest. HaveHooktake only the device and NVIDIA inputs it needs.
-
Two records of the active slot, with no rule for when they disagree.
state.Applied.ActiveSlot(L949) and the host runtime'sactive-slot.json(L1503) both track it.ErrSlotActive(L753) means the runtime also decides what is "active".- A crash after
Start Bwrites the runtime's record but before the node manager commits (L1125) leaves the two out of step.Recover(L1133-1137) only looks atPendingand A's phase. - The slot phases (L728) and the host runtime's active-slot phases (L1511-1521) are never mapped to each other, so what
Inspectreturns forhostis undefined.
-
Nothing defines which slot starts after a host reboot.
- Section 8.3 says systemd starts "the active slot's boot unit" (L1041), but the interface never says that
Startenables a slot or thatStopdisables it. - v1 enables the machine when it starts it (
pkg/agent/phases/nodestart/nspawn.go:99) and disables it on reset (pkg/agent/phases/reset/machine.go:39). - The doc should also say that lifecycle hooks do not fire when systemd restarts the node after a host reboot.
- Section 8.3 says systemd starts "the active slot's boot unit" (L1041), but the interface never says that
-
Repaves and stuck states can fail with no defined outcome.
- If B never becomes ready,
Recoverkeeps going back to "continue from Start B" (L1135). Nothing defines a backoff, a terminal state, a rollback, or what status is reported. - Nothing says how the node manager reacts when
Stop(A)fails withErrNodeBusyorErrMountsRemain. - In the
Decidetable, the row for a Node never observed is "Wait" (L1089). A node whose kubelet never registers can never be repaved, because there is no Node object to delete.
- If B never becomes ready,
-
The
Decidetable is missing the case it was designed for.- Level-triggered observation is meant to catch a Node deleted while the daemon was down (L1096-1099).
- If kubelet or someone else recreates the Node in that window, the daemon sees it as "Present" with a different UID. No row covers that.
- "Mark applied" for changes that do not cause drift (L1091, L1094) does not say whether those changes are applied to the running node or wait for the next repave.
-
The doc does not say which fields a goal may change.
goal.getreturns a fullAgentConfiguration(L536), which includesruntime.type,plugins,hooks,control,credentialsandextensions.- It is undefined whether a goal can change the runtime (see
Applied.Runtime, L948) or remove an extension. If an extension is removed, its host units and files would stay until a reset (L501). - Machina goals carry v1 config, and machina API changes are a non-goal (L160). That v1 config converts with the default runtime,
nspawn-container(L347). So it is unclear how a machina-managed host would ever choosehost. - Bootstrap calls
goal.registerbut notgoal.get(L1012), so the goal version recorded at bootstrap is undefined.
-
The new pre target conflicts with how the nftables flush is re-run.
- In v1 the flush is pulled in directly by the machine unit (
nftables-flush.service:21,RequiredBy=systemd-nspawn@.service). The pre-start hook is also a separate one-shot unit pulled in withRequires=(service-override.conf:37-38). - v2 puts
unbounded-node-pre.targetin between (L508), and itself says that starting an active target does not run its members again (L1584-1585). Only thehostruntime is said to restart those units explicitly. - NodeReboot on
nspawn-containerissystemctl restart systemd-nspawn@<m>(L1166). Section 10.9 says both that the flush re-runs on each start (L1586-1588) and that starting an active target does not re-run its members (L1584-1585). With the target in between, both cannot hold fornspawn-container. This should become a validation task. - Section 8.3 says the pre-start hook is an
ExecStartPre(L1042), but v1 runs it as a separate one-shot unit (config-regeneration.service:11). The doc should pick one.
- In v1 the flush is pulled in directly by the machine unit (
2. Internal contradictions
| Where | Contradiction |
|---|---|
| L311-320 vs L359-360 | The example config sets both runtime.nspawnContainer and runtime.host, which validation rejects. |
| D6 (L173) vs 5.6 (L571-573), 9.1 | v2 is said to share only the binary layout. It also shares nftables-flush.service, kubelet.service, containerd.service, localdns.service and /var/lib/machines/kube{1,2}. The doc covers v1 detecting v2 (L661) but not v2 detecting v1. On nspawn-container, a v1 host looks the same as a v2 host. |
G4 (L151), the pkg/agent non-goal (L162), §14.5 (L1822) vs L661-666 |
"v1 unchanged" and "pkg/agent frozen", yet pkg/agent/phases/host/preflight_existing_deployment.go changes, and the change reaches Flex. The new NVIDIA Container Toolkit artifact also changes hack/cmd/agent-artifacts-builder. |
| 5.3 (L382) vs L350, L539 | Plugins are said to provide "daemon credentials", but credentials.daemon.type has no plugin value. No config field selects the credentials.cluster method. |
| L366-368 vs L587 | The runtime is said to make the kubelet exec command visible inside the node, yet the Flex mapping also needs a sharedHostPaths entry. Section 9 never covers it. |
| L1478-1480 | Preflight requires cgroup.kill, but a fallback for its absence is also described. The fallback can never run. |
| L1305 vs L1575 | "Delete only what is provably owned" vs deleting iptables chains by KUBE-* and CNI-* prefix. |
| L1338 vs L1757 | Original sysctls are recorded in two places: the host runtime's ownership record and the host preparation undo log (which applies to both runtimes). |
| L1374-1376 vs the table at L1380-1400 | Four cleanup classes are defined, but the table also uses process, mount, network namespace, staged and as declared. |
| L1386 vs L1529-1534 | /var/run/cdi/nvidia.yaml goes through the /var/run symlink, which the safe remover refuses (inference: /var/run links to /run on systemd distros). Use /run/cdi/. The path is also classed volatile, but the Wipe step for persistent paths lists it. |
| Rule 4 (L764) vs L1505-1508 | The rule is "the first Start after Provision is fresh", but host wipes only when the slot or digest changes. Re-provisioning with the same digest does not wipe. |
| L767 vs L1424 | "Restart keeps all state", yet the host restart clears volatile paths. |
| L1742 | Shared hostnet returns []host.Leftover, so a package used by both runtimes depends on the host runtime. |
| L1632 vs L540 | Leftovers are said to go to the status sink, but status.report has no field for them. |
| L534 vs L410-412 | A plugin that fails describe fails startup, yet "a broken hook can never block stopping or cleanup". It is unclear whether v2 reset calls describe. |
| L1893 vs L261 | One says the help text shows "(experimental)", the other "Experimental v2 agent (unstable)". |
| L52-53 | v1's agent-upgrade is listed as visible, but it is hidden (cmd/agent/internal/cmd/agentupgrade.go:70). |
3. Underspecified areas
- Hooks:
- With wildcard subscriptions, it is unclear whether
describemust list every concretehook.<event>method. - The example config subscribes
akstonode.start.aftertwice, once directly and once throughnode.*.after, with different failure policies (L335-338). Is it called twice? - When a wildcard entry has no failure policy, does each concrete event use its own default?
- On a resumed bootstrap, a running A skips its steps and their events (L1017). A failed
node.start.afterhook with policyFailis then never retried. - During a repave,
node.start.afterfires after the commit (L1125-1126), so aFailfailure has nothing to fail.
- With wildcard subscriptions, it is unclear whether
- Plugins:
- Timeouts are "configurable" (L545), but the plugin schema has no timeout field.
- Response-size limits are not given.
- The path checks do not say whether parent directories are checked.
- The
status.reportphase values are not listed, and "versions" ingoal.registeris not defined.
- Credentials: if both the config's
cluster.apiServer/caCertBase64and the bootstrap provider's response supply these values, it is not said which wins. - State files:
- Only
owned-paths.jsonhas aschemaVersion. The doc says the on-host layout may change without notice, and an upgrade can roll back tolast-good(L1191), so compatibility between binary versions is undefined. node-state.jsonstoresConfig, which can includecredentials.bootstrap.static.token, but its file mode is not stated.- What happens when a
.sha256check fails is not stated.
- Only
- Reset:
- When reset runs from the CLI, it is unclear when the running daemon is stopped (L1225).
- The diagram leaves out
/etc/unbounded/agent-v2/, and does not say whether the shared binary directory is removed.
- On-host list: Section 5.6 is presented as the complete footprint that replaces Flex's
uninstall.sh(L599). It leaves out:- the temporary containerd under
/run/unbounded-agent-v2/containerd-maint/; /etc/sysctl.d/99-kubernetes.conf;nftables-clear.nft, the apt drop-ins, the dockerdaemon.jsonedit, fstab edits and unit masks;- the LocalDNS slice and network unit;
- the CDI file;
- extension host files;
- the first-boot unit.
- the temporary containerd under
- Extensions:
- No rule stops an extension's node or host files from colliding with built-in paths such as
/etc/containerd/config.toml. - Nothing covers an extension host file that already exists.
- It is unclear which unit
nodeDependencies.requiresattaches to onhost. sharedHostPathsentries wherehostPathandnodePathdiffer are undefined onhost.
- No rule stops an extension's node or host files from colliding with built-in paths such as
hostruntime pre-start hook: the doc does not say which unit runs it (L1237, L1042).Restartalso runs it directly (L1424).- Smaller items:
- What "from the host" means for
v2 agent-upgrade, and whether upgrade hooks fire when it is run from the CLI. - Where
components.*andartifacts.downloadsoverlap. - How v1 config fields map to v2 when converting.
- Whether
adoptPreserveMaxBytesapplies per path or in total. adoptExistingPaths[].modereuses the name of the filemodefield. Something likepolicywould be clearer.
- What "from the host" means for
4. Clarity and terminology
- Decision IDs:
- Section 3's D1-D6 and section 17's D1-D5 repeat each other.
- O1-O11 (no O5) and H-D1 to H-D6 are never defined.
- O6 is recorded as decided (L1901) and also listed as open (L1921).
- Event names: the middle part is called a "state", but the values (
install,start,repave) are actions, and theimage-importexample matches no event (L391).<subject>.<action>.<before|after>would be clearer. - "A" and "B": the repave diagrams and lifecycle table (L443, L1112-1128) use A and B as roles, but they are also the literal slot names. After the first repave the active slot is
b. Use "old" and "new". - "Hook": the word means four things: plugin lifecycle hooks,
NodeRuntime.Hook,v2 runtime-hookandHookPhase. Renaming the runtime side to "boot hook" would separate them. - "Slot" for
host: the glossary says a slot "holds a node" (L133). Onhost, a slot only holds staged binaries and files; the node's state lives in shared paths. - G2 "parity": the intended differences from v1 are spread across sections 8.4, 8.5, 10.9, 11, 12 and 13. Collect them in one "Behavior changes from v1" table and reword G2 as "parity except where listed".
- Upstream behavior: some statements are not marked (inference), such as the target member re-run behavior, kubelet certificates living in
/var/lib/kubelet, andcgroup.killneeding Linux 5.14.
5. Simplification
Design:
- Defer lifecycle hooks or cut them down. None of the AKS Flex Node mappings in section 5.7 (L581-599) uses a lifecycle hook. The 16 events, wildcards, default failure policies and hook config have no identified consumer. If they stay, use one
hookmethod with the event in the payload. That also solves thedescribequestion above. - Defer Preserve adoption and
retainContainerImages. Preserve needs backups with xattrs, SELinux labels and a size cap.retainContainerImagesneeds a temporary containerd and prototype V2. The defaults, refusing existing content and wiping images, are already the simple behavior. - Use one record for the active slot. Either the runtime owns it and reports it through
Inspect, or the node manager does. DropNeverStarted, which repeatsPhase == Provisioned. - Keep the nftables flush as a direct dependency of the boot unit instead of routing it through the pre target.
- Let systemd do the stopping. Use
systemctl stop unboundednode.slicewith a stop timeout. systemd already sends SIGTERM and then SIGKILL across everything in the slice (inference). Keep thecgroup.killlogic as verification and fallback. - Unify credentials. Use one exec or plugin mechanism, with a clear table of which credential each type serves.
- Make
control.operationsa boolean, since its values are onlymachinaanddisabled. - Optionally split
NodeRuntime(14 methods) into a planning interface (HostRequirements,Preflight,OwnedResources,Layout) and a lifecycle interface.
Document:
- Move section 10 into a companion
designs/agent-v2-host-runtime.md, and keep the main doc to the external contract, interfaces and lifecycles. - Merge sections 3 and 17 into one decision table with a status column.
- Move the host network reset out of section 10, since it applies to both runtimes.
6. Citation accuracy
Of the 93 local citations, 83 are accurate. The other 10 point to the wrong lines or only partly support the claim; none point to the wrong code entirely.
| Doc line | Problem |
|---|---|
L58 coordinator.go:29-38 |
These lines are only the Stages interface. The lock is at :72, the run classification at :83-115, and the stage loop at :126-135. |
L62 phases.go:17-20 |
Serial is at :62 and Parallel at :87. |
L79 bootstrap.go:220 |
This line is about saving the applied config. The machinectl list call is at pkg/agent/phases/reset/machine.go:256. |
L83 lifecycle.go:67-129 |
This is the Go code. The systemd wiring is service-override.conf:37-38,48 plus config-regeneration.service. |
L365 config.go:615-623 |
authorization.mode is also reserved (:604-612). |
L1241 resolve.go:163-166 |
Applies only when LocalDNS is enabled. It is the resolvConf config field, not a --resolv-conf flag (kubelet.go:129-130). |
L1580 nftables-flush.service:20-21 |
This is RequiredBy=, not ordering. The ordering is service-override.conf:39. |
L1759 configure_docker.go:30-66 |
Masking docker is in disable_systemd_unit.go:30-33,59-75. |
L1391 bootstrap_helpers.go:263 |
The directory is created at :314-319. |
L1238 Containerfile:21-31 |
Off by one; the block is lines 22-32. |
Two more code-versus-doc disagreements could go into section 19:
api/machina/v1alpha3/machineoperation_types.go:51-53says "custom operations may be supported", but the enum on the next line is closed. This matters for open question 1.internal/provision/assets/unbounded-agent-uninstall.shreverses host preparation, butUnboundedAgentUninstallScript(internal/provision/script.go:35) is only called from tests. This is relevant to section 11's claim that v1 never reverses host preparation.
Restructure only, in response to review. No design content changes. - Move section 10 (host runtime cleanup) to designs/agent-v2-host-runtime.md, renumbered 1-14, and leave a short summary in the main doc. - Move the host network reset, which applies to both runtimes, into the main doc as section 11, together with the hostnet package sketch. - Merge the core decisions (section 3) and the decision log (section 17) into one decisions table with a status column, numbered D1-D32. Open questions from section 18 appear as open rows. - Renumber sections 11-16 to 12-17 and update cross-references.
- Cite the bootstrap coordinator's lock, classification and stage loop, and phases.Serial and phases.Parallel, not only the interfaces. - Distinguish the bootstrap.go call sites: the rootfs check is :161, the applied-config check is :220, and machinectl list is in reset/machine.go. - Cite the nspawn hook wiring (service-override.conf, the separate config-regeneration unit), not only the Go code. - Include authorization.mode in kubelet's reserved fields, describe resolvConf as a config field set only when LocalDNS is enabled, and fix the Containerfile, docker masking, nftables-flush ordering and CNI directory line numbers. - List v1's agent-upgrade command as hidden. - Add three code/doc disagreements: the closed OperationKind enum, the v1 uninstall script that only tests call, and v1's repave reusing a partially unpacked rootfs.
- Save the bundle manifest in the slot at Provision, with a digest that leaves out secret contents. Boot hooks and Recover read it and never render again, so a refreshed bootstrap token cannot trigger a wipe. - Make the runtime the only authority on which slot is enabled and running. Drop Applied.ActiveSlot and SlotState.NeverStarted, add SlotState.Enabled, and define how Recover reconciles the runtime's slot digests with the applied and pending goals. - Start enables a slot and Stop disables it, as v1 does with machinectl; no lifecycle hooks fire when systemd starts a node at boot. - Rename the runtime's hook to BootHook, BootPhase and v2 boot-hook. - Clarify rule 4 (identical content is not new content) and Restart (volatile state under /run is lost). - Define repave failures to match v1: indefinite retries with controller-runtime's default backoff, no rollback, no restart of the old node. - Add Decide rows for a recreated Node and for a node that never registers (reported as NodeNotRegistered after 10 minutes), and define what recording a goal version as applied means. - Split install-time settings from goal settings; goal.get returns goal settings only. Bootstrap records an empty goal version. - Explain that machina-provisioned hosts always get nspawn-container. - Use old and new instead of A and B in the repave lifecycle.
Review found that routing the nftables flush and other prerequisites through unbounded-node-pre.target would stop them from running again on a restart, because starting an active target does not start its members again. - Remove unbounded-node-pre.target. Prerequisite oneshots attach directly to each runtime's boot unit, as v1's flush does: systemd-nspawn@.service for nspawn-container (RequiredBy= plus After= in the machine override), and a new unbounded-node-prestart.service for host. - Run the pre-start boot hook as a separate oneshot unit, as v1 does, not as ExecStartPre. Post-start stays ExecStartPost on nspawn-container; host needs none. - Keep unbounded-node.target only as the host runtime's internal grouping of its node units, not as a consumer contract. Extensions say orderBeforeNode and the runtime writes the wiring. - Add validation task V5: prerequisites and the pre-start hook run again on every start of the boot unit.
- Use one plugin method, hook, with the event name in the payload, so describe lists it once regardless of wildcard subscriptions. - Name events <subject>.<action>.<before|after>; the middle segment is an action, not a state. - Reject two subscriptions from the same plugin that can match the same event, and fix the example config that did this. - A wildcard entry without a failure policy uses each event's default. - On a resumed bootstrap, fire the after event of each step that is already done again, with resumed: true, so a failed hook is retried before the install completes. - Fire node.start.after for the new slot before the repave commit, so a failing hook leaves the repave pending and it is retried. - Fire agent.upgrade.before from v2 agent-upgrade as well as from the daemon, and use old and new instead of A and B in the hook tables.
Contradictions: - Show one runtime block in the example config, with the host block separately. - D6: v2 shares some unit and machine names with v1, not only the binary layout. Add v2's detection of a v1 install. - List every change outside internal/agentv2 (the v2 command, the v1 detection check, the artifacts builder) and reword G4, the pkg/agent non-goal and the export policy to match. - credentials.daemon gains plugin, which selects credentials.cluster. Kubelet's exec stays, since kubelet runs it itself. The runtime makes the kubelet exec command visible in the node, so Flex needs no shared host path for it. - Drop the cgroup.kill fallback that preflight made unreachable, and let systemd stop the slice, with cgroup.kill as a backstop. - Scope "delete only what is provably owned" to files; network objects are deleted by node-only names or declarations. - Keep original sysctl values only in the host preparation undo log. - Replace the inconsistent class column with "cleared at". - Write the CDI spec to /run/cdi, not through the /var/run symlink. - Move Leftover to the shared runtime package, and report leftovers through status.report. - Reset never fails because of a plugin. Gaps: - Plugin timeouts, output limits, parent directory checks, status phases and goal.register fields. - Credential precedence, and components versus artifact sources. - State file versioning, checksums and modes. - The order of stopping the daemon on reset, and removal of the agent's configuration and binary directories. - A complete list of what v2 creates on the host. - Extension collisions, existing host files and shared host paths. - The v2 agent-upgrade path, control.operations.enabled, the adoption policy field, and a v1 configuration mapping (appendix A).
- Reword G2 as parity except for intended changes, and add section 14.1, one table of every intended behavior change from v1 with the section that explains it. - Fix the glossary's slot entry: on host a slot holds only staged content, and the node's state lives in shared paths. - Mark the remaining upstream claims as inference: containerd's CDI directories and kubelet's certificate directory. - Rename open question 5 to the boot hook binary and cite v1's helper.
The AgentUpgrade lifecycle said "inactive slot" for the blue or green agent binary, which reads like a node slot. Say blue or green binary.
Thanks for the thorough review. I verified each claim against the code before acting on it, and all but one checked out exactly (see citations below). Here is what changed, grouped like the review. 1. Gaps: all fixed
2. Contradictions: all fixed
3. Underspecified areas: all addressed
4. Clarity: all fixed
5. Simplification
6. Citations
Commits
Still open for reviewers, in section 18: MachineOperation kind scoping (D12), separate v2 state names (D6), the default runtime (D29), copying rendering templates (D30), the boot hook binary (D31), and observing the Node on every reconcile (D32). |
Remove sections 19 (code and documentation disagreements) and 20 (alternatives considered). Keep the two facts other sections rely on: the v1 partial-rootfs evidence moves inline into section 8.5, and the closed OperationKind enum moves into open question 1.
The decided rows restated what the body already says. Section 3 now holds the open questions, moved up from section 18 so reviewers see them first; section 18 is removed and sections 4-17 keep their numbers. - Drop decision IDs from the questions and the body. The corrected description of shared v1 names moves into question 2. - Keep the two points only the table made: why node content is a declarative bundle (now in section 4) and that there are no in-process Go plugins (now in section 5.3). - Point references to the old section 18 at section 3, and update the review guide.
Consistency: - A hook's timeout is set only by hooks[].timeout (default 60s); drop "hook" from plugins[].timeouts. - The plugin request context uses machineName and nodeName, matching goal.register. - Only the daemon CSR group is configurable; there is no name field. - Conservative host Teardown no longer lists host preparation units. - State the containerd shim point once in the host runtime doc. - Export decisions are recorded in a design doc, not appended here. - Drop the vague "documentation must say this" sentence. Cuts, where text repeated other sections: - The list of Flex additions in 1.3 (see 5.7), a duplicate "why a bundle" bullet, the logging implementation note in 5.1, the credential rows in the enumerated fields, the before/after naming rationale, the exec model paragraph, the bundle naming note in 7.2, the v1 comparisons in 7.7, 8.4 and 8.5 (their citations move to 14.1), the repeated flush wiring in 11, and the uninstall script note in 12. Wording: - "written durably" becomes "written and synced to disk". - "status sink" becomes "status reporter" (StatusReporter); the control.status field is unchanged.
- Install check: the fingerprint covers install-time settings only. On a complete install, v2 start accepts different goal settings and leaves them to drift handling. A resumed install fails only if the node already started from a different bundle. Listed in 14.1. - static goal source: the daemon watches its copy at /etc/unbounded/agent-v2/config.json. Goal-setting edits are a new goal; install-time edits are ignored with a warning. - Add control.goal.pollInterval for the plugin goal source, default 10m with up to 10% jitter. - A goal change during a repave replaces Pending if the new slot has not started; otherwise the repave commits and the new drift needs another Node deletion. - Status phases table with reasons. Ready means the Node object has been seen; until then the phase is Provisioning. Failed carries StateMismatch, StateCorrupt and RepaveFailed. UnsupportedOperation is set on the MachineOperation, not on the node's phase. - Readiness waits use v1's timeouts: 2 minutes for the kubeconfig at bootstrap, 60 seconds for kubelet after a repave or reboot, 1 minute for LocalDNS. - The LocalDNS network unit is node content: the node manager writes it from the bundle's HostUnits before every Start, as v1 does, and the host network reset removes it. It leaves the host preparation lists. - The host runtime refreshes observedHostPaths whenever Inspect runs while containerd is up; Observe calls Inspect on every reconcile, and Terminate uses the last list.
Restructure only, no design changes. The design was hard to review as one long document, so it moves to designs/agent-v2/: - README.md: purpose, goals, open questions, architecture, terms, and a table of the documents with who should read each one. It replaces the review guide. - 01-cli-and-configuration.md, 02-extensions.md, 03-node-runtime.md, 04-control-and-state.md, 05-lifecycles.md, 06-host-changes.md, 08-v1-compatibility.md and 09-implementation.md take the sections of the old main doc. - 07-host-runtime-cleanup.md is the former companion doc. Old section 10, which only pointed to it, is removed. Each document numbers its own sections. Cross-references became links to the target document and section, or plain text inside code blocks and diagrams.
Review set a rule: v2 should have the same outcomes as v1 unless there is a very good reason. This changes v2 to match v1 in five places: - Only a Node deletion the daemon sees as it happens starts a repave, as in v1. A Node deleted while the daemon is not running is ignored, so stopping the daemon is a safe escape hatch. Node UID tracking is removed and open question 6 is settled. - The goal version and the Machine's RepavePending condition are recorded only when the daemon handles a Node deletion, as in v1. - The nspawn pre-start boot hook finds host devices again at every machine start and rewrites the machine's host-side config, as v1 does. Device paths are not part of the bundle digest. - The install check uses v1's fingerprint (Kubernetes version, OCI image and API server) plus the install-time settings, and v1's resume behavior: a running node is kept, and the settings saved in its manifest are recorded as applied. - On host, kubelet gets a new boot ID at every node start, so a NodeReboot shows the same signals as on nspawn-container. Validation task V6 covers it. One new difference, for self-healing: Pending is saved to disk as soon as the daemon sees a deletion that needs a repave, so a daemon or host restart before the repave finishes no longer loses it. The behavior changes table in 08 now states, for each kept difference, the situation, the v1 outcome, the v2 outcome and the reason, and lists where v2 matches v1 on purpose.
Existing v1 users need an upgrade path, so in-place migration is no longer a non-goal. The new document, designs/agent-v2/10-migration-from-v1.md, specifies it: - v2 takes over the running nspawn machine as its slot and replaces only the agent around it, so pods keep running and the Node and Machine are unchanged. v1's daemon certificate is reused, so no bootstrap token is needed. - A migration starts from the host with v2 migrate, or from the cluster with an AgentUpgrade that has the parameter agentMode: v2, which a machineSelector can apply to a fleet. Older daemons ignore the parameter, so one operation works from any v1 release that supports AgentUpgrade. - It runs as a unit with a phased migration record, so a crash or reboot resumes it. Any failure before it commits rolls back to v1. Rollback stays possible until v2's first repave. - v1's applied config, credentials, Machine status and host preparation are converted. Host preparation changes v1 made without a backup are left in place and reported at reset, as a v1 reset would. - Machines built by other pkg/agent consumers, such as AKS Flex Node, can be taken over with --adopt-machine and a supplied configuration, after the v1 path. - Moving to the host runtime stays a reset and a new start. Also: goal G6, the README documents table, v2 migrate in the CLI, a migration entry in the lifecycles, the migration paths in the host changes list, the v1 daemon change in the list of changes outside internal/agentv2, and the migrate package.
The new document, designs/agent-v2/11-testing.md, takes over the testing section of 09 and adds: - The v1 end-to-end suites as v2's acceptance tests. The harness gains AGENT_PROFILE (v1, v2) and NODE_RUNTIME (nspawn-container, host); a profile object holds the commands, names, state reads and node access that differ. Every swapped check is listed against its v1 line, and v1 runs must stay identical, including the harness tests that pin suite contents. - Checking reset against a host snapshot taken before install. - A migration-v2 suite modeled on the existing host-root migration suite: no pod restarts, the same Node, v1's lifecycle steps afterwards, and variants for rollback, interruption in every phase, failure in every phase, refused checks, moving to the host runtime and immutable hosts. Interruptions are injected from outside, as the existing bootstrap-recovery test does, without failure points in the product. - End-to-end tests for what v1 never had, including a Node deleted while the daemon is stopped and a daemon restart after a seen deletion. - CI on pull requests and nightly, and validation tasks, with V7 for the host runtime on immutable hosts. To support the harness, v2 status --output json becomes a documented interface, and a hidden v2 convert-config command converts a v1 configuration. A migration risk is added to the risks.
All five open questions are settled: - MachineOperation scoping: custom kinds are not a feature; the agent owns NodeReboot, AgentUpgrade, AgentReset and HostOSReboot plus future Node* and Agent* kinds. The API comments are fixed in #1045. - Names: contract-level names (units, commands) are final and shared with v1: unbounded-agent-daemon.service, its recovery unit, and unbounded-agent-migrate.service. Internal state keeps -v2 names. v1 and v2 now detect each other by install records, and v1's daemon and reset refuse on a v2 host. - nspawn-container stays the default runtime, and templates are copied. - Boot hooks run from unbounded-agent-boot-hook, a copy of the binary written at every provision that a rollback does not revert, as v1 does. v1 is not supported after the first official v2 release, so the migration doc gains a release plan: a last v1 release (N) whose daemon can undo a migration, and a first official v2 release (N+1) whose daemon migrates a v1 host on its first start after an ordinary AgentUpgrade. Rollback goes back to N, upgrades must go through N, and the binary rollback slot always holds a binary that can undo the migration. Also: - The migration health check is v1's (stable for 3 seconds within 30), replacing the 5-minute wait. - --adopt-machine is required for converting AKS Flex Node hosts and is part of the first implementation. - Fleet migrations use one AgentUpgrade per Machine: a machineSelector operation reports one combined result (#1040). - HostOSReboot, added on main after the baseline, is handled as in v1. - v2 CI jobs do not block merges while v2 is experimental; the testing doc adds cutover and adopted-machine variants.
The design cited two commits: dca6c96 as its baseline, and e132dfa for HostOSReboot, which landed later. Move the baseline to e132dfa so every citation points at one commit, and drop the "at e132dfa" and "after this design's baseline" wording. Between the two commits, the HostReboot split moved code in the v1 daemon. Update the citations that moved: - nodeoperator.go: the interface, hasDrift, RestartNode, RepaveNode and the applied-config lookup - controller.go: the Node delete watch - controller_machineoperation.go: AgentUpgrade handling and the startup gate - daemon.go: Machine registration v1's daemon now handles four MachineOperation kinds through machineOperations.handlers(), and publishes HostOSReboot results at startup. Say so in the README, the reconcile diagram and the startup gate, cite metalman for not executing HostOSReboot, and list hostosreboot.go and the pkg/agent/daemon helpers it uses among the code v2 copies or imports. Citations to files unchanged between the two commits are unchanged.
Wording and consistency fixes, with no change to the design: - The host runtime's Teardown no longer lists the host network reset and the undo log replay, which the node manager runs after it. - The host runtime enables unbounded-node.target, as section 4 says, not the node units. - The state file mode rule covers state files and secrets, not slot binaries or the artifact cache, and the active-slot record has a schemaVersion like every other state file. - nodeDependencies.requires units are required by the boot unit, because the agent never edits host units it does not own. - The approved-repave state field is renamed from Pending to Repave, so it no longer shares a name with the Pending status phase. - Add the NodeChanged trigger, and list v2 reset among the commands that call describe. - A converted v1 configuration enables MachineOperations; the setting is a boolean now. - The LocalDNS template contract lists all six variables, including NodeUpstreamIPs. - The machina goal source says exactly what v1 reads: the applied configuration with the referenced version's template overlaid. - The glossary no longer says a goal version changes whenever the goal does, which is not true for machina (#1038), and defines the terms used across documents. - Remove leftover Signal and Sysctl operations from the cleaner's system boundary, call v1's nspawn hooks boot hooks, and fix stale section references and the CLI list in the architecture diagram.
Philip Lombardi (plombardi89)
left a comment
There was a problem hiding this comment.
AI-assisted review (Claude via OpenCode). Findings were checked against the code where cited, but please verify each one before acting on it.
This review covers everything since my first review. Line references like 03-node-runtime.md:247 are to designs/agent-v2/ at 4160c05ed. Code citations are at the design's new baseline, e132dfada.
Pushed to the branch:
-
56729bd68moves the baseline fromdca6c964dtoe132dfada, so every citation points at one commit. It updates the citations that moved and addsHostOSRebootwhere v1's handler list, the reconcile diagram and the startup gate were missing it. It deliberately stops before #1034, whose kind renames are still in progress. -
4160c05edfixes wording and consistency, with no design change:- the Teardown flowchart;
- which unit
hostenables; - the 0600 rule, and
schemaVersionon the active-slot record; nodeDependencies.requireswiring;- the
Pendingstate field renamed toRepave; - the
NodeChangedtrigger; - the six LocalDNS template variables;
- glossary terms;
- stale references.
Each item is listed in its commit message.
The earlier review: nearly every finding from it is fixed in the docs, not just in the reply. Two notes on the reply:
- It cites decision IDs (D11-D37) that no longer exist, because the decisions table was removed.
- It says the first reconcile settles an empty goal version. The docs instead record the goal version only when the daemon handles a Node deletion (
04-control-and-state.md§1). The docs are consistent with each other; only the reply is wrong.
1. Migration and cutover (doc 10)
- At the first official v2 release (N+1), a migration failure can loop.
- §6 says any failure before
Committedrolls back to v1 automatically (10:203). - The rollback's step 3 is "start v1's daemon" (
10:246). At N+1 the daemon unit runs<current> daemon, which is N+1's v2 daemon. That daemon would start the migration again. - §2 describes the intended path instead (
10:53-57): N+1 exits, and the recovery unit restores N, whose daemon undoes the migration. - Suggestion: state that N+1 never performs the rollback itself. It should exit with a normal failure code, not the deferred exit code the unit treats as success (
unbounded-agent-daemon.service:17-22).
- §6 says any failure before
- After the rollback window closes, a daemon crash loop can leave the host without an agent.
- v1's recovery unit restores
last-goodafter any repeated daemon failure, not only during an upgrade (unbounded-agent-daemon.service:8-10,unbounded-agent-daemon-recovery.sh:11,26). - After a cutover,
last-goodis N. Once the first repave deletes the backup, N cannot undo anything, and it refuses a v2 host (08§4). - Suggestion: when the backup is deleted, point
last-goodat the running v2 binary.
- v1's recovery unit restores
- The health check before commit is circular at N+1.
10:198-202reuses v1's "stable for 3 seconds within 30" check (hostupgrade.go:23-26). At N+1 the daemon running the migration is the daemon being checked. - The early migration from the cluster is underspecified (
10:103-134):- who publishes the AgentUpgrade result;
- how the v2 daemon learns the migration committed (
10:117) and which operation it belongs to, given v1 and v2 keep separate upgrade signal files; - how the v2 daemon's startup gets the install lock while
v2 migrateholds it (10:168) (inference).
- "Upgrade to N first" cannot reach the operation as written (
10:63-66,11:193). v1's recovery script records a fixed message (unbounded-agent-daemon-recovery.sh:19-20). N+1 would have to record its own reason before exiting. - The check "no other v1 MachineOperation in progress" (
10:144) can't be computed for selector operations. They share one status across agents, and agent kinds other thanHostOSRebootrecord no per-Machine targets (#1040). Suggest counting onlymachineRefoperations for this Machine.
2. Node runtime
- kubelet and containerd restarts would flush the firewall and look like a reboot.
- Each
hostnode unitRequires=unbounded-node-prestart.service(03:247). - The doc says these oneshots run again on every start of a unit that requires them (
03:275-280). - kubelet and containerd have
Restart=always(pkg/agent/phases/nodestart/assets/kubelet.service:8,containerd.service:12). - So each kubelet or containerd crash could re-run
flush ruleseton a live node, and write a new boot ID, which gives the Node a falseRebootedevent (inference about systemd's handling ofRequires=on automatic restarts). - v1 never does this, and it isn't in the behavior-changes table (
08§2). - Suggestion: have only
unbounded-node.targetrequire prestart. Order the node unitsAfter=it withoutRequires=, and extend V5 and V6 to cover a kubelet restart.
- Each
- A refreshed bootstrap token may never reach the slot.
- Secret contents are left out of the digest (
03:199-201). - "Provision, same digest is a no-op" (
03:225). - A repave retry that fetches a new token would keep the expired one in the slot.
- Suggestion:
Provisionalways rewrites secret files.
- Secret contents are left out of the digest (
- The
Recovertable can match two rows (04:164-167, inference).- On
nspawn-container,Layoutdoes not depend on the slot. - The image reference is in neither
NodeSpecnorNodeBundle. - So an image-only repave can give the same digest for old and new.
- Suggestion: compare
Repave.Slotwith the enabled slot, or put the image reference in the digest.
- On
3. Interaction with the machina API (cross-checked against #1045's findings and issues #1038-#1044)
- The rule for agent-owned kinds misses
HostOSRebootitself.04:268-272treats the known kinds plus futureNode*/Agent*kinds as the agent's.HostOSRebootis aHost*kind the agent owns, so a future agent-ownedHost*kind sent to an older agent would stay pending forever, the case this rule exists for.- Failing an unknown selector operation would also fail it for every matching agent: status is shared, and agents skip it once it is terminal (
pkg/agent/daemon/machineoperation.go:105-107, #1040). - Suggestion: an explicit list of agent kinds, and
UnsupportedOperationonly formachineRefoperations.
- Which Machine fields the machina status reporter writes is unspecified (
02:199-213,04§1).- machina owns
Machine.status.phaseand branches on it (cmd/machina/machina/controller/machine_controller.go:276-310). v2 must not write itsPending/Ready/Failedphases there. NodeNotRegistered,RepaveFailed,StateMismatchandStateCorrupthave no field on the Machine without an API change, which is a non-goal.AgentBootstrappedreasons includeNSpawnFailed, which thehostruntime needs a mapping for.
- machina owns
- Gaps v2 inherits but doesn't mention:
- #1039, provisioning ignores
configurationRef: after bootstrap or migration, any Machine whose referenced version differs from what the node was built from will showPending, since v2 now evaluates drift on every reconcile. That differs from v1, which only checks on a Node deletion. HostReplace and metalman reinstalls also produce v1 configurations, so ahostruntime host comes back asnspawn-container(01:210-215mentions only machina). - MachineConfigurationVersion's
agentblock onhost: the block is required, and its image overrides the nspawn image. How it maps onto ahostinstall is unspecified, and the e2e template has noagentblock, so tests won't catch it. - #1043, TTL: the agent never deletes operations. The fleet migration's one-operation-per-Machine pattern (
10:132-134) piles up on clusters without machine-ops-controller or metalman. v2's copy of the reconciler is where #1043's fix would go. - #1044,
HostOSRebootserialization: v2's handler could wait for older out-of-band operations on the same Machine, or defer to #1044 explicitly (05§4).
- #1039, provisioning ignores
- The new
agentModeparameter isn't listed among changes outsideinternal/agentv2(08§4). TheparametersAPI comment and the CRD reference list the accepted keys exhaustively, so both need updating.
4. Testing (doc 11)
- The cutover variants need builds of N and N+1 before those releases exist (
11:190-193). Say how CI produces them, for example a build setting that selects N or N+1 behavior. - The test for a Node deleted while the daemon is stopped (
11:206-207) needs kubelet to register again. That only happens on a kubelet restart (inference), so the test should say how it triggers one. - The host snapshot (
11:127-133) needs an allowlist forsysctl -akeys that change on their own, such as entropy, inode and dentry counters. - At N+1, v1 commands are removed, so
AGENT_PROFILE=v1jobs stop working. §6 should say what replaces them as required jobs.
5. Optional
- The reasons for keeping lifecycle hooks and
Preserve/retainContainerImagesin the first implementation went away with the decisions table. One sentence each, where those features are described, would save reviewers asking again.
- The first official v2 release never rolls a migration back itself. It has no v1 commands, so starting v1's daemon would only start it again. It exits with an ordinary failure code, which the daemon unit escalates to the recovery unit, and release N's daemon undoes the migration. - When the first repave deletes the migration backup, it points the binary rollback slot at the running v2 binary. The recovery unit restores that slot after any repeated daemon failure, and after the window release N refuses a v2 host. - The pre-migration check for operations in progress uses only what can be tied to this host: v1's install lock, its AgentUpgrade signal, this Machine's own operations and its HostOSReboot target entries. The status of selector operations is shared by every agent (#1040). - Provision always rewrites secret files, so a refreshed bootstrap token reaches a slot whose digest has not changed. - The agentMode parameter is listed among the changes outside internal/agentv2, as a documentation change to the API comment and reference page. - Testing: the deleted-Node test restarts kubelet with a NodeReboot, the host snapshot allows sysctl keys that change on their own, a crash loop after the rollback window is a migration variant, and the v2 jobs become the required ones at the first official v2 release.
- On host, kubelet and containerd restart themselves after a failure. If they required the pre-start unit, each restart would run the nftables flush on a live node and give kubelet a new boot ID. The pre-start unit now stays active after it succeeds, the node units use Requisite= on it, the target pulls it in at boot, and Start and Restart restart it explicitly. V5 and V6 cover a kubelet restart. - Recover checks the enabled slot as well as its digest, and the nspawn-container digest covers the OCI image, so an image-only repave can no longer look like no repave. - Whether a runtime can run on a host is decided by capability probes, not a list of operating systems. Each runtime declares what it needs; preflight probes it, and a failing probe names what is missing. The host matrix tests the probes, including the immutable-host entry, and V7 is replaced by them. A Machine whose host cannot run its runtime fails bootstrap with reason RuntimeUnsupported.
… reporting - The runtime becomes the Machine field spec.agent.runtime. A reinstall wipes the host, so the runtime has to live in the cluster or a replaced host could come back as nspawn-container. Every provisioning path writes it into the agent configuration, refuses agent versions that cannot run host, and runs v2's start for a host Machine; newer v1 builds refuse a host configuration. A change takes effect at the next reinstall, shown by RuntimeChangePending. The non-goal of no machina API changes now excludes this field and the new conditions. - What v2 writes on the Machine: everything v1 writes, never status.phase (machina branches on it), status.agent.runtime, and the AgentHealthy and RuntimeChangePending conditions. AgentBootstrapped gains NodeStartFailed and RuntimeUnsupported. No Events, so no RBAC change. - MachineOperation kinds without a handler are ignored, as in v1; the prefix rule missed HostOSReboot and could fail shared selector operations for every agent (#1040). - The agent deletes expired operations it ran for its own Machine (#1043). HostOSReboot keeps v1's behavior and cites #1044. - A MachineConfigurationVersion's agent image is ignored on host and left out of drift; its LocalDNS settings still apply. - A failing repave keeps v1's retries and is reported on the Machine as AgentHealthy=False.
The old plan needed a last v1 release, N, whose daemon could undo a migration, and forced every upgrade through it. That tied the safety of the cutover to code in a v1 release and left hosts on older releases with a confusing "upgrade to N first" failure. - Before the commit, v2 only adds files under its own agent-v2 paths. v1's files, units, boot wiring and install record stay as they were, so whichever v1 binary ran the host can run it again. - The commit first points last-good at v2, backs up v1, installs v2's units, swaps the machine override in one rename, removes v1's files and writes v2's install record last. Any v2 process that finds it unfinished finishes it, so a v1 binary never runs on a changed host. - Going back to v1 inside the rollback window is done by v2: restore v1 from the backup, then switch to the v1 binary. v2's daemon unit only starts a binary that includes v2, so a crash during the restore cannot start v1 on a half-restored host. agentMode: v1 lets an early migration roll back from the cluster without changing releases. - At the cutover, a failure before the commit is reported by v2 itself and the daemon switches back to last-good. In an early migration the migrate unit records Abandoned or RolledBack, and v1's daemon reports the failure instead of migrating again. - Phases are Preparing, Prepared, Committing, Committed, Abandoned, Restoring and RolledBack, with diagrams for each path and a table of what a host reboot does in each phase. - The oldest supported v1 release is set by the migration test matrix, which replaces the release N rule and the Skipping-N test. - The daemon is Type=notify and reports READY=1 when its startup gate opens. An AgentUpgrade completes only once the new daemon is ready, and the start timeout and start limit make a daemon that never gets ready roll back like one that crashes. v1 reports Complete as soon as the daemon registers.
Plugins ran as root from any path the configuration named, were never upgraded by the agent, and could drift from the agent binary. That made every plugin a root-level trust decision guarded only by file ownership. - Each plugin has a sha256 in the configuration. Calls run in a transient systemd unit whose command is the agent's hidden plugin-exec: it hashes the stored plugin through the open descriptor and execs that descriptor, so the file checked is the file that runs. - Only ELF executables run. A #! script cannot be pinned this way, since its interpreter reopens it by path and is not pinned itself. Shared libraries are not pinned either, so preflight warns about dynamically linked plugins. - By default a plugin runs as a dynamic unprivileged user with no capabilities, a read-only OS, no access to the agent's credentials or state, and a clean environment. plugins[].permissions declares extra paths, capabilities or root. Each call writes an audit line without the request or response. - Plugin executables live in a set beside each blue/green binary and change only with an AgentUpgrade, which stages a new set from a plugin manifest or copies the running one. Binary and plugins switch and roll back together, and the daemon checks every plugin with describe before it reports ready. Names, permissions, env, timeouts and hooks stay install-time settings, so an upgrade cannot widen what a plugin may do. - First install takes the executables from the configuration file; the file's plugins stay in the install fingerprint, so v2 start with changed plugins is refused.
…riteria The experimental label had been used to let v2 jobs report without blocking, and nothing said what must hold before v2 replaces v1 on every host. Operators migrate hosts early onto the experimental releases, so they need the same bar as the release that follows. - The v2 jobs, including the migration suite and the cutover variants on a test build, are required on agent pull requests from the first v2 code. At the first official v2 release the v1 jobs are removed and AGENT_PROFILE defaults to v2. - The cutover variants run on a development build with a test-only build tag before the release exists, and against the real release builds once they do. - Implementation section 4 lists the exit criteria for the first official v2 release: validation tasks V1 to V6, required CI, the migration matrix against release builds, the capability probes, and public documentation. The README states that "experimental" covers the interfaces only, not the quality of the code.
Summary
This PR adds
designs/agent-v2/, a design for an experimental v2 of the unbounded agent. It is opened for commentary on the design; there are no code changes.v2 would ship inside the existing binary as
unbounded-agent v2 <command>, logging a warning that it is experimental. It has two goals:NodeRuntimehas two implementations:host: kubelet and containerd run directly on the machine.nspawn-container: today's behavior, and the parity reference.pkg/agent. The hook points are:All v2 code would start in
internal/agentv2. Moving anything to an exported package requires an explicit, recorded decision.How to review
Start with the README. It covers the purpose, goals, architecture and terms, and lists the other documents with who should read each one:
01-cli-and-configuration.mdv2commands, the configuration file, install-time and goal settings, credentials02-extensions.md03-node-runtime.mdNodeRuntimeinterface and its rules, the node bundle, slot states, boot units, the two runtimes04-control-and-state.md05-lifecycles.md06-host-changes.md07-host-runtime-cleanup.mdhostruntime stops, cleans up and removes a node08-v1-compatibility.md09-implementation.md10-migration-from-v1.md11-testing.mdWhere review is most needed:
hostruntime cleanup, in document 07.