carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

Census proposal: claim trial step 6 artefacts

Status: RULED (platform-1e, 2026-10-18): accepted as proposed (rev3=census-ruling-claim-step6-artifacts). Branch claim-step6-artifacts, cut from main at 3b69c2bd (v0.28.1). Paul approved the build on 2026-10-18.

What changed

Step 6 of the dev cohort-1 trial runs carlos system guard-arm. Two of its inputs had no producer, and the protected console copy had no installer. This branch adds all three. Each is an operator command that does nothing unless someone runs it (ruling 6).

  • claims run phase sign-policy (internal/claimactivation/artifacts.go, Runner.signPolicy). It signs an armed hostfloor.Policy with hostfloor.ArmedPolicy and writes nothing to the bucket. hostfloor.Authorize is now the one signer of the floor pair: hostfloor.Publish calls it too, so the later publish-floor writes the same bytes. Tests pin that equality in hostfloor, in the runner (sign-policy then publish-floor) and in the root-runner witness. The phase refuses a floor the deployment is not bound to: the roster's preparing or published digest, a published pair, or half of one.
  • claims run phase publish-identity (Runner.publishIdentity, identityRelease, consoleArtifact, readConsole). Nothing is typed (controller ruling C2). - The router's hash is the one in the release's SHA256SUMS.txt, verified with publicrelease.VerifySums against the vendor key the binary embeds. A root-package test pins that key to update.go's cliUpdatePubKey. - The console's hash comes from the console channel's root-verified pointer, its signed manifest digest for the explicit version, and that manifest. The artefact is then read from the bucket, hashed, and its version.Build stamp (release.ArtifactBuild) must equal the tag. - The identity is signed with release.SignSystemIdentity and published create-only through the existing release.PublishSystemIdentity, via the runner's checkedStore. checkedStore.Put is unchanged. The phase then reads the identity back and verifies it.
  • claims run --out (internal/host, writeActivationOutput, writeActivationOutputAt). The two phases write their file there byte for byte (controller ruling C4): mode 0600, created exclusively in a checked root-owned directory, then read back.
  • claims install-console (runInstallConsole, installProtectedConsole, consoleDir, consoleSlotHolds, readConsoleSource). A root box installer. It copies a console build into /opt/carlos/system/console/console and .prev as 0755 root:root, after these checks: - the staged identity verifies and is byte-equal to the published one; - the identity names this box's build; - the source's sha256 is the identity's console sha256.

.prev equals current (ruling C3, on install-channel's A2-2 precedent). It does not touch console.service.

  • guard-arm (RunSystemGuardArm): its parse of --policy and --identity moved unchanged into guardArmInputs, so tests feed it the phases' files. It reads the same files, now in a different order.

Root-read census (TestPlan14RootReadCensus)

Overlay internal/provision/testdata/plan14-root-reads-claim-step6-artifacts.tsv, loaded last, after plan14-root-reads-box-publish-record.tsv. It adds no accepted-legacy row, so the seal (fc584e11...1b5d) stands.

| Unit | Executing identity | Class / tasks | Basis | |---|---|---|---| | internal/claimactivation/runner.go:Runner.Run (changed) | operator CLI, uid 0, on the trusted signing host (claims run) | authority F,G,J4 | as ruled for activation-tools; sign-policy has no writable key, publish-identity only its tag's identity key | | internal/claimactivation/artifacts.go:Runner.signPolicy (new) | operator CLI, uid 0, on the signing host | authority F,G,J4 | reads the floor pair only to refuse; writes nothing | | internal/claimactivation/artifacts.go:readConsole (new) | operator CLI, uid 0, on the signing host | authority F,G,J4 | reads the console artefact named by a verified pointer and digest-checked manifest, bounded; sha256 and stamp checked by its caller | | internal/release/program.go:ArtifactBuild (new) | operator CLI, uid 0, on the signing host (publish-identity) | authority G,J4 | parses build info of bytes already hashed against a signed manifest; one plain -X version.Build flag or nothing. No box path calls it | | internal/host/activation_command_linux.go:runSystemClaims (changed) | operator CLI, uid 0 (signing host for run, box for installers) | authority G,J4 | as ruled; --out only for the two phases and checked before signing; install-console checks the staged identity against the published one | | internal/host/activation_artifacts_linux.go:writeActivationOutputAt (new) | operator CLI, uid 0, on the signing host | authority G,J4 | checked parent; refuses any existing entry; exclusive 0600 create, fsync, exact readback | | internal/host/activation_artifacts_linux.go:consoleDir (new) | box installer, uid 0 (claims install-console on the console box) | authority G,J4 | no-follow walk from /; root-owned, not group or other writable, other-searchable; creates 0755 | | internal/host/activation_artifacts_linux.go:consoleSlotHolds (new) | box installer, uid 0 | authority G,J4 | absent, or root:root 0755 one-link regular file holding the sha256; anything else refuses | | internal/host/activation_artifacts_linux.go:installProtectedConsole (new) | box installer, uid 0 | authority G,J4 | source verified before anything is created; staged rename; readback through hostfloor.OpenProtected | | internal/host/activation_artifacts_linux.go:readConsoleSource (new) | box installer, uid 0 | authority G,J4 | no symlink on the path, regular, one link, at most 512 MiB, must hash to the sha256 | | internal/host/guardarm.go:RunSystemGuardArm (changed) | carlos system guard-arm, offline CLI, uid 0 | authority F,J4 | as ruled in p3; parse moved into guardArmInputs, same files read |

Source envelopes re-pinned: internal/claimactivation, internal/host, internal/hostfloor (Authorize, ArmedPolicy), internal/publicrelease (VerifySums), internal/release (SignSystemIdentity, ArtifactBuild). The root package envelope is unchanged: only a test file was added there.

No box agent, update, recovery or edge path reaches a new unit. guard-exec, Disk.VerifyFile, Disk.Running, hostfloor.Publish's two writes and the box report path are byte for byte main's, apart from Publish calling Authorize.

Record-write census (TestPlan9B8RecordWriteCensus)

No new bucket writer, so no 9B8 entry is added. The test passes unchanged. The one new bucket write is the identity's create-only PUT. It goes through the existing release.PublishSystemIdentity and the runner's checkedStore.Put (already nonrecord, digest 26c6e1f7...0bfd6, unchanged), which admits only the plan's identity key. sign-policy admits no key. The installer and --out write local files only, through safefile, and no record key or record path is involved.

Tests behind the claims

  • internal/hostfloor/armedpolicy_test.go: the armed policy's floor and authorization are byte-equal to what Publish stores; deterministic re-signing; invalid floors refused.
  • internal/claimactivation/artifacts_test.go: production S3Store against leasetest's S3 server, roster made by the runner's own phases, the console shipped and promoted with pointer digests on. - sign-policy: sends no write request; its output passes guard-arm's parse; it refuses a different or half-published floor, a roster prepared for another floor, and a stale revision. - publish-identity: one create-only PUT (If-None-Match *); the stored bytes are the output; the identity passes CheckImage and CheckComponent; a replay is a no-op; another console under the same tag is refused and nothing moves; a history version resolves through its signed history digest. - Refusals, each with no write: another sums signer, sums changed after signing, no linux-arm64 router, a console hash that is a release file, no sums key, protocol 0, a build sha as tag, a version not on the channel, another channel or account, a pointer without digests, a swapped manifest, a swapped artefact, a console stamped with its build sha, an unstamped console, the CLI shipped as the console.
  • internal/claimactivation/runner_test.go: sign-policy then publish-floor through the runner; the published pair equals the policy.
  • internal/host/activation_artifacts_linux_test.go: the --out files round-trip into guardArmInputs; a re-indented policy is refused; exclusive create, 0600, dry run writes nothing. The installer's identity checks; the claims runner uses the vendor key.
  • internal/release: ArtifactBuild on real built binaries; SignSystemIdentity create-only and refusals.
  • Root runner (20261018T070118-3fa2a1, PASS): - TestRootActivationConsoleRuntimePayload produces the identity through publish-identity over the production S3 store, the policy through the real binary's sign-policy and --out, and the console copies through the real binary's install-console. There is no hand-published identity and no hand-made console copy. guard-arm, guard-exec and the console then start under the guard drop-in as the service user, from the protected copy's bytes, and the floor publish-floor writes is the policy's. - TestRootActivationConsoleInstall covers the installer as real root.

Fix round 1 (RULED, rev3=census-ruling-claim-step6-artifacts-fix1)

Astra's review blocked the merge on four findings. The controller ruled F1, F3 and F4 FIX, and accepted F2 as a trial gap. The rows above stay as ruled. The changes are in a second overlay, internal/provision/testdata/plan14-root-reads-claim-step6-artifacts-fix1.tsv, loaded after the first. RULED (platform-1e, 2026-10-18): accepted as proposed, the overlay's rows and the 9B8 entry alike; the seal stands.

  • F1, bounded ELF parsing. release.ArtifactBuild runs release.CheckELFBounds (new, internal/release/elfbounds.go) before buildinfo.Read. The check reads only fixed-offset header fields and has no read site. It accepts only ELF64 little-endian EMAARCH64, with at most 256 sections and 64 segments. Every table and range must lie inside the file, the tables must not overlap each other or the ELF header, sections with contents must not overlap, and no section may be SHFCOMPRESSED. An unstripped Go binary is refused, because its DWARF is compressed. Release and console builds are stripped (-s -w).
  • F2, recorded flag, not linked value: trial gap. ArtifactBuild reads the recorded -X flag. Release binaries are stripped, so there is no symbol table to read the linked value from. The chain rests on the release key. Owed before production cohorts: trusted build attestation tied to the artefact digest.
  • F3, sums bound to the tag. sumsNameRelease (claimactivation, no read site) requires carlos_<X.Y.Z>_amd64.deb and carlos_<X.Y.Z>_arm64.deb for exactly the tag's X.Y.Z, and no other carlos_*.deb.
  • F4, --out reserved before the phase. In internal/host: - reserveActivationOutputAt creates the file exclusively, 0600, and holds it open before the phase can write. - activationOutput.finish writes, syncs and reads back through that descriptor. - activationOutput.abandon removes the file only while activationOutput.ours shows the entry is still that file (same device and inode). - A failure after any write attempt keeps the file and says so. - writeActivationOutputAt is gone (@remove).

Root-read rows: ArtifactBuild and runSystemClaims are changed; reserveActivationOutputAt, activationOutput.finish and activationOutput.ours are new, all authority G,J4, executing as the operator CLI, uid 0, on the trusted signing host. The internal/claimactivation, internal/host and internal/release envelopes are re-pinned. The seal stands.

Record-write census (9B8): one new entry, internal/host/activation_artifacts_linux.go:putAttempts.Put, proposed nonrecord (internal/provision/claim_step6_artifacts_census_test.go, digest 0c8bbda8...b848). It wraps the store claims run hands the two phases, only to record that a write was attempted. Every Put it passes on comes from the runner's checkedStore and its allow-list, and it adds no key, etag or body.

Fix round 2 (RULED, rev3=census-ruling-claim-step6-artifacts-fix2)

Astra's re-review found F1 incomplete and a new LOW (F5). The controller ruled both FIX. Overlay internal/provision/testdata/plan14-root-reads-claim-step6-artifacts-fix2.tsv, loaded after fix1. RULED (platform-1e, 2026-10-18): accepted as proposed; the 9B8 inventory and the seal are unchanged.

  • F1, section names. release.CheckELFBounds also caps the section-name table: type SHT_STRTAB, 1 byte to 64 KiB, every section's name offset inside it, and every name at most 256 bytes up to its NUL. debug/elf copies each section's name, so the names it can copy are at most 256 x 256 bytes. No read site is added, ArtifactBuild's digest is unchanged, and the internal/release envelope is re-pinned. A real stripped cmd/console and carlos linux-arm64 build pass. A 32 MiB name repeated across every section is refused through publish-identity with no write.
  • F5, never remove a reservation. activationOutput.abandon is gone. On any failure the reserved --out file stays, and the error names it, says whether the phase wrote to the bucket first, and asks for a new --out path. reserveActivationOutputAt is now a wrapper with no read site (@remove). Its reads moved unchanged into reserveOutput (new row, authority G,J4, operator CLI uid 0 on the trusted signing host), which also names the full path in its errors. The internal/host envelope is re-pinned.

The 9B8 inventory is unchanged: putAttempts.Put is byte for byte as ruled. The seal stands.