carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1# Census proposal: claim trial step 6 artefacts
2
3Status: 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.
4
5## What changed
6
7Step 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).
8
9- **`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.
10- **`claims run` phase `publish-identity`** (`Runner.publishIdentity`, `identityRelease`, `consoleArtifact`, `readConsole`). Nothing is typed (controller ruling C2).
11 - 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`.
12 - 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.
13 - 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.
14- **`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.
15- **`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:
16 - the staged identity verifies and is byte-equal to the published one;
17 - the identity names this box's build;
18 - the source's sha256 is the identity's console sha256.
19
20 `.prev` equals current (ruling C3, on install-channel's A2-2 precedent). It does not touch `console.service`.
21- **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.
22
23## Root-read census (TestPlan14RootReadCensus)
24
25Overlay `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.
26
27| Unit | Executing identity | Class / tasks | Basis |
28|---|---|---|---|
29| `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 |
30| `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 |
31| `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 |
32| `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 |
33| `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 |
34| `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 |
35| `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 |
36| `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 |
37| `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` |
38| `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 |
39| `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 |
40
41Source 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.
42
43No 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`.
44
45## Record-write census (TestPlan9B8RecordWriteCensus)
46
47No 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.
48
49## Tests behind the claims
50
51- `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.
52- `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.
53 - 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.
54 - 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.
55 - 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.
56- `internal/claimactivation/runner_test.go`: sign-policy then publish-floor through the runner; the published pair equals the policy.
57- `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.
58- `internal/release`: `ArtifactBuild` on real built binaries; `SignSystemIdentity` create-only and refusals.
59- Root runner (`20261018T070118-3fa2a1`, PASS):
60 - `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.
61 - `TestRootActivationConsoleInstall` covers the installer as real root.
62
63## Fix round 1 (RULED, `rev3=census-ruling-claim-step6-artifacts-fix1`)
64
65Astra'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.
66
67- **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 EM_AARCH64, 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 SHF_COMPRESSED. An unstripped Go binary is refused, because its DWARF is compressed. Release and console builds are stripped (`-s -w`).
68- **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.
69- **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`.
70- **F4, `--out` reserved before the phase.** In `internal/host`:
71 - `reserveActivationOutputAt` creates the file exclusively, 0600, and holds it open before the phase can write.
72 - `activationOutput.finish` writes, syncs and reads back through that descriptor.
73 - `activationOutput.abandon` removes the file only while `activationOutput.ours` shows the entry is still that file (same device and inode).
74 - A failure after any write attempt keeps the file and says so.
75 - `writeActivationOutputAt` is gone (`@remove`).
76
77Root-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.
78
79Record-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.
80
81## Fix round 2 (RULED, `rev3=census-ruling-claim-step6-artifacts-fix2`)
82
83Astra'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.
84
85- **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.
86- **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.
87
88The 9B8 inventory is unchanged: `putAttempts.Put` is byte for byte as ruled. The seal stands.
89