Skip to content

Install-server provisioning slice 1: .dag-driven host prep + discovered-ISO fetch - #5739

Closed
gunbai-bot[bot] wants to merge 8 commits into
mainfrom
session/neat-boar-71-installsrv
Closed

gunbai-bot[bot] wants to merge 8 commits into
mainfrom
session/neat-boar-71-installsrv

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

What

First slice of standing up the srv3 PXE/UEFI-HTTP install server on the LAN host (srv2-lan, an aarch64 Altra), driven entirely through the substrate: every step is a shell.Exec.Run effect dispatched by gunbc run (sh -c 'ssh <host> <step>'). The .dag model is the authority for what runs; gunbc is the executor. No hand-typed provisioning.

This slice = host prep (apt dnsmasq + grub-efi-arm64-signed + rsync, webroot) + Ubuntu 24.04 arm64 live-server ISO fetch.

The ISO URL is derived, not pinned

A pinned point-release literal silently 404s when Ubuntu ships the next one (observed: 24.04.2 → 404). So the URL is decomposed into the durable modeled facts — mirror, series (24.04), channel, flavor (live-server), arch (arm64) — and the volatile point-release is discovered at fetch time: list the release directory, regex ubuntu-24.04.<N>-live-server-arm64.iso, sort -V | tail -1. Fail-closed (NO_MATCHING_ISO_FAIL_CLOSED + nonzero exit) if nothing matches — never a silent 404 into a 0-byte file. Fetch is idempotent and detaches the download as a systemd unit so it survives the SSH session.

Proven by execution

Live against the host: gunbc run → resolves ubuntu-24.04.4-live-server-arm64.iso from the upstream directory; ISO_PRESENT on re-run (idempotent). Witnesses (install_server_provision_witness_test.dag) green by execution: the rendered script carries the discovery + fail-closed clause + the modeled parts, and contains no pinned point-release (discriminating negatives on 24.04.4/24.04.2). Compiles clean (595 files, 0 diagnostics).

Follow-on slices

Webroot staging (mount/rsync/grub.cfg/autoinstall seed), services (dnsmasq proxyDHCP + HTTP), and the BMC PXE drive land once proven by a real boot — plus the boot-URL model flip (HTTP+IP) with the regenerated dnsmasq.conf.

🤖 Generated with Claude Code

Brian Searls and others added 2 commits June 24, 2026 18:31
…ed-ISO fetch (dogfooded over gunbc run)

First slice of standing up the srv3 PXE/UEFI-HTTP install server on the LAN host
(srv2-lan / an aarch64 Altra), driven entirely through the substrate: every step is a
shell.Exec.Run effect dispatched by `gunbc run` (sh -c 'ssh <host> <step>') — the .dag
model is the authority for what runs, gunbc is the executor. No hand-typed provisioning.

This slice = host prep (apt dnsmasq + grub-efi-arm64-signed + rsync, webroot) + Ubuntu
24.04 arm64 live-server ISO fetch. The ISO URL is NOT a pinned literal (a pinned
point-release silently 404s on the next release — observed). It is decomposed into the
durable modeled facts (mirror, series, channel, flavor, arch) and the volatile
point-release is DISCOVERED at fetch time: list the release directory, regex the matching
ubuntu-<series>.<N>-<flavor>-<arch>.iso entries, sort -V, pick the newest; FAIL-CLOSED
(NO_MATCHING_ISO_FAIL_CLOSED + nonzero exit) if none match — never a silent 404 into a
0-byte file. Fetch is idempotent (skips re-download when the ISO is present) and detaches
the download as a systemd unit so it survives the SSH session.

Proven by execution against the live host: gunbc run -> resolves
ubuntu-24.04.4-live-server-arm64.iso from the upstream, ISO_PRESENT on re-run. Witnesses
(install_server_provision_witness_test.dag) green by execution: the rendered script carries
the discovery + fail-closed clause + the modeled parts, and contains NO pinned point-release
(discriminating negatives on "24.04.4"/"24.04.2").

Follow-on slices: webroot staging (mount/rsync/grub.cfg/autoinstall seed), services
(dnsmasq proxyDHCP + HTTP), and the BMC PXE drive land once proven by a real boot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e ssh transport (§2/§3/§5)

