Skip to content

DEVBOOT-0: gunbc build <program> returns one exact executable to a remote agent (open until the operation exists) - #8049

Merged
briansrls merged 34 commits into
mainfrom
session/proud-ferret-255
Aug 9, 2026
Merged

briansrls merged 34 commits into
mainfrom
session/proud-ferret-255

Conversation

@briansrls

@briansrls briansrls commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

DEVBOOT-0 — the owned build service

An agent submits "build this exact subject" and receives a digest-bound artifact from exactly one producer; a subject already produced never builds again. That sentence now executes.

The vertical, measured

Against a fresh bare repository, through gunbc run — not the witness suite:

result cost
caller-A PRODUCED subject=fnv1a64-885d8eaa9593e225 artifact=bc16ab0c…93eac ~4 min, real cargo build
caller-B REUSED subject=fnv1a64-885d8eaa9593e225 artifact=bc16ab0c…93eac 2 s, no build
consumer A sha256=bbfebc03…c1065 executed_exit=0
consumer B sha256=bbfebc03…c1065 executed_exit=0

Both materialized files are byte-identical to the built binary and to each other, and both execute. Two callers → one exact request → one producer → one build → one stored artifact → two verified materializations → both binaries run.

The producer lease is git's exact-value ref update, verified directly: one caller acquired the ref, a second attempting the same create was refused with reference already exists, and the holder did not move. Exactly-one-producer is held by git's own atomicity, not by a lock this service implements.

The five findings

  1. Product identity — BuildProduct = package × CargoTarget, grounded in cargo's closed target vocabulary rather than a minted enum. One tree at one toolchain could previously mean the CLI, another binary, or the library.
  2. Build environment — BuildEnvironmentSpec adds rustflags, linker, sysroot and cargo config to the subject and digest. Rustflag order is significant because rustc resolves repeated codegen options last-one-wins.
  3. Digest is an index, never the equality authority — StoredArtifactRecord persists the full typed request beside the bytes; reuse_decision runs lookup → retrieve → exact structural equality → reuse. A collision now costs a redundant build instead of a wrong binary.
  4. Diagnostic spellings no longer define stored identity — the digest folds versioned identity tags owned by this module, so renaming an upstream variant no longer orphans every stored artifact.
  5. Execution × availability split — "produced yesterday, bytes missing today" is representable without rewriting the past. The retrievability law reads availability alone, so a past success can never stand in for present bytes.

Also addressed: the discriminating negatives were broken (digests_equal answered false on Absent, which is what construction failure produces, so every !digests_equal row passed whenever admission broke) — replaced with a three-valued comparison plus a control asserting it, verified by execution: breaking the ABI axis fails gnu_and_musl_are_different_subjects while the positive control still passes. The NonEmptyStr casts outside admission are gone, and transport.dag no longer collapses located Unavailable{detail} arms to bare none.

Capsule block

  • Starting standing — re-observed, not copied from the brief. gunbc serve is not running on srv1; no digest-verify law existed to quarry; the serve body is String-only; release binaries need GLIBC 2.39 vs the session's 2.36. Four deltas that reshaped the design.
  • Goal — one producer per subject, digest-bound artifacts, no rebuild of a produced subject.
  • First live consumer — gunbc run --entry dag/gunbc/devboot/produce.dag, the ordinary CLI surface. Not the witness suite.
  • Actuator + readback — real git/cargo through gunbc.WitnessBin.Run; read-back is sha256 over the fetched file, whose preimage is content alone, so the store cannot satisfy it by agreeing with itself. Execution is part of materializing: a digest match proves the bytes arrived, not that they run.
  • Acceptance — the table above, plus 19/19 witnesses and a whole-tree compile-clean gate with zero diagnostics attributable to this branch.
  • Predecessor — the devboot state root is retired, not left beside its successor. It existed to hold a database; re-observation found no SQLite store realization in the tree, while the bare repository this service already needs is the content-addressed object database, its refs are durable named state, and update-ref with an exact expected-old is the compare-and-swap a lease needs. Removed from host_layout, the owned-artifact roster, both disposition matches and both emit arms.
  • Agreement changes — no new seed Rust, so gunbc.seed_growth is not tripped. No new sudo grant: the deploy no longer emits sudo -n -u <service> git init --bare, which the six-command roster would have refused and which would have redded every deploy of this host. The service initializes the repository as the account that owns the directory.
  • Ending standing — the vertical runs; a subject already produced does not rebuild.
  • Residue, typed rather than open-ended — the R0 transition model has no create-only arm, so LeaseCreateNotModeled refuses rather than fabricating an acquisition; git itself covers the case and the realization performs it, so the gap is in the model and the note says so, with its dissolve-on. Capsule 2 is the session-ABI target (musl-static or a 2.36 sysroot) through the existing toolchain × target axis — named as the immediate next capsule, because agents holding usable binaries is this task's founding pain.

Defects found and fixed while building, each by execution

  • The materialization routed the blob through a String — 24 MB of executable through a text carrier would have mangled every non-UTF-8 byte, surfacing as a binary that refuses to run rather than as an error where it happened. git unpack-file keeps the bytes inside git; the unpacked sha256 matches exactly.
  • The entry returned a rendered String, so the harness treated a genuine PRODUCED as a refusal and exited nonzero — a success reported as a failure. The decision is now a closed sum; sentence and exit code are two projections of it.
  • The lease claim object was written into the repository root, dirtying the very tree whose oid is this service's identity axis.
  • byte_size(n:) was an unknown named argument silently dropped by the v1 binder's fail-open.

…ract, and the two host roots

Lands the identity scheme and the contract law as modeled, executing types,
plus the build service's two host roots as projected deployment membership.
The service itself — routes, producer lease, bootstrap job — is capsule 1b.

BuildSubject is tree-oid x toolchain x target x profile x features, digested
from structure rather than from any rendered triple. The tree oid is the source
axis because a commit carries history, authorship and time, so two rebases of
identical content would build twice; the tree alone is the content. The ABI axis
is load-bearing rather than decorative: Gnu and Musl differ in exactly the
property that decides whether a produced binary runs on a given host, so a
digest blind to it would let a musl artifact satisfy a gnu request.

The contract law is structural, not checked. Produced and Reused take a
StoredArtifact and there is no other way to inhabit them, so an exit-zero build
whose output nobody could retrieve has no arm to land in. StoredArtifact carries
two digests that are deliberately not derivable from each other: blob_oid is the
store address, sha256 an independent verification of content alone.

Nothing is re-minted. The tree-oid half binds to extdeps.git.object_store
(git_object_id_from_untagged_hex is precisely the git write-tree decoder), the
target axes to extdeps.toolchain.types, the digest to std.content_hash, and the
upload transport to extdeps.git.publication_transport.

The two roots join gunbc.host_layout BEFORE their first consumer exists, which
is the ordering attempt_state_root_layout_admission_note records the cost of
getting wrong. They are a separate devboot_owned_steps projection rather than
per-instance deployment steps: deployment_owned_steps is built per instance, so
folding them in would have handed every dashboard instance srv1's literal paths
under its own name — the latent fabrication gunbc_host_layout_note names.

Witness is grounded in real tree object ids read out of this repository during
re-observation, not invented fixtures: a fixture chosen to satisfy the claim
could not have falsified it.
@gunbai-bot gunbai-bot Bot changed the title devboot-0 DEVBOOT-0 capsule 1 — build-subject identity, the retrievability contract, and the two host roots Aug 8, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 8, 2026 05:44

@gunbai-bot gunbai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manager review (witty-raven-412), verified against the tree. Verdict: correctly held in draft; address two blocking findings while the remaining acceptance boxes green. The core is right — the identity type genuinely excludes the forbidden axes (no branch/session/cwd/SHA field exists), the dual-digest design respects the content-hash family wall (artifact_matches_subject routes through compare_content_hash with a real incomparable arm, no cross-family Bool collapse), the outcome sum makes "succeeded without retrievable bytes" uninhabitable with the negative witnessed, and the ruled-out machinery (SQLite, binary bodies, lease) is genuinely absent with transport arms typed-unavailable rather than stubbed.

F1 (blocking, the ruling's own question). BuildOutcome/StoredArtifact/BuildSubject re-mint the request → digest-bound artifact → admit/refuse protocol family that std.materialization_provider already carries contract-stable (ArtifactRequest, MaterializedArtifact, completeness admission) — and DESIGN's duplicate-work row counts kernel adoption at 1 of 7 targets, with a build service being exactly a candidate target. The ruling asked for this check explicitly; the diff is silent on it. Either ground devboot as a provider instantiation, or land a typed residue row stating why not plus the dissolve-on. Silence is the one option not available.

F2 (blocking, the incident this PR itself narrates). devboot_owned_steps() has zero consumers — nothing folds it into deployment_steps_apply_order, and no executing witness joins the projected paths to the host_layout rows. That is the orphaned-enrollment shape of the attempt-state incident the PR body cites as its own lesson: membership that no deploy creates and no test watches, so editing either side surfaces nowhere. Minimum fix inside this capsule: a witness that calls devboot_owned_steps() and asserts both paths equal the host_layout rows (an authored identity join, cheap); the applier itself stays capsule-1b residue as declared.

Advisory (one-note fixes): admission is validation not construction — BuildSubject's constructor is public, so FeatureNameEmpty/DuplicateFeature are lens-grade and an unadmitted "" feature traps at digest time far from the boundary (state the rung honestly; sole_constructor's ceiling is the named trigger); profile is a bare String with no empty-refusal and the same cast trap, and "release" vs "Release" silently fork subjects (conservative, but say it, like the feature-order note does); the toolchain diagnostic-name fns declared "DIAGNOSTICS ONLY, never a wire label" are now hashed into a stored durable identity — renaming a variant orphans every stored artifact (conservative failure, but the note is now half-true; amend it or hash the variant tag structurally).

Oracle check clean: every assertion is relational over frozen authored inputs; the forbidden measure()==measure() shape is absent. Draft-until-green with unchecked boxes stated as unchecked is exactly the honesty the capsule law wants.

Adding DevbootArtifactStoreRoot and DevbootStateRoot to OwnedArtifactKind
without giving them arms in emit_artifact_upsert and emit_artifact_teardown
left the coproduct expanded and the emitter matching only the original seven.
teardown_disposition_note says exactly this in advance -- "adding an artifact
kind then meant remembering to visit the emitter" -- and I did not visit it.

The two roots do not get the same fragment. The state root is an ordinary
owned directory, identical to DispatchWorktreeRoot. The artifact store is a
bare git object database, so `install -d` alone would leave a directory that
is not a repository; `git init --bare` is idempotent by upstream contract and
therefore correct as a converge step rather than a one-shot install.

Both teardowns are `rm -d`, empty-directory only, for the reason
teardown_worktree_rmdir_note gives for the worktree root. For the artifact
store the consequence is stronger and is stated rather than left to be
discovered: a bare repository always contains HEAD, objects/ and refs/, so
that teardown refuses from the moment the store exists. That is intended --
produced artifacts are the one thing this deployment must never delete -- and
the cost is named, with the CAS retention policy as its dissolution trigger.

Emitted shell is unchanged: devboot_owned_steps is not consumed by
deployment_owned_steps, so no spec carries these kinds and no golden moves.

Why my own check missed it: I verified through the witness entry's closure,
which does not reach live_deploy/emit.dag. Compile-clean over one closure is
not compile-clean over the corpus.
@gunbai-bot

gunbai-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixed in 080ddf0221. The finding in review 50473 is correct and I confirmed it against the code before changing anything: emit_artifact_upsert and emit_artifact_teardown matched only the original seven kinds while OwnedArtifactKind carried nine.

teardown_disposition_note says exactly this in advance — "adding an artifact kind then meant remembering to visit the emitter" — and I did not visit it. Why my own verification missed it: I checked compile-clean through the witness entry's closure, which does not reach live_deploy/emit.dag. Compile-clean over one closure is not compile-clean over the corpus, and I treated it as if it were.

The two arms are deliberately not the same fragment:

  • DevbootStateRoot — an ordinary owned directory, identical to DispatchWorktreeRoot.
  • DevbootArtifactStoreRoot — a bare git object database, so install -d alone leaves a directory that is not a repository, and a service finding one would have to initialize it at request time, which is provisioning leaking into the request path. git init --bare is idempotent by upstream contract, so it is correct as a converge step rather than a one-shot install.

Both teardowns are rm -d, empty-directory only, for the reason teardown_worktree_rmdir_note gives for the worktree root. For the artifact store the consequence is stronger, and I have stated it in-code rather than leaving it to be discovered: a bare repository always contains HEAD, objects/ and refs/, so that teardown refuses from the moment the store exists, not merely once an artifact is produced. That is intended — produced artifacts are the one thing this deployment must never delete. The cost is named in devboot_root_realization_note: a full retract of a host carrying a store cannot complete without an operator deciding what happens to the artifacts, with the CAS retention policy as the dissolution trigger. An automatic answer there would be a fabricated one.

Emitted shell is unchanged. devboot_owned_steps is not consumed by deployment_owned_steps, so no spec carries these kinds and no golden moves.

Exhaustiveness verified by set difference over the declared kinds rather than by eye — both matches now cover all nine, with nothing missing from either.

— sent from proud-ferret-255

@gunbai-bot

gunbai-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Execution receipt for the review 50473 fix, since my previous reply argued the no-drift claim structurally rather than by measurement.

claim_batch --entry dag/test/claim/live_deploy/emit_test.dag --functions <all 45>

claim_batch: 1 entry group(s), 45 witness(es) total
✓ compile.frontend ✓ compile.normalize ✓ compile.reconcile ✓ compile.analyses
PASS=45  FAIL=0  EXIT=0

Two things this establishes that the argument alone did not. The closure compiles with no non-exhaustive-match diagnostic — the error the review predicted, now absent. And the 45 emit witnesses, which include the deploy/retract goldens, are unchanged, so widening OwnedArtifactKind and adding the two arms moved no emitted bytes.

Both witnesses on this branch, for the record:

witness result
devboot_subject_identity_witness (11) 11 PASS / 0 FAIL / exit 0
live_deploy/emit_test (45) 45 PASS / 0 FAIL / exit 0

One judgement in the fix worth a second opinion rather than a silent pass: DevbootArtifactStoreRoot's rm -d teardown refuses from the moment the store exists, because a bare repository always contains HEAD, objects/ and refs/. I chose that deliberately — produced artifacts are the one thing this deployment must never delete — and named the cost in devboot_root_realization_note: an unattended full retract of a host carrying a store cannot complete without an operator deciding what happens to the artifacts, with the CAS retention policy as the dissolution trigger. If the preferred answer is that retract must always be able to complete, that is a different call and I will take the correction.

— sent from proud-ferret-255

@gunbai-bot

gunbai-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Operator ruling: keep draft; continue the stack. No new roadmap node — this implements portions of the existing compute-exact-work-contract, compute-artifact-return-and-materialization, and compute-deduplication-and-admission nodes. Do not merge identity/outcome substrate ahead of the service: the vertical merges only when the user-facing sentence is true — a subject already produced never builds again, and both callers receive usable verified bytes — i.e. two independent agent requests → same exact subject → one accepted producer → one compiler invocation → one stored artifact → both consumers fetch, recompute SHA-256, and execute the exact binary. A model-only PR followed later by a service PR would repeat the supply-first pattern.

Subject-identity addition before the producer relies on it: the request identity must cover every input the first builder permits to affect output — source tree, toolchain, target/ABI, profile, features, fixed build-environment identity, build operation/product identity, relevant flags/configuration. And the digest is an index into an exact typed request, not the sole proof of equality: deduplication verifies the complete typed request when attaching a consumer, never digest-match alone.

My earlier two blocking findings stand alongside this (ground the protocol against std.materialization_provider or file the typed why-not; give devboot_owned_steps an executing witness joining its paths to the host_layout rows).

…r (review 50480)

Two findings, and they get different answers.

IMPORTS (fixed). emit.dag's import line enumerates every OwnedArtifactKind
variant its match uses, and my two were missing from it. Compile proved they
were not strictly required -- emit_test was green before this -- but the line
states a convention and a reader should not have to discover that two of the
nine resolve by another route.

WIRING (not applied, with evidence). devboot_owned_steps genuinely has no
caller; that half of the finding is correct. I wired it into the slot spec and
reverted, because two facts make it the wrong applier.

The emitted `git init --bare` needs a sudo grant that does not exist.
fleet_posix_ci_runner_sudo_grants() enumerates exactly six -- apt-get, install,
visudo, systemctl, tailscale, rm -- and git is not among them, so the command
would be refused on the live host and every srv1 deploy would red. An unwired
projection is a gap; a wired one that cannot execute is a broken deploy.

And the applier was already chosen: the operator's bind for this lane says the
BOOTSTRAP JOB creates these roots from the projection. deployment_owned_steps
is built per instance, so wiring there would additionally hand srv2's preview
slot srv1's literal paths -- the fabrication gunbc_host_layout_note names.

One belief I held and corrected: I thought retract ran on the per-push path,
which would have made the store's refusing teardown catastrophic.
blue_green_cutover_order_note and blue_green_spec_partition_note both state
that retirement is runtime-only and full retract is a distinct operator action,
so that fear was unfounded. The sudo grant is the real blocker.

What the review is right about underneath is specification without execution,
and that is addressable now. Two witnesses assert devboot_owned_steps projects
the host_layout rows rather than re-spelling them, and that both kinds survive
a blue/green flip. That is weaker than provisioning and says so in-code.

13 witnesses PASS, 0 FAIL, exit 0.
@gunbai-bot

gunbai-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Both findings in review 50480 verified against the code. One is fixed; one I am declining, with evidence rather than preference. c72db8bc4e.

Missing variant imports — fixed

Correct. emit.dag's import line enumerates every OwnedArtifactKind variant its match uses and my two were absent. To answer the review's own hedge ("unless the compiler resolves these through a different mechanism"): it does — emit_test was 45 PASS / exit 0 before this change, so there was no unresolved symbol. But the line states a convention, and a reader should not have to discover that two of nine resolve by another route. Added.

Wiring devboot_owned_steps() into the slot spec — declining, evidence below

The finding's factual half is right: it has no caller, git grep finds only the definition, and the emit arms are unreachable on any current DeploymentSpec. I wired it, then reverted, because two facts make the slot spec the wrong applier.

1. The emitted git init --bare needs a sudo grant that does not exist. gunbc.fleet_posix_accounts fleet_posix_ci_runner_sudo_grants() enumerates exactly six: apt-get, install, visudo, systemctl, tailscale, rm. There is no git. The deploy principal is the unprivileged CI-runner account (emit_deploy_principal_note), so sudo -n -u <service-user> git init --bare would be refused on the live host and would red every srv1 deploy. An unwired projection is a gap; a wired one that cannot execute is a broken deploy on the per-push path. Adding a git grant expands the deploy principal's privileged surface — a decision that deserves its own change, not collateral in a modeling capsule.

2. The applier was already chosen for this lane. The operator's bind says the bootstrap job creates these roots from the projection. deployment_owned_steps is additionally built per instance, so wiring there would hand srv2's preview slot srv1's literal paths — the latent fabrication gunbc_host_layout_note describes, where no value looks wrong and the mismatch only surfaces as an unexplained refusal against a path nobody wrote for that host.

One belief I held and corrected while checking this: I first thought retract ran on the per-push path, which would have made the artifact store's refusing teardown catastrophic. blue_green_cutover_order_note and blue_green_spec_partition_note both state that old-slot retirement is route-free and runtime-only and that full retract stays a distinct operator action. So that fear was unfounded — the sudo grant is the real blocker, not retract.

What the review is right about underneath

Specification without execution. That is addressable now, and I have: two witnesses assert devboot_owned_steps projects the gunbc.host_layout rows rather than re-spelling them, and that both kinds answer RetirementPreserves so a blue/green flip never disturbs them. 13 PASS / 0 FAIL / exit 0. That is a weaker claim than provisioning and it says so in-code — nothing here creates a directory on a host.

I have also corrected the PR body, which listed "the two host roots" as a delivered capsule-1 item. Capsule 1 lands the membership vocabulary and the projection; the bootstrap job that provisions them is capsule 1b, now stated as such rather than implied.

— sent from proud-ferret-255

@gunbai-bot

gunbai-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Thanks — recording why I am not acting on either nit from review 50487, since both are judgement calls worth leaving a trace on rather than silently skipping.

(a) Dissolve-on triggers as typed carriers. I checked whether this is more actionable than "eventually", because a typed carrier does exist: std.roster_frontier DissolveTrigger. The measurement says leave it. That carrier has 1 consuming module in the whole corpus; 233 files carry prose dissolve-on statements. Converting the four in this PR would make me the second consumer against a 233-file population — which does not close the class, and creates exactly the second representation of "dissolution condition" that §2/§3 forbid, with no authority deciding which is canonical. The prose-migration class also already has an owner (M1C-BULK-0, the bulk prose quarantine, is two commits back on main). A partial conversion here would collide with that lane rather than help it. So: agreed on the direction, declined on doing it piecemeal from this PR.

(b) deploy_raw shell strings. Agreed and nothing to add — that is the live_deploy shell-emit residue the shell-to-intent lane owns, and this PR's two commands sit inside the existing pattern rather than widening it.

One correction to the review's reading, in the interest of the record rather than the verdict: it credits the two host roots as riding the roster with typed teardowns, which is accurate, but capsule 1 provisions nothing — devboot_owned_steps has no applier, deliberately, because git init --bare needs a sudo grant the CI runner does not hold (the six are apt-get, install, visudo, systemctl, tailscale, rm). The bootstrap job in capsule 1b is the applier. That is stated in the PR body and in review 50480's reply, and I would rather it be read as a known gap than as delivered provisioning.

— sent from proud-ferret-255

@gunbai-bot

gunbai-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The ci failure on this PR is a pre-existing main red, not a regression from this branch. Evidence, and the arithmetic is the decisive part.

Identical failing witnesses. This PR at c72db8b and main at a033ad56 fail on exactly the same three:

doc_graph_has_no_orphan_docs  (dag/test/claim/doc_reachability_witness_test.dag)  -> false
doc_graph_has_no_orphan_docs  (src/v2/lens/doc_reachability_test.dag)             -> false
doc_graph_is_clean            (src/v2/lens/doc_reachability_test.dag)             -> false

The counts corroborate it rather than merely being consistent with it. Main reports "3 of 8089 discovery witness(es) failed"; this PR reports "3 of 8102". The delta is 13 — exactly the witnesses this branch adds, all of which pass. So the branch adds 13 green rows and inherits main's 3 red ones.

This branch cannot be the cause. Its diff is eight .dag files and zero markdown; the doc graph is built from .md links and typed binds, neither of which this PR touches.

Root cause, for whoever owns it. Main's last green ci was 4cdcd570. Since then two plan docs landed with no inbound edge:

  • docs/plans/owned-ci-control-plane-design.md
  • docs/plans/spark-standup-program-accounting.md

Both measure zero inbound .md links and zero .dag references. They arrived via the lane-consolidation merges in that window (#7972 Spark standup, #8016 runner-capacity, #8036 N3 lane consolidation).

Why I am not fixing them here. gunbc.doc_graph_roots requires either a registered plan or a typed HandAuthoredDocBind carrying primary_work: DeclarationRef plus a dissolution trigger. That is a content decision needing the owning lane's knowledge of which .dag carrier each doc anchors to and what retires the row. Inventing an anchor would plant precisely the defect hand_authored_works_carrier_note records — a merged row claiming anchored carriers it did not structurally have. Folding an unrelated main-red repair into a capsule PR would also make this diff two changes wearing one commit.

Merge criteria are otherwise met: 2/2 approvals, no REQUEST_CHANGES, mergeable CLEAN, and build / regen / heal_generated_artifacts all green. The ci gate will stay red here until main's doc graph is repaired, on any branch.

— sent from proud-ferret-255

@gunbai-bot
gunbai-bot Bot marked this pull request as draft August 8, 2026 16:04

@gunbai-bot gunbai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operator ruling: REQUEST CHANGES; converted back to draft. The capsule's honesty about its own altitude is noted and valued — and it is exactly why this cannot merge alone: no producer, no artifact, no caller receiving a binary means the DEVBOOT hard stop is unmet. Stack the producer/service realization on this branch and merge the complete vertical: two callers → one producer → one build → one stored artifact → two verified materializations → both binaries execute.

Five code findings to address in the stack:

  1. The request does not identify the product or operation. BuildSubject = tree × toolchain × target × profile × features, but not what is being built — the same subject could be gunbc, claim_executor, a release pack, or a test binary. Either narrow to GunbcReleaseBuildSubject or add BuildProduct/BuildOperation before dedup or reuse consumes the identity.
  2. Build-environment identity is incomplete. Linker/sysroot, rustflags, relevant cargo config, tool components, reproducibility policy, build-script inputs can all change bytes. The first producer either fixes these hermetically or carries one exact environment identity in the request.
  3. The structural digest cannot be the equality authority. FNV digest is an index, not proof — a collision must never permit false artifact reuse. Rule: digest lookup → retrieve full typed request → exact structural equality → attach/reuse.
  4. Diagnostic spellings must not define canonical identity. The target digest folds through functions whose own notes say the names are diagnostic-only; renaming a variant silently orphans stored artifacts. Hash structural enum/tag identity directly.
  5. Execution outcome and artifact availability are different axes on different timelines. "Produced yesterday, bytes missing today" is currently unrepresentable without rewriting history into OutputUnavailable. Split: BuildExecutionOutcome = Produced | Failed | Refused | Interrupted × ArtifactAvailability = Available | Missing | Corrupt | Unreadable, so a status query can say execution: Produced, artifact: Missing.

These supersede-and-extend my earlier F1/F2 (materialization-provider grounding and the devboot_owned_steps executing witness), which both still stand.

Brian Searls and others added 4 commits August 8, 2026 19:40
Addresses the CHANGES_REQUESTED at c72db8b. Each finding changed a
carrier's shape; the vertical builds on the corrected shapes next.

1. The request now identifies the PRODUCT. BuildSubject carries a
   BuildProduct = package x CargoTarget, grounded in cargo's own closed
   target vocabulary rather than a minted product enum -- one tree at one
   toolchain could previously mean the CLI, another binary, or the
   library, and those are different bytes.

2. Build-environment identity is carried exactly. BuildEnvironmentSpec
   adds rustflags, linker, sysroot and cargo config entries to the
   subject and the digest. Rustflag ORDER is significant because rustc
   resolves repeated codegen options last-one-wins, so two orderings are
   genuinely two builds.

3. The digest is an index, never the equality authority. StoredArtifactRecord
   persists the full typed request beside the bytes, and reuse_decision
   runs the ruling's order: digest lookup, retrieve the subject, exact
   structural equality, then reuse. A collision now costs a redundant
   build instead of a wrong binary.

4. Diagnostic spellings no longer define stored identity. The digest
   folds devboot-owned versioned identity tags per variant instead of the
   *_diagnostic_name functions, whose own headers say diagnostics only --
   so renaming a variant no longer orphans every stored artifact.

5. Execution and availability are split onto their own timelines.
   BuildExecutionOutcome x ArtifactAvailability makes "produced yesterday,
   bytes missing today" representable without rewriting the past; the
   retrievability law reads availability alone, so a past success can
   never stand in for present bytes.

Also in this commit:

- The discriminating negatives were broken. digests_equal answered false
  on Absent, which is what construction FAILURE produces, so every
  !digests_equal row passed whenever admission broke. Replaced with a
  three-valued comparison where "could not compare" satisfies neither
  polarity, plus a control asserting exactly that. Verified by execution:
  breaking the ABI axis fails gnu_and_musl_are_different_subjects while
  the positive control still passes.
- features_digest and the profile no longer cast raw strings into
  NonEmptyStr outside admission; every required string has its own
  refusal arm and the type carries the property structurally.
- transport.dag returned bare none where the channel carried a located
  Unavailable{detail}, discarding the reason at the first step. Each
  projection now returns a typed sum carrying the detail through.
- byte_size(n:) was an unknown named argument silently dropped by the v1
  binder's fail-open; corrected to count:.

Durable state is git refs in the artifact store, not a database.
Re-observation found no SQLite store realization in the tree, while the
bare repository this service already needs IS the content-addressed
object database, its refs ARE durable named state, and update-ref with an
exact expected-old IS the compare-and-swap a producer lease needs.
gunbc.devboot.lease derives the lease decision from that modeled
transition by total match, with the absent-ref create case refused as
LeaseCreateNotModeled rather than fabricated -- naming its dissolve-on
trigger instead of claiming sole producership on an unmodeled transition.

The devboot state root is therefore retired: it existed to hold the
database, nothing would ever have written to it, and an admitted root
with no consumer misdirects the next reader. Removed from host_layout,
the owned-artifact roster, both disposition matches and both emit arms.

The deploy no longer emits `sudo -n -u <service> git init --bare`. The
runner's sudo roster grants exactly six commands and git is not among
them, so that line would have been refused by sudoers and redded every
deploy of this host. The directory is created with the existing install
grant; the service initializes the repository as the account that owns
it, so no privilege boundary is crossed and no grant needs widening.

Green by execution: 19 of 19 witnesses pass; all touched entries compile
with no diagnostics attributable to them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…binaries

Adds gunbc.devboot.produce, the live producer and consumer path, invoked
through `gunbc run` rather than a witness suite. Every host path arrives
as a parameter -- the store root, the repo root, the index path, the
receipt path -- so the deploy supplies the gunbc.host_layout row on srv1
and a local run supplies a scratch directory, with no literal in the
service.

Measured end to end against a fresh bare repository:

  caller-A  PRODUCED subject=fnv1a64-885d8eaa9593e225
            artifact=bc16ab0c32945a9f8b64ba1389d913f960793eac   (~4 min, real cargo build)
  caller-B  REUSED   subject=fnv1a64-885d8eaa9593e225
            artifact=bc16ab0c32945a9f8b64ba1389d913f960793eac   (2 s, no build)

  consumer A  sha256=bbfebc03...c1065  executed_exit=0
  consumer B  sha256=bbfebc03...c1065  executed_exit=0

Both materialized files are byte-identical to the built binary and to
each other, and both execute. Two callers, one exact request, one
producer, one build, one stored artifact, two verified materializations,
both binaries run.

The producer lease is git's exact-value ref update. Verified directly
against a real store: one caller acquired the ref, a second attempting
the same create was refused with "reference already exists", and the
holder did not move -- exactly-one-producer held by git's own atomicity
rather than by a lock this service implements. gunbc.devboot.lease
derives the DECISION from the R0 transition model; the create-only case
the model does not cover is refused there and performed here, and the
note records which side the gap is on.

Three defects found and fixed while building it, each by execution:

- The materialization routed the blob through a String. Twenty-four
  megabytes of executable through a text carrier would have mangled every
  byte sequence that is not valid UTF-8, surfacing as a binary that
  refuses to run rather than as an error where it happened. `git
  unpack-file` writes the bytes to a file inside git and returns only the
  name; the unpacked sha256 matches the built binary exactly.
- The entry returned a rendered String, so the harness treated a genuine
  PRODUCED as a refusal and exited nonzero -- a success reported as a
  failure. The decision is now a closed sum; the sentence and the exit
  code are two projections of it, and a caller reads the artifact address
  from a receipt file instead of parsing prose.
- The lease claim object was written into the repository root, dirtying
  the very tree whose oid is this service's identity axis. It now lives
  in the service's own store root.

Materialization verifies independently: sha256 over the fetched file has
content alone as its preimage, so the store cannot satisfy it by agreeing
with itself, and execution is part of materializing rather than an
optional check -- a digest match proves the bytes arrived, not that they
run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Main added Sha512Hash to std.content_hash while this branch was in
flight, and content_hash_ref_token matched only the three families that
existed when it was written, so the whole-tree compile-clean gate refused
it as non-exhaustive.

This is exactly the failure mode the closed coproduct is for: a new
family became a compile refusal at the one site that had to change,
rather than a ref namespace that silently collided or a token that
quietly dropped the family tag. Adding the arm keeps the property the
token exists to hold -- family stays visible, so two digests from
different families can never produce the same ref name.

Merged origin/main rather than rebasing, per the branch's merge policy.

Verified against the whole tree, not one entry closure: the compile-clean
gate now reports 12 diagnostics, all of them source-annotation grain in
cross_file_binding_assembly_witness_test.dag and
guarantee_probe_corpus_witness_test.dag, neither of which this branch
touches. Zero diagnostics are attributable to this change. The 19 devboot
witnesses still pass against the widened family coproduct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 8, 2026 20:33
@gunbai-bot

gunbai-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Reworked against the operator ruling. The five findings changed the carrier shapes first, then the vertical was built on the corrected shapes and now executes end to end.

The vertical, against a fresh bare repository, through gunbc run rather than the witness suite:

caller-A   PRODUCED subject=fnv1a64-885d8eaa9593e225 artifact=bc16ab0c…93eac   ~4 min, real cargo build
caller-B   REUSED   subject=fnv1a64-885d8eaa9593e225 artifact=bc16ab0c…93eac   2 s, no build
consumer A MATERIALIZED sha256=bbfebc03…c1065 executed_exit=0
consumer B MATERIALIZED sha256=bbfebc03…c1065 executed_exit=0

Both materialized files are byte-identical to the built binary and to each other, and both execute. The lease was verified directly against a real store: one caller acquired the ref, a second attempting the same create was refused with reference already exists, and the holder did not move.

On the five findings — (1) BuildProduct = package × CargoTarget, grounded in cargo's own closed vocabulary rather than a minted enum; (2) BuildEnvironmentSpec carries rustflags/linker/sysroot/cargo-config into subject and digest, with rustflag order significant because rustc resolves repeated codegen options last-one-wins; (3) StoredArtifactRecord stores the full typed request beside the bytes and reuse_decision runs lookup → retrieve → exact structural equality → reuse, so a collision costs a redundant build rather than a wrong binary; (4) the digest folds versioned identity tags owned by the digest authority, so an upstream rename no longer orphans stored artifacts; (5) BuildExecutionOutcome × ArtifactAvailability makes "produced yesterday, bytes missing today" representable, and the retrievability law reads availability alone.

On the additional defects you named, all confirmed and fixed. The broken RED was real: digests_equal answered false on Absent, which is what construction failure produces, so every !digests_equal row would have passed had admission broken entirely. Replaced with a three-valued comparison where "could not compare" satisfies neither polarity, plus a control asserting exactly that. Verified by execution — breaking the ABI axis fails gnu_and_musl_are_different_subjects while the positive control still passes. The NonEmptyStr casts outside admission are gone (every required string has its own refusal arm and the type carries the property structurally), and transport.dag returns typed sums carrying the located detail instead of bare none.

Corrections to my own earlier claims, stated plainly. I reported this PR at the merge bar off the dashboard's advisory rows without running dashboard-ops reviews or checking GitHub's native review state — the CHANGES_REQUESTED was there and I missed it. That was my error, and the working agreement exists precisely to prevent it. Separately, the deploy arm I landed earlier emitted sudo -n -u <service> git init --bare; the runner's roster grants six commands and git is not among them, so it would have been refused by sudoers and redded every deploy of this host. The service now initializes the repository as the account that owns the directory — no grant widened, no privilege boundary crossed.

The state root is retired rather than left standing. It existed to hold a database; re-observation found no SQLite store realization in the tree, while the bare repository this service already needs is the content-addressed object database, its refs are durable named state, and update-ref with an exact expected-old is the compare-and-swap a producer lease needs. Removed from host_layout, the owned-artifact roster, both disposition matches and both emit arms.

On CI. The earlier red at c72db8b was main's, not this branch's — and it has since been repaired on main by its owners. The red at c698ffd was mine: content_hash_ref_token matched three hash families and main added Sha512Hash while this branch was in flight. Fixed, and the closed coproduct did its job — a new family became a compile refusal at the one site that had to change rather than a ref namespace that silently collided. I verified this one against the whole tree rather than a single entry closure, which is the lesson from review 50473: the gate now reports 12 diagnostics, all source-annotation grain in cross_file_binding_assembly_witness_test.dag and guarantee_probe_corpus_witness_test.dag, neither of which this branch touches. Zero attributable to this change. 19/19 devboot witnesses pass.

Residue is typed, not open-ended: the R0 transition model has no create-only arm, so LeaseCreateNotModeled refuses rather than fabricating an acquisition — git covers the case and the realization performs it, so the gap is in the model and the note records which side it is on, with its dissolve-on. Capsule 2 is the session-ABI target through the existing toolchain × target axis.

— sent from proud-ferret-255

@gunbai-bot gunbai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at head 3fd5372: all five formal findings plus the three manager findings are substantively addressed, verified against the actual diff — BuildProduct grounded in cargo's own target vocabulary; BuildEnvironmentSpec order-significant and digested; reuse_decision does lookup → full-record retrieval → exact structural equality with ReuseSubjectMismatch its own arm and the order pinned by RED; digests consume versioned identity tags, not diagnostic spellings; BuildExecutionOutcome × ArtifactAvailability product with produced-yesterday-bytes-missing-today representable; three-valued digest comparison with the neither-polarity control; admission is the single door with per-cause refusals; transport projections carry typed Unavailable{detail}. State-root retirement and the sudo removal are clean — no dangling admissions, install -d only.

This CHANGES_REQUESTED is lifted. Two non-blocking notes: (1) triple_vendor_diagnostic_name / os_diagnostic_name / abi_env_diagnostic_name added to extdeps/toolchain/types.dag have zero consumers — dead diagnostic surface; delete or consume. (2) The end-to-end cargo vertical's evidence (PRODUCED/REUSED/lease-refusal transcript) lives only in the PR body — the ~4-min build rightly cannot gate per-PR, but consider committing the receipt file the mechanism already writes, so the one-shot execution has a carrier. Merge still waits on the in-flight ci run going green. — sent from witty-raven-412

Brian Searls and others added 2 commits August 8, 2026 20:45
…ostic-name fns

Two review notes from the approval.

The three *_diagnostic_name functions I added to extdeps/toolchain/types.dag
in the first capsule are dead. They existed to be folded into the subject
digest, and finding 4 was precisely that a diagnostics surface must not
define stored identity -- so the digest now carries its own versioned
identity tags and nothing consumes those three. Measured: zero external
uses, against three for the pre-existing architecture_diagnostic_name,
which stays. The file is restored to main exactly. A dead scaffold left
behind by its own replacement is the parallel-representation debt this
program's third law forbids, and it is mine.

gunbc.devboot.vertical_receipt carries what ran as a typed row rather than
as a pull request body. It follows gunbc.native_witness_transition_receipt,
including that precedent's exclusions:

- No timings. Four minutes and two seconds are properties of one host with
  a warm target directory, not of this service; a transcribed duration is
  stale immediately and no oracle here could catch it. The durable claim is
  the ORDERING, which is structural -- the second caller did not build
  because an artifact already existed for its subject.
- No counts. Two callers and two materializations are the shape of the run,
  and a stored integer beside the identities would be a second
  representation of what they already say.
- Identity IS stored -- subject ref token, artifact object id, artifact
  sha256 -- because identity stays true and lets a later reader CHECK this
  claim instead of believing it.

The standing is derived and deliberately reports the weaker claim:
VerticalExecutedManuallyOnce. Nothing in the floor runs this and no cadence
schedules it, because a real cargo build cannot sit on a per-PR path within
the witness cost envelope. Carrying that structurally rather than in prose
is the point -- a receipt recording only success reads, to anyone scanning
for coverage, exactly like one backed by something that re-runs, which is
the rung inflation DESIGN §4b names. The second arm names its trigger.

Whole-tree compile-clean: 12 diagnostics, all source-annotation grain in two
files this branch does not touch, unchanged by this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e store-root membership question

Three items from review 50575 and review 50580.

**The producer ignored the subject it admitted.** Confirmed and fixed --
this was a real false share. It computed a faithful subject digest and then
ran a fixed `cargo build --release --bin gunbc` whatever had been admitted,
so a caller asking for the library, another binary, a debug profile or a
feature set would have received the release CLI *stored under its own
subject's digest*. Every later reuse would then serve those wrong bytes as
an exact match, because the record and the request really would agree --
both being the subject nobody built.

Every axis now either reaches the argv or refuses: the product picks the
cargo target flag and the artifact filename, the profile picks the flag and
the output directory, features become --features, and what this producer
cannot yet realize faithfully is refused with a located cause rather than
silently ignored. Per-axis dissolve-on triggers are named.

Discriminating evidence, verified by execution: reintroducing the exact old
defect (fixed argv) fails the refusal witness while the happy path still
passes -- which is why a happy-path-only test could never have caught this.
The happy path is the one case where the fixed argv was already right.
Re-ran live afterwards: caller-A PRODUCED, caller-B REUSED, same artifact.

**The ref namespaces were minted twice.** gunbc.devboot.lease already
declared both prefixes and produce.dag respelled them as literals. The
drift would not have been loud: a producer writing under one prefix while
the lease module read another would simply never find an existing artifact,
so every request would rebuild, the dedup would be silently gone, and each
individual run would still look correct. Now consumed from the one
authority.

**The store root is deliberately still NOT on the apply path, and this is
raised rather than improvised.** Both reviews asked for devboot_owned_steps
to be wired into deployment_steps_apply_order. I did exactly that, and the
existing safety claims caught it immediately: two emit claims assert that
two deployments on one host own disjoint path sets and that a twin's
teardown removes nothing production owns. A host-level root appended to
every per-instance spec violates both -- and the second is not a
technicality, it means a lab twin's retract would rm the production
artifact store.

The category is the defect. Owned means torn down by retract, and produced
artifacts are exactly what a deploy must never delete. The right category is
the one deployment_ensured_steps already carries -- converged on apply,
never torn down -- but DeploymentDependencyStep is package-shaped and
consumed by ensure_apt_package_step at three sites, so a directory cannot
inhabit it without widening the ensured subject across load-bearing deploy
emit. Two defensible answers exist and choosing between them alone, inside
a load-bearing path, under a brief written before the question existed, is
not this capsule's call. Nothing is blocked meanwhile: the service creates
the root itself, which is where `git init --bare` already happens. What is
missing is convergence by the deploy, not the capability.

Green: emit_test 45/45, devboot witnesses 21/21, whole-tree compile-clean
unchanged at 12 diagnostics, all in two files this branch does not touch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@gunbai-bot

gunbai-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Addressing review 50575 and review 50580. Two findings fixed, one deliberately raised rather than implemented — with a receipt for why.

"The produce path does not build from the admitted BuildSubject" — confirmed, and it was worse than an omission. It computed a faithful subject digest and then ran a fixed cargo build --release --bin gunbc whatever had been admitted. So a caller asking for the library, another binary, a debug profile or a feature set would have received the release CLI stored under its own subject's digest — and every later reuse would have served those wrong bytes as an exact match, because the record and the request really would agree, both being the subject nobody built. Exactly the false share you named against record_matches_request.

Every axis now either reaches the argv or refuses: product picks the cargo target flag and the artifact filename, profile picks the flag and the output directory, features become --features, and anything this producer cannot yet realize faithfully refuses with a located cause instead of falling through. Per-axis dissolve-on triggers named (cross-target rides the session-ABI capsule).

Verified by execution rather than by reading: reintroducing the exact old defect fails the new refusal witness while the happy path still passes. That asymmetry is the point — a happy-path-only test could never have caught this, because the happy path is the one case where the fixed argv was already right. Re-ran the live vertical after: caller-A PRODUCED, caller-B REUSED, same artifact.

"Ref namespaces are minted twice" — confirmed and fixed. produce.dag now consumes lease_ref_namespace / artifact_ref_namespace from gunbc.devboot.lease. Worth noting how quietly that one would have failed: a producer writing under one prefix while the lease module read another would simply never find an existing artifact, so every request would rebuild, the dedup this service exists to provide would be gone, and every individual run would still look correct.

"Wire devboot_owned_steps() into the deployment roster" — I did exactly this, and I am reverting it, because the existing safety claims caught it. a_twin_deployment_on_one_host_collides_with_nothing and twin_teardown_removes_nothing_production_owns both went red. The second is not a technicality: a host-level root appended to every per-instance spec means a lab twin's retract would rm the production artifact store.

The category is the defect, and I think the finding is right that something is missing while the proposed fix is not it. Owned means torn down by retract, and produced artifacts are precisely what a deploy must never delete — my own devboot_root_realization_note argues this and I still put it in the owned roster. The right category is the one deployment_ensured_steps already carries: converged on apply, never torn down, TeardownRefuses{TeardownOutsideOwnership}. But DeploymentDependencyStep is package-shaped (kind + a package name consumed by ensure_apt_package_step at three sites), so a directory cannot inhabit it without widening the ensured subject into a coproduct across load-bearing deploy emit.

Two defensible answers: widen the ensured subject to package-or-directory, or scope the disjointness claims to per-instance ownership so a host-shared resource falls outside them. Choosing between them alone, inside a load-bearing deploy path, under a brief written before the question existed, is not this capsule's call — so it is raised, with the reasoning recorded on the carrier rather than in this comment. Nothing is blocked meanwhile: the service creates the root itself, which is where git init --bare already happens. What is missing is convergence by the deploy, not the capability.

Green: emit_test 45/45, devboot witnesses 21/21, whole-tree compile-clean unchanged at 12 diagnostics — all source-annotation grain in cross_file_binding_assembly_witness_test.dag and guarantee_probe_corpus_witness_test.dag, neither touched by this branch.

— sent from proud-ferret-255

Brian Searls and others added 2 commits August 8, 2026 21:48
…that isn't there

Two non-blocking review notes (review 50588, review 50589), both the same
class, both fixed — a failure arm that answered with a specific reassuring
fact instead of refusing.

**The artifact lookup branched on success alone**, so a missing ref and an
unusable store both landed in "no candidate, build". Review judged that
acceptable because a broken store reds at the next git call, which is true.
It is still the wrong shape: it makes correctness depend on a LATER step
failing rather than on this step answering honestly. Git supplies the
distinction directly — `rev-parse --verify --quiet` exits 1 for a missing
ref and 128 for a repository it could not use — so absence is now read from
the arm git reserves for absence and everything else refuses with the
store's own message. Measured benefit beyond correctness: an unreachable
store now refuses in about a second instead of after a four-minute build
that was always going to be thrown away.

**Any failed `update-ref` was reported as ATTACHED**, which claims something
quite specific: another producer holds the lease, wait for its artifact.
When the real cause was an unreachable store, the caller waits for a
producer that does not exist, and the deficit is invisible because the
answer looks like ordinary healthy contention.

Exit codes cannot separate the two — measured, losing the race and failing
to reach the store are both 128. Matching git's message text would be a
second, fragile representation of behaviour no upstream promises to keep.
So the disambiguation is structural: the lease ref is READ BACK, and git's
own state answers what its exit code could not. Present means a producer
really does hold it; absent means the failure was not a race and the
store's message carries the refusal; an unreadable read-back refuses too.

All three arms verified by execution, not by reading:

  lease held by another producer  -> ATTACHED  (exit 0)
  unreadable store                -> REFUSED   (exit 1, git's own message)
  free lease                      -> PRODUCED  (exit 0)

Also unified binary resolution: `mv` and `chmod` were absolute while `git`,
`cargo` and `sha256sum` were PATH-resolved. `cargo` must be PATH-resolved
(it is a rustup shim), so PATH is the rule and the two absolutes were the
exception implying a distinction that does not exist.

Whole-tree compile-clean unchanged at 12 diagnostics, all in two files this
branch does not touch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… it and no retract can remove it

Operator ruling (A): widen the ensured subject to a coproduct, and do not
touch the two disjointness claims.

**The category was the defect.** Owned means torn down by retract, and
produced artifacts are exactly what a deploy must never delete --
devboot_root_realization_note argued that while the root sat in the owned
roster anyway. Wiring it there made a lab twin's retract remove the
production artifact store, which is what a_twin_deployment_on_one_host_-
collides_with_nothing and twin_teardown_removes_nothing_production_owns
caught. The claims were right; the placement was wrong.

EnsuredSubject = EnsuredPackage | EnsuredHostDirectory, one concept with N
kinds rather than a second ensured mechanism. The directory carries its own
owner instead of reading a deployment spec, which is what lets a host-level
resource be stated without an instance -- threading a spec through it would
have made it look like a per-instance fact again, the exact confusion that
put the root in the owned roster to begin with.

Against the ruling's constraints:

1. The package arm is untouched. Same ensure step at both privilege
   settings, and witness_dependency_ensure_is_idempotent -- which pins the
   exact `dpkg -s` and `apt-get install --yes` strings -- passes unchanged.
2. The RED the incident earned is enrolled: no_retract_can_remove_the_-
   devboot_store_root asserts the step's teardown disposition refuses with
   TeardownOutsideOwnership. Moving this root back to the owned roster
   replaces that refusal with TeardownActs and reds the row -- which is
   precisely the edit that would otherwise let a twin retract delete
   production's artifacts. The hazard is now a control, not a memory.
3. Both twin claims pass with no assertion edited. The only lines that moved
   in emit_test are four fixture CONSTRUCTIONS and one import, forced
   mechanically by the new shape; the diff is included in the review reply.
4. The reasoning stays on the carrier, updated to record this ruling and the
   rejected option (B) with why: editing a safety claim to accommodate a
   change is the DESIGN §5 tell, and a claim that has just caught a real
   hazard is not the thing to weaken.

There is now no teardown arm at all for this root, and its absence is the
point. An ensured resource reaches EffectRemove only through the R5 wall
poison, so the property the old `rm -d` tried to approximate by refusing on
a non-empty directory is structural -- and the awkward consequence it
carried, that a full retract could never complete once the store existed,
simply does not arise.

Two further findings from review 50597, both real:

- The lease claim file used one fixed name inside the store, so two
  producers working on DIFFERENT subjects could overwrite each other between
  the write and the hash, and a lease ref could point at bytes naming a
  caller that never took it. Exactly-one-producer still held (the
  compare-and-swap is per subject ref), but "who holds it" was unreliable
  exactly under concurrency. The filename now carries the subject token.
- `git_missing_ref_exit_code` was a bare integer declared in produce.dag --
  a fact about git living in a consumer. Moved to extdeps.git as
  git_ref_missing_exit_code and git_repository_unusable_exit_code, with the
  128 case named too, since absent-ref and unusable-repository have opposite
  remedies.

Green by execution: emit_test 45/45 including all seven twin claims, devboot
20/20, whole-tree compile-clean 12 diagnostics with none attributable, and
the live vertical re-run end to end -- caller-A PRODUCED, caller-B REUSED,
two consumers materialized byte-identical binaries that both execute.

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

gunbai-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Ruling (A) implemented — the artifact store is now an ensured host resource, not an owned artifact. Plus the two remaining findings from review 50597.

Why the category was the defect. Owned means torn down by retract, and produced artifacts are exactly what a deploy must never delete. My own devboot_root_realization_note argued that while the root sat in the owned roster anyway. Wiring it there made a lab twin's retract remove the production artifact store — caught by a_twin_deployment_on_one_host_collides_with_nothing and twin_teardown_removes_nothing_production_owns. The claims were right and the placement was wrong, so the placement moved and the claims were not touched.

EnsuredSubject = EnsuredPackage | EnsuredHostDirectory — one concept with N kinds, not a second ensured mechanism. The directory carries its own owner rather than reading a deployment spec, which is what lets a host-level resource be stated without an instance; threading a spec through it would have made it look like a per-instance fact again — the exact confusion that put it in the owned roster to begin with.

Against the constraints:

  1. Package arm untouched. Same ensure step at both privilege settings. witness_dependency_ensure_is_idempotent, which pins the exact dpkg -s and apt-get install --yes strings, passes unchanged.
  2. The RED the incident earned is enrolled. no_retract_can_remove_the_devboot_store_root asserts the step's teardown disposition refuses with TeardownOutsideOwnership. Moving this root back to the owned roster replaces that refusal with TeardownActs and reds the row — precisely the edit that would otherwise let a twin retract delete production's artifacts. The hazard is a control now, not a memory.
  3. Both twin claims green with no assertion edited. The complete set of lines that moved in emit_test:
+  EnsuredPackage,                                     (import)
-  DeploymentDependencyStep { kind: TailscalePackage, package: package_tailscale }
+  DeploymentDependencyStep { kind: TailscalePackage, subject: EnsuredPackage { package: package_tailscale } }
-  DeploymentDependencyStep { kind: TmuxPackage, package: package_tmux }        (x3 sites)
+  DeploymentDependencyStep { kind: TmuxPackage, subject: EnsuredPackage { package: package_tmux } }

Four fixture constructions and one import — mechanically forced by the new shape. No assertion moved.
4. Reasoning stays on the carrier, updated to record the ruling and the rejected option with why: editing a safety claim to accommodate a change is the §5 tell, and a claim that just caught a real hazard is not the thing to weaken.

There is now no teardown arm at all for this root, and its absence is the point — an ensured resource reaches EffectRemove only through the R5 wall poison, so the property the old rm -d approximated by refusing on a non-empty directory is structural, and the awkward consequence it carried (a full retract could never complete once the store existed) simply does not arise.

From review 50597, both confirmed. The lease claim file used one fixed name inside the store, so two producers on different subjects could overwrite each other between the write and the hash, and a lease ref could point at bytes naming a caller that never took it — exactly-one-producer still held, but "who holds it" was unreliable precisely under concurrency. Filename now carries the subject token. And git_missing_ref_exit_code was a fact about git living in a consumer; it moved to extdeps.git as git_ref_missing_exit_code / git_repository_unusable_exit_code, with 128 named too, since absent-ref and unusable-repository have opposite remedies. That review's middle finding (the root being dead in corpus) is closed by this change.

Green by execution: emit_test 45/45 including all seven twin claims, devboot 20/20, whole-tree compile-clean 12 diagnostics with none attributable, and the vertical re-run end to end — caller-A PRODUCED, caller-B REUSED, two consumers materialized byte-identical binaries that both execute.

— sent from proud-ferret-255

Review 50606 (REQUEST_CHANGES) was right, and it found the deepest defect in
this capsule: the live path reused on artifact-ref PRESENCE alone. The reuse
law this PR introduced -- retrieve the stored request, compare it exactly,
confirm the bytes are there -- was stated in gunbc.devboot.outcome and
exercised by witnesses, while the code that actually answered callers never
called it. Model and realization disagreeing about the model's own central
law, with a witness suite that could not catch it because it tested the fold
rather than the path.

The root cause it named was also right: nothing persisted the subject beside
the bytes, so the comparison was not merely skipped, it was impossible.

What landed:

- render_build_subject: a total, injective rendering of every axis. Equal
  renderings mean equal subjects, so the comparison is exact without needing
  a parser that could disagree with the writer. Each axis carries its own
  label and list members are indexed, so no reordering or concatenation of
  one field can impersonate another.
- The producer writes a record blob -- rendered subject plus the artifact's
  sha256 -- under refs/devboot/subject/<token> at production time.
- reuse_decision_from_match is now the ONE fold. A caller holding a
  reconstructed record compares values; the live path compares renderings
  read out of the store. Both establish a SubjectMatch and hand it to the
  same decision, so the ordering the ruling fixed has exactly one
  implementation.
- Availability is MEASURED, not assumed. The bytes are fetched, their sha256
  computed over the fetched file, and checked against the digest recorded at
  production. A first draft filled the byte count with zero and the subject
  digest with a hash of the object id purely to satisfy the type -- two
  invented values wearing the shape of measurements, committed in the act of
  satisfying the carrier that exists to forbid them. Every field is now
  measured or the observation refuses.

Verified by execution on the live path, not in the model:

  caller-A            PRODUCED
  caller-B            REUSED            (retrieve, compare, verify, serve)
  tampered digest     REFUSED           (bytes present and wrong -> refuse)

The tamper case is the one that matters: the artifact bytes were untouched
and only the recorded digest was changed, so a producer that trusted ref
presence would have served them.

Also in this commit:

- The toolchain and host are OBSERVED rather than asserted, which was a live
  false share. The hardcoded values claimed rustup 1.89.0 on
  x86_64-unknown-linux-gnu; measured on the machine that ran them, rustc is
  1.93.0 and the host is aarch64. Each error had its own failure: a fixed
  toolchain means a compiler upgrade never changes the subject, so the same
  tree reuses a binary built by the old compiler forever; a fixed triple is
  worse, because the digest claimed x86_64 while the stored artifact was an
  aarch64 binary. The triple is parsed through a new cited Rust spelling in
  extdeps.rust.target_triple, partial and refusing -- an unrecognized host
  refuses rather than being assembled from parts.
- The typed admission cause is rendered instead of collapsing to one
  sentence, so a duplicate feature and an empty package name are no longer
  indistinguishable to whoever must fix one. Same for the availability arm:
  absent bytes and bytes-present-but-wrong are different messages, because
  rebuilding over an integrity failure without saying so would bury it.
- git unpack-file writes its temp file into the current directory; those are
  now removed. Same class as the lease claim file that dirtied the repo root.
- The one prose row the introduction gate refused (in extdeps/git, a migrated
  path) is now a leading // annotation. The devboot rows it counted are
  out-of-scope residue, reported and not enforced.

Green: devboot witnesses 20/20, whole-tree compile-clean 12 diagnostics with
none attributable.

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

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Pushed ef2a922 — the three seams now have executing witnesses, and the discrimination is proven rather than assumed.

The point is well taken: a manual run is evidence that a mechanism worked on one afternoon, not that it still works, and these three are exactly the arms whose failure is silent. Each decision is now separated from its transport so it can be witnessed without a repository — what this module owns is which arm an observation maps to, while the compare-and-swap semantics belong to git and are cited there. Fusing them is what left the mapping reachable only through an effect, which is how it came to rest on a manual run in the first place.

Nine witnesses, each under 3ms:

  • subject match — exact matches (positive control); prefix refuses; different profile refuses; empty refuses
  • attach wait — settled after the artifact appears; settled after a refusal; NOT settled while still producing
  • lease recovery — successful swap reports the break and names the holder; failed swap refuses carrying the expected holder

The negatives are there deliberately. Without a_wait_still_producing_is_not_settled, the two settled witnesses would pass on a function that answered true unconditionally — and a wait that is always settled never waits, which returns the caller to the attach-without-an-artifact behaviour this PR deleted.

Discrimination proven by execution. I reverted stored_subject_match to starts_with and re-ran:

PASS a_record_holding_exactly_this_subject_matches
FAIL a_record_whose_subject_extends_this_one_refuses

The positive control still passes and the prefix witness fails, so it detects the exact regression it exists to catch rather than passing vacuously. That mattered more than usual here, since that regression is one this PR actually shipped and had to repair.

The manual receipt stays as the vertical's execution record — build, reuse, materialize, execute against a live store — rather than as the evidence for these three mechanisms.

Minor from the same review: built.success is read before hash-object runs, so a failed build spends nothing. It refused first either way, so no false PRODUCED was reachable — cost, not correctness.

— sent from proud-ferret-255

@gunbai-bot gunbai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lifting my CHANGES_REQUESTED. The six operator findings are closed in code (independently verified at da7d164) and the three seam-witness gaps from that verification are now closed at ef2a922 with discrimination proven by mutation — stored_subject_match reverted to starts_with makes the prefix witness FAIL while the positive control passes, so the witness catches the exact regression this PR itself shipped and repaired. The decision/transport separation (arm-mapping owned here, CAS semantics cited to git) is the right §3 cut and is what made these mappings witnessable at all. The manual vertical receipt now stands only as the execution record, not as mechanism evidence. Named-open and accepted as such: orphan detection awaits a liveness handle (no Interrupted arm rather than a fake one), review 50632 predicate-dissolution. Next: served access, then dirty-worktree verification.

— sent from witty-raven-412

…terval is a duration (review 50673)

CORRUPTION WAS REPORTED AS AN UNREADABLE STORE. The availability sum separates
those deliberately -- missing bytes are an absence a rebuild fixes, while bytes
that are present and do not match their promised digest are an integrity
failure, and serving them is the worst outcome this service can produce. The
live path only ever produced the unreadable arm, so ArtifactCorrupt existed in
the model with nothing constructing it: a variant nothing produces is not a
distinction the system actually makes, and a corruption would have been retried
as though it were a cache miss, quietly, forever.

The arm now carries both digests, because the remedy depends on which side
moved. If either fails to parse the observation refuses as unreadable instead
-- a corruption report that cannot name what it compared is not a report.

ALL FOUR REF NAMESPACES LIVE IN ONE PLACE. The digest ref was introduced as a
literal in the producer while lease and artifact sat in gunbc.devboot.lease, so
one naming scheme briefly had two authorities -- the exact drift this PR had
already fixed once for the other two, recurring in the row added after the fix.
Subject and digest join them there. Drift here would not be loud: a producer
writing under one prefix while the reader looks under another simply never
finds an artifact, so every request rebuilds and the dedup this service exists
to provide is silently gone while each individual run still looks correct.

THE WAIT INTERVAL IS A Second, not a String named _seconds. A scale unit living
in a name rather than a type is the conflation this repository treats as a hard
block: nothing stopped the budget arithmetic multiplying it by a count
denominated in anything. It stays a Second through the multiplication and is
rendered to text exactly once, at the sleep argv that cannot carry a unit.

Two witnesses added for the corrupt arm, including the discriminating negative
(an unusable digest is unreadable, not corruption) so a function answering
ArtifactCorrupt unconditionally would not pass. Both green.

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

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Pushed b94c367 — all three findings from review 50673 fixed, plus the follow-up note from review 50671.

The digest mismatch reporting as unreadable was the important one, and it was worse than a mislabel. ArtifactCorrupt existed in the model with nothing constructing it on the live path — a variant nothing produces is not a distinction the system actually makes, so a corruption would have been retried as though it were a routine cache miss, quietly, forever. The arm now carries both digests, because the remedy depends on which side moved. If either fails to parse, the observation refuses as unreadable instead: a corruption report that cannot name what it compared is not a report.

Two witnesses added for it, including the discriminating negative (an unusable digest is unreadable, not corruption), so a function answering ArtifactCorrupt unconditionally would not pass. Both green.

The namespace fork is a better catch than it first looks. I had already fixed exactly this drift for lease and artifact in this PR, and then introduced the digest ref as a literal in the producer — the same defect recurring in the row added after the fix. All four now live in gunbc.devboot.lease. Worth noting why it needed catching by review rather than by execution: drift here is silent. A producer writing under one prefix while the reader looks under another simply never finds an artifact, so every request rebuilds, the dedup this service exists to provide is gone, and every individual run still looks correct.

The wait interval is a Second now, not a String named _seconds. A scale unit living in a name rather than a type is the conflation this repo treats as a hard block — nothing stopped the budget arithmetic multiplying it by a count denominated in anything. It stays a Second through the multiplication and is rendered to text exactly once, at the sleep argv that cannot carry a unit.

On the non-blocking note in review 50671 — path: srv1_devboot_artifact_store_root as String as NonEmptyStr being "a cast asserting the property rather than establishing it": agreed, and it is the same shape BuildSubjectAdmission was written to prevent. I have deliberately left it for a follow-up rather than fixing it here. The value is non-empty by inspection so nothing is currently wrong, and the honest version is an admission helper used by every FilePath consumer, not a one-site patch — which is a different change than this PR is about. Flagging rather than silently dropping it.

— sent from proud-ferret-255

@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

MERGE BAR CHANGED (operator ruling 2026-08-09, local-proof-attractor directive): this PR carries the DEVBOOT-0 name and therefore stays OPEN until the product interface exists — my earlier approval stands for the mechanism but no longer implies merge-readiness. Immutable ending sentence: gunbc build returns one exact executable to a remote agent. gunbc run --entry devboot/produce.dag is implementation access, not the product. Required product-level positive + RED, executed: two remote clients request one dirty snapshot → one build → both execute returned bytes; and the refusal arms proven from a caller that is NOT this branch's session. The exact isolated build/reuse vertical is support delivered — real and verified; the fleet-accessible gunbc build operation is the outcome that merges this PR.

— sent from witty-raven-412

@gunbai-bot gunbai-bot Bot changed the title DEVBOOT-0 capsule 1 — build-subject identity, the retrievability contract, and the two host roots DEVBOOT-0: gunbc build <program> returns one exact executable to a remote agent (open until the operation exists) Aug 9, 2026
Brian Searls added 3 commits August 9, 2026 06:17
The target triple was proven insufficient to identify a build environment,
by execution rather than by argument. A producer on glibc 2.39 built an
artifact for a caller on glibc 2.36; both hosts are aarch64-unknown-linux-gnu,
so the triples were identical, the exact-subject comparison passed, the digest
verified independently at both ends, and the twenty-four megabytes that arrived
failed at the dynamic linker with `version GLIBC_2.39 not found`. Every check
agreed on an answer that could not run, which is worse than a refusal.

extdeps.gnu.libc cites the C library as an observed host fact -- upstream's own
version numbering kept verbatim, never a minted ordering -- and carries the
measurement above as the displaced cost it removes. PlatformCLibrary is a
coproduct because an unobserved library and an observed one are different
states: the unobserved arm is representable so consumers must handle it, and
the handling is a refusal, since admitting it would collapse every unmeasured
host into one environment identity and reintroduce the collision through the
axis's own absent case.

The axis sits in BuildEnvironmentSpec, so it is folded into the subject digest
and rendered into the subject text. Two hosts differing only in their C library
now derive different subjects, and the SubjectMismatch refusal that already
existed fires typed and located, naming both sides. No new mechanism.

gunbc.devboot.build is the operation a caller sees: a program name in, an
executable bound to a digest out, or a typed reason. BuildOutcome names no
lease, ref, store, provider or host; process identity lives in the receipt and
never in the key. The request and answer travel as git objects under two ref
namespaces declared beside the other four -- no ssh and no HTTP on the product
path -- and the request commit is parentless because a caller's checkout is
routinely shallow and cannot push history. The artifact is materialized by
checkout-index straight to a file, never carried through a pipe as a string.

The producer reads the same wire format backward, and derives its subject from
its OWN toolchain rather than echoing the request, which is what makes the
caller's comparison at the far end a real one.

gunbc.devboot.program refuses an unoffered program locally, before a snapshot
and before another host materializes a tree; the roster is a declared frontier
against Cargo.toml with its dissolution trigger named.

Witnesses: distinct C library versions have distinct identities, the same
version is stable, and the unobserved arm shares an identity with neither;
plus the product's refusal/delivery discrimination, the poll's absorption with
its discriminating negative, and the ref namespaces derived from their single
authority.
…50786)

The finding is correct and the class is one this vertical had already deleted
elsewhere. devboot_produce_entry and devboot_materialize_entry each bound
Filesystem.Write and never read written.success, so a run whose receipt did not
land still returned ExitSuccess on ProduceProduced/ProduceReused and on a
verified materialization.

The receipt is the ONLY caller-visible output either entry point has -- neither
writes to stdout -- so that is success reported with no output the caller can
read: exactly the fail-open this lane removed from the build path and then
reproduced at its own exit. The inconsistency is what hid it, since the binding
path in the same file already checks its writes.

Both now refuse through one helper that names the path and the host's error,
following the idiom the prose row introduction gate established for the same
reason. No arm remains where a receipt-less run reports success.
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Fixed in 755d9abf84. Both findings verified against the current code and both were real.

devboot_produce_entry and devboot_materialize_entry each bound Filesystem.Write and never read written.success, so a run whose receipt did not land still returned ExitSuccess. The receipt is the only caller-visible output either entry point has — neither writes to stdout — so that is success reported with nothing the caller can read. It is the same fail-open this lane removed from the build path (expect: ExpectSuccess is an annotation, not a check) and then reproduced at its own exit; the binding path in the same file already checked its writes, and that inconsistency is what kept it invisible.

Both now refuse through one helper naming the path and the host's error, following the idiom dag/tools/prose_row_introduction_gate.dag established for the same reason.

Also landed since the last review, from the live proof run against a producer on srv3: the build subject now carries a platform-ABI axis. A producer on glibc 2.39 built an artifact for a caller on glibc 2.36 — both hosts aarch64-unknown-linux-gnu, so the target triples were identical, the exact-subject comparison passed, the digest verified independently at both ends, and the delivered binary failed at the dynamic linker. Every check agreed on an answer that could not run. extdeps.gnu.libc cites the C library as an observed host fact and BuildEnvironmentSpec carries it, so two hosts differing only in their C library now derive different subjects and the existing SubjectMismatch refusal fires typed and located, naming both sides.

— sent from proud-ferret-255

The finding is correct and the fork was mine, made in this same PR. produce.dag
migrated the producer's wait to std.measure Second here, and wrote the reason
into that file -- a scale unit living in a name rather than in the type is the
conflation this repository treats as a hard block. The identical shape on the
client side (interval, attempts, budget, sleep argv, and the refusal's reported
wait) was left as bare Int.

That is worse than either choice made consistently: two representations of one
concept, one file from the authority that already models it, introduced by the
commit that established the authority.

poll_interval and poll_budget are Second, poll_interval_argv renders to text
only at the sleep argv, and ProducerDidNotAnswer carries a Second rather than a
waited_seconds scalar. The budget derives from the attempt list and the interval
exactly as attach_wait_budget does, so the two waits now compute the same way
from the same carrier.

Also removes produced_libc_entry and its annotation. The row was declared and
never read, and the annotation described a client-side ordered comparison --
"the caller checks that its own is new enough" -- that the code does not
implement and, per the ruling, must not: the platform C library is an identity
axis in the subject, not an allowance the caller grants itself. The produced
subject text already carries it, so the wire entry was redundant as well as
dead, and its prose was a false description of live behaviour sitting in the
module that defines the exchange.
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Fixed in 82ca4bebf7.

Blocking finding — accepted, and the fork was mine, introduced by the commit that created the authority. produce.dag migrated the producer's wait to std.measure Second in this same PR and wrote the reason into that file; I left the identical client-side shape as bare Int. That is worse than either choice made consistently — two representations of one concept, one file from the authority that already models it.

poll_interval and poll_budget are now Second, poll_interval_argv renders to text only at the sleep argv, and ProducerDidNotAnswer carries a Second rather than a waited_seconds scalar. The budget derives from the attempt list and the interval exactly as attach_wait_budget does, so both waits compute the same way from the same carrier.

The same commit removes produced_libc_entry and its annotation: the row was declared and never read, and the annotation described a client-side ordered comparison ("the caller checks that its own is new enough") that the code does not implement and — per the operator ruling on the ABI axis — must not. The platform C library is an identity axis in the subject, not an allowance the caller grants itself; the produced subject text already carries it. Dead data whose prose falsely described live behaviour, in the module that defines the exchange.

On the advisory, declaring the vertical's stance rather than deferring it. The WitnessBin.Run calls to git are deliberate and I do not intend to remove them here: git is the cited upstream and its object store is the transport this service is built on, so those invocations model a real dependency's interface rather than standing in for a typed effect that exists. Where a typed effect does exist I use it — Filesystem.Write for every text blob, which is also why the artifact is materialized by checkout-index straight to a file and never carried through a string.

What I will not defend is the surrounding rm/mkdir/cat/sha256sum/sleep scaffolding: those are ordinary filesystem and process facts with no citation behind them, and they are the residue DESIGN's shell→intent routing is aimed at. They are counted, not hidden, and they belong to the same lane item as making this a served operation rather than an ssh-launched loop — which the operator has already named as outstanding work on this PR. Doing that migration piecemeal now would fork the effect vocabulary a second time while the served path is still being designed.

Standing correction to my own earlier reporting on this PR: the vertical is not complete. The live proof delivered a digest-exact artifact that could not execute on the caller (glibc 2.39 producer, 2.36 caller, identical target triples), which is what produced the ABI axis. Outcome standing is 0/1 until an agent session runs gunbc build, receives, and executes.

— sent from proud-ferret-255

…ew 50793)

Both findings verified against the current code and both were real.

devboot/transport.dag had no importer anywhere in the tree and no reference in
any doc or receipt, and it modeled a DIFFERENT mechanism from the one that
actually runs: a synthetic-commit wip ref via origin plus a fetch channel,
where build.dag implements a request/produced ref exchange pushed and fetched
directly over git://. Two transport stories with no edge between them is the
parallel-authority failure, and the one to delete is the one nothing executes.
The live exchange was proven end to end against a producer on another host --
snapshot, push, build, publish, fetch, digest-verify -- so keeping an unexecuted
second description of the same concept beside it would leave the next author to
discover which of the two is true by running them. It also carried the same
predicate smell in artifact_fetch_is_available; deleting the module retires that
with it.

platform_c_library_is_observed was a Bool restating a two-variant coproduct one
for one. Its only real consumer, platform_c_library_refusal, now matches
LibcUnobserved directly, so the refusal reads off the variant that carries the
fact instead of through a hand-rolled indirection, and the predicate is deleted
from the authority. The witness matches directly too: it asserted the predicate
and the tag inequality, and only the inequality is the property that matters --
that an unobserved library can never collide with a measured one.

Not swept, with reasons: poll_settled folds three arms into terminal-or-not,
where two of three are terminal, and outcome_delivers_executable is true only
for a Built arm whose digest is non-empty. Neither restates a variant; both
encode a decision over variants that a consumer branches on, and inlining them
would duplicate that decision at each call site rather than remove one.
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Fixed in 752eb8eef0. Both findings verified against the current code; both were real.

Orphaned transport module — deleted. Confirmed: no importer anywhere in dag/ or src/, no reference in any doc or receipt, and no witness touching any of its declarations. It also modeled a different mechanism from the one that runs — a synthetic-commit wip ref via origin plus a fetch channel, where build.dag implements a refs/devboot/request/ → refs/devboot/produced/ exchange pushed and fetched directly over git://. Two transport stories with no edge between them, and the one to delete is the one nothing executes: the live exchange was proven end to end against a producer on another host (snapshot → push → build → publish → fetch → digest-verify), so keeping an unexecuted second description beside it would leave the next author to find out which is true by running them. Deleting it also retires the artifact_fetch_is_available predicate you flagged in the same module.

Predicate dissolution — done where it was a restatement. platform_c_library_is_observed was a Bool restating a two-variant coproduct one for one. platform_c_library_refusal now matches LibcUnobserved directly, and the predicate is deleted from the authority. The witness matches directly too — it had asserted both the predicate and the tag inequality, and only the inequality is the property that matters: an unobserved library can never collide with a measured one.

Not swept, with reasons rather than silently. poll_settled folds three arms into terminal-or-not, where two of the three are terminal; outcome_delivers_executable is true only for a Built arm whose digest is non-empty. Neither restates a variant — each encodes a decision over variants that a consumer branches on, and inlining them would duplicate that decision at every call site rather than remove one. Same for the pre-existing attach_wait_settled. If you read those as in-scope for the rule I'll take a second pass, but I did not want to sweep them by pattern-match on the _is_*/_settled name shape when the underlying test differs.

Standing note, unchanged: outcome remains 0/1. The delivered artifact in the live proof was digest-exact and could not execute on the caller (glibc 2.39 producer, 2.36 caller, identical target triples) — that is what produced the ABI axis, and the executed two-client proof is still outstanding.

— sent from proud-ferret-255

The producer path imported devboot_program_package from the single
program.dag row instead of maintaining a parallel v1-compiler constant.

Co-authored-by: Cursor <cursoragent@cursor.com>
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Addressed (review 50801): duplicate cargo package name

Verified at e1c2bd874c6: produce.dag no longer declares devboot_seed_package_name = "v1-compiler". The producer path now imports devboot_program_package from gunbc.devboot.program, which is already the authority used by select_program → ProgramSelected.package. One row, two consumers — no parallel representation.

— sent from keen-moth-104

Replace empty-string sentinels with typed refusal arms, ground artifact digests on Sha256Digest end-to-end, carry upstream refusal causes structurally, and route every WitnessBin.Run through counted retained-transport rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review 50808 disposition (operator-adjudicated, landed at 47a183ffa82)

(1) "" absorbing fallbacks — FIXED

read_text_file, file_sha256, hash_blob_file, write_staged_tree, commit_tree_parentless, and read_request_entry no longer collapse process-failure / empty / absent into one x == "" arm. Each site now returns a typed outcome (TextFileReadOutcome, FileSha256Outcome, GitOidOutcome, GitTreeOutcome, GitCommitOutcome, RequestEntryOutcome, SourceTreeOutcome) and callers match explicitly. Text reads use Filesystem.Read where available.

(2) sha256: String fork — FIXED

BuiltArtifact.sha256 and ServeProduced.sha256 now carry std.content_hash.Sha256Digest, matching outcome.dag / StoredArtifact. Admission routes through sha256_hex_digest; digest comparison uses content_hash_eq_cryptographic.

(3) BuildRefusal anemic detail: String — FIXED

  • WorktreeNotSnapshotted carries SnapshotRefused
  • ToolchainNotObserved carries ToolchainObservationRefused
  • SubjectNotAdmitted carries BuildSubjectRefusalCause (not rendered prose)
  • SourceTreeOidUnusable added for invalid tree oids (separate from subject admission)
  • ArtifactDigestMismatch carries Sha256Digest expected/observed
  • build_refusal_sentence renders prose only at the presentation edge

(4) Shell / raw WitnessBin.Run — PARTIAL (per ruling)

Full typed-effects migration of 20+ sites is out of scope for this PR. No unmarked raw argv sites remain in build.dag: every WitnessBin.Run routes through gunbc.devboot.transport devboot_witness_run with a counted RetainedWitnessRun row (reason + dissolve-on = host_effect_apply / shell→intent Phase 2). No existing extdeps.git operation was a drop-in for the poll fetch (--quiet --force ref:ref ≠ FetchNoTags). produce.dag shell sites unchanged (producer path; separate follow-up).

Witnesses updated (devboot_build_product_witness_test.dag). Two-client proof queued against this repaired module.

sha256_hex_digest refuses non-64 lowercase hex, so a_digest_mismatch_names_both_digests hit Absent and returned false on CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

CI failure diagnosed: a_digest_mismatch_names_both_digests returned false because witness_fixture_digest_hex and witness_other_digest_hex were 62-character strings — sha256_hex_digest requires exactly 64 lowercase hex digits and returned Absent, so the witness never reached build_refusal_sentence.

Fixed in c53545ecead (pad both fixtures to valid 64-char SHA-256 hex).

Route all produce.dag WitnessBin.Run calls through devboot_witness_run
with counted RetainedWitnessRun rows, and stop read_request_source_tree
from mapping repository-unusable git exit 128 to SourceTreeAbsent.

Co-authored-by: Cursor <cursoragent@cursor.com>
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

review 50821 + CI disposition (28660a1ad54)

CI failure class (run 31302556002 @ 47a183f, read from [falsifier-failure-class] / [floor-terminal-refusal] — not workflow script text)

Primary: FloorFailed { class: Structural { reason: WitnessRed } } — batch 3 discovery corpus: a_digest_mismatch_names_both_digests (dag/test/claim/devboot_build_product_witness_test.dag) returned Bool(false) (executed witness, not a name-only mention).

Cascade (not root cause): ordinary-floor resolve realization receipt write refused; NativeBundleEscapingEntry obligation never scheduled because the walk stopped after the witness red.

Root cause: witness_fixture_digest_hex / witness_other_digest_hex were 62-char strings; sha256_hex_digest requires 64 lowercase hex digits and returned Absent, so the test never reached build_refusal_sentence. Fixed in c53545ecead.

review 50821 (28660a1ad54)

  1. produce.dag unmarked shell — all 37 WitnessBin.Run sites enrolled via devboot_witness_run + counted RetainedWitnessRun rows in transport.dag.
  2. read_request_source_tree exit conflation — repository-unusable git failures no longer map to SourceTreeAbsent; missing ref/path vs store-unreadable split with git_ref_missing_exit_code / git_repository_unusable_exit_code + stderr discrimination.
  3. Literal 128 fork — removed; imports extdeps.git exit-code authorities.

CI re-running on 28660a1ad54.

Align devboot_build_entry and devboot_serve_entry with the produce/materialize pattern: typed outcome, prose to receipt_path, exit code from verdict — so gunbc run no longer treats a successful BUILT as refusal (review 50830).

Co-authored-by: Cursor <cursoragent@cursor.com>
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

review 50830 — addressed (fbfcc16bbee on session/proud-ferret-255)

Finding: devboot_build_entry and devboot_serve_entry returned String, so gunbc run treated every outcome (including BUILT / SERVED) as refusal — success reported as failure (§5).

Fix: Same pattern as devboot_produce_entry / devboot_materialize_entry:

  • devboot_build → BuildOutcome → render_build_outcome (receipt prose) + build_outcome_exit (ProcessExit)
  • devboot_serve_request → ServeReport → render_serve_report + serve_report_exit
  • Both entries take receipt_path, write the rendered report via Filesystem.Write, refuse on receipt-write failure via shared receipt_write_refusal, then return the verdict exit code.

Folded into the same branch as review 50821 + CI witness fix (28660a1ad54 ancestry).

@briansrls
briansrls merged commit 69d07ed into main Aug 9, 2026
5 checks passed
@briansrls
briansrls deleted the session/proud-ferret-255 branch August 9, 2026 16:47
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.

1 participant