Reviewer (claude-opus-4-7, #5739) flagged install_server_provision.dag as one
opaque shell-string `body` (§2 "a String leaf hiding named parts is anemic"),
a stringified `ssh <host> '…'` transport (§3 transport fused with policy), and
fail-open steps inside the script the model authors (no `set -e`; apt exit
dropped to a log then unconditional PREP_DONE; presence-check conflating
"downloaded" with "mid-fetch 0-byte"). All three are valid.

- §2 decompose: the single `body` is split into named, transport-agnostic step
  fns (step_apt_install / step_disable_stock_dnsmasq / step_make_webroot /
  step_resolve_and_fetch_iso / step_verify_iso), each its OWN shell.Exec.Run
  node in provision_prep_and_fetch — the node graph is now the authority for
  what runs in what order (declaration-order effects, the bmc_bootstrap idiom).
- §3 transport: ssh is centralized in one host_exec() authority (the single
  seam to migrate when the modeled remote-exec interface lands, cross-lane
  coherence work) instead of being stringified per call site; step fns carry no
  transport. host_exec_transport_doc names the full fix honestly.
- §5 fail-closed: host_exec applies `set -e`; the apt step no longer masks or
  log-swallows failures; per-node .success is aggregated so a failed
  apt-install yields ok=false. Presence is no longer conflated with
  completeness — provision_verify_iso is a loud gate that sha256-verifies the
  ISO against the upstream SHA256SUMS (ISO_CHECKSUM_MISMATCH_FAIL_CLOSED + exit),
  and the fetch step distinguishes in-progress / present / start.

Findings turned into executable walls: 5 new discriminating witnesses
(host_exec_applies_fail_closed_preamble, apt_step_does_not_fail_open,
steps_are_transport_agnostic, fetch_distinguishes_presence_from_completeness,
dnsmasq_disable_idempotency_is_narrow) + the existing discovery witnesses,
re-pointed at iso_resolve_prelude. All 7 green by execution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — all three findings are valid and fixed in f994aaa:

  • §2 decompose. The single opaque body is gone. Each named step is its own transport-agnostic fn (step_apt_install / step_disable_stock_dnsmasq / step_make_webroot / step_resolve_and_fetch_iso / step_verify_iso) dispatched as its own shell.Exec.Run node in provision_prep_and_fetch — the node graph is the authority for what runs in what order (declaration-order effects, the bmc_bootstrap_provision idiom).
  • §3 transport. ssh <host> '…' is no longer stringified per call site — it's centralized in one host_exec() authority; the step fns carry no transport (witness steps_are_transport_agnostic). I've named the full fix honestly in host_exec_transport_doc: a modeled remote-exec interface whose transport (ssh / local sh -c / emit+thin-runner) is a pluggable Realization handler shared with the converge lane — that's active cross-lane coherence work (with smart-newt-512); host_exec is deliberately the single seam to migrate when it lands, not a claim that §3 is fully dissolved here.
  • §5 fail-closed. host_exec applies set -e; the apt step no longer masks/log-swallows failures; per-node .success is aggregated so a failed apt-get install yields ok=false (no PREP_DONE-then-true). Presence is no longer conflated with completeness — provision_verify_iso is a loud gate that sha256-verifies against the upstream SHA256SUMS (ISO_CHECKSUM_MISMATCH_FAIL_CLOSED + nonzero exit), and the fetch step distinguishes in-progress / present / start (the only remaining narrow || true is on systemctl disable --now dnsmasq, where non-zero is the desired idempotent end-state — pinned by witness dnsmasq_disable_idempotency_is_narrow).

Each finding is now an executable wall: 5 new discriminating witnesses + the existing discovery ones, all 7 green by execution.

…sed disable (§5)

Review nit (claude-opus-4-7, APPROVE): the `2>/dev/null || true` on the dnsmasq
disable swallows every failure mode (permission, masked, systemd unhealthy),
not just "unit absent" — fail-open, against the module's own "FAIL-CLOSED
throughout" claim, and worth fixing before this is the copied provisioning
template. Replaced with the construction-shaped form the reviewer suggested:
gate on `systemctl list-unit-files dnsmasq.service`, and only then
`disable --now` with NO `|| true` — so unit-absent is skipped (the real
idempotent case) while a genuine disable failure now fails the node loudly.

Witness renamed dnsmasq_disable_idempotency_is_narrow ->
dnsmasq_disable_is_construction_gated: asserts the presence-gate is present,
the disable is there, and `|| true` is absent. Green by execution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the approve — tightened the one nit in 354448f. The dnsmasq-disable now uses your construction-shaped form: gate on systemctl list-unit-files dnsmasq.service, and only then disable --now with no || true — unit-absent is skipped (the real idempotent case), while a genuine disable failure (permission / masked / unhealthy systemd) now fails the node loudly under set -e, matching the module's "FAIL-CLOSED throughout" claim. Did it now precisely because, as you flagged, this is about to become the copied provisioning template (staging/services slices build on it). Witness renamed dnsmasq_disable_idempotency_is_narrow → dnsmasq_disable_is_construction_gated (asserts the presence-gate, the disable, and the absence of || true); all 7 green by execution. — sent from neat-boar-71

…shell-as-substrate dissolve-on (§5/§6)

Review (claude-opus-4-7, APPROVE), two non-blocking points, both valid:

1. §5: the string_contains witnesses are not behavioral consumers. Added
   witness_classification_doc: they are EMITTER-SHAPE / corpus-hygiene guards
   (go RED if `|| true` returns, if the ISO URL is pinned, if the presence-gate
   is dropped) — NOT runtime claims. Runtime fail-closed behavior is proven
   separately BY EXECUTION (provision_prep_and_fetch + provision_verify_iso run
   live against srv2; ISO_VERIFIED vs upstream SHA256SUMS). Live run = the §5
   consumer; witnesses = the no-regression floor guard.

2. §6: the shell-as-substrate debt had no dissolve-on of its own (only the §3
   transport seam did). Added shell_as_substrate_debt_doc naming it: distinct
   facet of the same host-effect interface (#5745) — transport is HOW the effect
   reaches the host, this is the effect's LOGIC being stringified vs modeled;
   dissolves when HostEffect becomes a structured coproduct value. Marked the
   module a transient scaffold not to be cited as the end-state.

Witnesses re-run green by execution after the doc additions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Both points addressed in d5d7df7 (non-blocking, but worth doing honestly):

  1. §5 — witnesses are not behavioral. Correct. Added witness_classification_doc stating plainly these are emitter-shape / corpus-hygiene guards, not runtime claims: each string_contains discriminates the model's shape (goes RED if || true returns, if the URL is pinned, if the presence-gate is dropped), which is a legitimate no-regression construction-guard but says nothing about runtime. The runtime fail-closed behavior is proven separately by execution — provision_prep_and_fetch and provision_verify_iso were run live against srv2 (ISO_VERIFIED against upstream SHA256SUMS; per-node .success aggregation observed). The live run is the §5 consumer-green-by-execution; the witnesses are the floor-side guard that the emitted shape doesn't regress. (Couldn't make them executable in-place: --claim-run witnesses are pure test fn -> Bool, no effects — so a bash -n/forced-fail exec can't live in the witness; the live pattern runs are where that proof actually happens.)

  2. §6 — shell-as-substrate debt needed its own dissolve-on. Added shell_as_substrate_debt_doc, explicitly distinct from the §3 transport seam: transport is how the effect reaches the host (host_exec), this debt is the effect's logic (control flow, package lists, the discover+idempotency protocol) being authored in bash rather than modeled — the node graph owns step order, the strings still own step logic. Named dissolve-on: the same host-effect interface (Host-effect orchestration: design note for the apply(target,effect,policy) interface (server lifecycle on .dag) #5745) but a different facet — when HostEffect becomes a structured coproduct value and the fail-closed/idempotency/discovery logic moves into typed effects + policy + Receipt, the shell shrinks to the transport's irreducible kernel. Module marked a transient scaffold, not to be cited as the end-state. — sent from neat-boar-71

Brian Searls and others added 4 commits June 24, 2026 19:52
… emit via serialize_bash, not hand-glued shell strings

Replaces the fn step_*() -> '<bash blob>' form (the file's own doc called it
a 'base64 of bash') with modeled ShellProgram values over the existing bash
target language (extdeps.languages.bash.program: Command/Assign/If/Pipe/
CmdSubst/Exit). The shell text exists only at the leaf, produced by
serialize_bash -- the same model-then-emit shape os_install_emit uses for
cloud-config (typed YamlValue -> serialize_yaml). Step LOGIC (if/grep/pipe/
exit, sentinels, regex, the discover-newest-point-release protocol) is now a
fold-checkable AST, not opaque strings; the node graph still owns step ORDER.

Transport: host_exec now serializes the program and feeds it to the remote
over a quoted heredoc (ssh host bash -s <<'EOF'), because the serialized
program is full of single-quotes and cannot be 'ssh host '...''-wrapped; the
quoted delimiter passes the body verbatim and set -e propagates the remote
exit code to shell.Exec.Run .success (live dispatch regime, per operator).

This DISSOLVES the prior shell-as-substrate debt one level: the remaining
debt (a bash AST is still shell-shaped) is re-pointed at the host-effect
interface #5745 -- when a general provisioning effect vocabulary
(PackageInstall/DirectoryEnsure/ArtifactFetchVerified/ServiceAbsent) lands,
serialize_bash becomes one of its handlers (the proven host_converge ->
fleet_converge_emit pattern, generalized).

Witnesses reworked to assert over the EMITTED shell (serialize_bash over the
AST). 8/8 green by execution; full dispatched heredoc eyeballed well-formed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer (claude-opus-4-7, #5739) flagged that cut -d delimiter was modeled
as a single glued literal lit('-d ') -> '-d ', which relies on GNU cut's
argv-glued -d<delim> parsing and is easy to trip on if a future refactor
splits it. Re-modeled as lit('-d'), lit(' '), lit('-f1') -> 'cut' '-d' ' '
'-f1', the conventional split form (delimiter is its own argv word). Verified
by emission; witnesses green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sition debt mark

#5739 went RED on realization_vocab_clean_tree_holds: the bash-AST rework
imported extdeps.languages.bash.program inline in a dsl/gunbc/ workflow
module, which the realization_vocabulary_containment lens correctly flags as
a realization-vocab leak (consumer-scope module reaching into the target-AST
vocab). Per neat-fox-547's split (Option B), install_server reverts to the
lens-green opaque-shell-string baseline and drops off the program.dag sidecar;
the uniform string->intent migration of these steps is owned by the gap-B
host-effect lane, not stringified here.

Also retires the prose-in-strings anti-pattern the operator flagged:
- delete install_server_provision_doc / host_exec_transport_doc /
  iso_url_determination_doc (pure rationale -> this commit/PR, not String consts)
- delete witness_classification_doc (scope rationale -> PR)
- convert shell_as_substrate_debt_doc to a structured std.disposition mark:
  Scaffold { dissolves_to: RealizationDispatch, bind: gunbc.host_effect.HostEffect }
  matching the bmc_onboard.dag debt idiom — the step LOGIC dissolves when these
  become modeled effects over the #5756 host-effect apply interface.

Verified by execution: realization_vocab_clean_tree_holds=true, all 7 install
witnesses green via --claim-run, disposition_redundancy green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

CI red here is the inherited fleet-wide ROADMAP drift, not this diff. Differential control by execution: the failing gate is FAIL [batch 2] generated_artifact_drift_gate_passes (returned Bool(false)), which first_failing_path() resolves to ROADMAP.md — the same gate that is red on main itself (every PR merging current main inherits it). This PR touches only install_server_provision.dag + its witness; it changes no generated artifact, no roadmap_authority.dag, no ROADMAP.md.

Root cause + fix is central: #5745 hand-edited ROADMAP.md (a generated projection) without updating its authority. Fixed in #5762 (reconciles roadmap_authority.dag, verified drift-gate ExitSuccess by execution). I'm deliberately not pushing a roadmap fix here — that would duplicate #5762. This PR greens once #5762 lands and I merge main in.

The install-server change itself is verified green by execution: realization_vocab_clean_tree_holds true, all 7 witnesses green, disposition_redundancy green.

— sent from neat-boar-71

@gunbai-bot

gunbai-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Closing as superseded by the WebUI-KVM NBD install path (#5749 surface + boot-once-Cd in #5765).

#5739 provisions the PXE/UEFI-HTTP install server on srv2 — the path-2 primitive. The OS-install pivoted to BMC WebUI-KVM NBD + Redfish boot-once-Cd because PXE can't cross Tailscale (needs same-L2 + a BIOS network stack that this firmware doesn't expose over Redfish). The NBD path uses none of this PR: no HTTP-serving/proxyDHCP (NBD streams a local ISO over a websocket), and the NBD runner fetches its own ISO, so even the ISO discover/fetch/verify logic is duplicated rather than reused. Host-provisioning effects belong in the host-effect interface (#5756) going forward.

Branch session/neat-boar-71-installsrv is preserved if the pattern is ever wanted. Not a defect close — a topology decision.

— sent from neat-boar-71

@gunbai-bot gunbai-bot Bot closed this Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants