basal never places its own binaries. In this fleet the operator places every module binary with subconscious/scripts/fleet/place-module.sh, from a card the module stages. basal ships two binaries, ck-basal (the module) and ck-basal-worker (the confined QuickJS worker it runs flows in), and each gets its own card. script/stage.sh builds, signs, smoke-tests and stages both and prints the card text. Posting it, and everything after, is the operator's step.
script/stage.sh # a real stage, from a pushed commit
script/stage.sh --local-only --staging-root "$(mktemp -d)" # rig testing only
script/stage.sh --verify <stage dir> # re-check an existing stageIn order, stage.sh:
- Refuses a dirty tree, untracked files included, and refuses unless basal has a remote
originandHEADis onorigin/main(it fetchesorigin mainfirst).--local-onlyskips only theorigin/maincheck, for staging an unpushed commit onto the rig; the card, and apushed=noline in each.currentfile, then say it was not pushed. basal has no remote yet, so until it has one only--local-onlystages, and such a stage is for testing, never for handing over. - Builds
ck-basalandck-basal-workerin release mode,--locked, intotarget/stage/, withCK_BUILD_GIT_SHA=<HEAD>andCK_BUILD_GIT_DIRTYset as prefrontal's deploy script sets them. Each crate'sbuild.rsembeds the commit:ck-basaldeclares it as the manifest's build provenance (build_git_sha), and both binaries print it from--version. It refuses if the build changed the tree, or ifck-basalcontains the rig's kill switch (the stringBASAL_RIG_KILL_FILE, present only in arig-kill-hookbuild). - Signs a copy of each, by the policy in
script/signing.sh:codesign --force --sign - --options runtime --identifier ck-basal(andck-basal-worker). Ad hoc, hardened runtime, a plain identifier, no entitlements: the worker's QuickJS is an interpreter and needs no JIT, and its sandbox is a Seatbelt profile it applies to itself. Neverget-task-allow. It then refuses unless the signature verifies strictly, the identifier is exact, theflags=line carriesruntime, and there are no entitlements. - Smoke-tests the signed files directly, never through cargo, which relinks a binary and silently reverts it to a linker signature:
ck-basal --manifestmust name modulebasaland declare the commit asbuild_git_sha;--versionon both must name the commit, clean;- the worker's sandbox self-check,
script/sign-worker.sh verifywithBASAL_WORKER_IDENTIFIER=ck-basal-worker, asflows-rig.sh placeruns it: the checked-in Seatbelt profile is embedded, and a file read, a socket connect and a program launch are all denied; - debugger hardening: an unhardened ad-hoc copy of
/bin/sleepis held running andlldb -pmust attach to it (the control, without which a refusal proves nothing), then each signed binary is held running (--version, with its stdout a full pipe so it blocks) andlldb -pmust be refused. Each held process is checked to be running that file's image, by inode, before the attach. Each file's inode and codesign flags are recorded before and after; if either changed, it fails.
- Checks the marker and the control. The marker is the full commit sha, which both binaries must embed (
strings | grep -cat least 1). The controls, strings every build carries, areflow.installinck-basalandrefusing to run unconfined(its refusal message) in the worker. Both are counted against the running binary in~/.local/share/cortexkit/binwhen there is one. - Stages both under
<staging root>/basal-<short sha>/, with a.sha256sidecar each that passesshasum -c, and arevisionfile. The default staging root is~/.local/share/cortexkit/staging. A stage directory is never rewritten: staging the same commit twice refuses. - Declares it current, writing
<staging root>/basal.currentand<staging root>/basal-worker.currentthrough a temp file and a rename. Each holdsstage=(the stage directory),revision=(the full 40-character sha) anddeclared_at=(UTC), pluspushed=no (--local-only)for a local-only stage. place-module.sh derives the file name from the destination binary's name withoutck-, so the worker's card readsbasal-worker.current. - Prints the card and saves it in the stage directory as
card.md: card, signing, marker and control, store and formats, order, and the post-placement check. It is not posted.
place-module.sh runs one binary per call (--dest names the worker's destination), read-only unless given --place. Against this card:
| Arm | Result |
|---|---|
sidecar (<binary>.sha256 beside the staged file, shasum -c) |
passes |
| kind (Mach-O, and the same kind as the running file) | the staged files are Mach-O; there is no running file to compare |
currency (<staging>/basal.current and basal-worker.current, stage= equal to the staged file's directory) |
passes |
signing posture compared with the running binary; hardened runtime may be added, and is removed only with --allow-unhardened |
no running binary to compare. Both carry runtime. |
get-task-allow refused |
passes: no entitlements at all |
| designated requirement satisfied by the staged file | no running binary; ad-hoc requirements are cdhashes in any case |
| marker (staged ≥ 1, live 0) and control (both ≥ 1) | staged counts pass; no live binary to read |
format floors (check-format-floors.sh basal) |
no floors: none recorded, which passes |
The script resolves --dest (default ~/.local/share/cortexkit/bin/ck-<module>) and refuses before any arm when it does not exist: "destination does not exist, so this is an install rather than a placement". So on the first placement it cannot run at all, not even read-only. Without --no-restart it refuses even earlier, at its supervisor check (ck module status basal), because the daemon has no basal module yet; --no-restart skips that check, and the destination check still stops it. The first placement is therefore an install that the operator does by hand from the card's sha256, and the gate applies from the second card on.
basal's store is new, so there is no migration and no format on disk: no format-floor.json is written, and none is needed for the gate to pass.
Before staging, run the formatting, both clippy configurations, workspace tests and shell checks listed in the README, then validate and replay the safety catalogue with the pinned ck-mutate runner:
cargo install --locked --git https://github.com/cortexkit/commons --rev 2d096217015f295fe92e8ce52fe0ab8103efa370 cortexkit-mutate
mkdir -p target/mutations
ck-mutate check
ck-mutate run --all --report target/mutations/handover.jsonEvery row must be CAUGHT; a build failure or a red test other than the named guard is not proof of that guard. Keep the JSON report with the handover evidence rather than committing machine output. New safety guards belong in mutations.toml: resolve the full libtest path, use ck-mutate prove to append only a caught edit, and commit the guard with its proof as described in the README. These source-level proofs complement, rather than replace, the signed-binary and live rig checks below.
Run the staged bytes on the ckdev-flows rig, basal's isolated test stack (script/flows-rig.sh documents it in its header):
STAGING=$(mktemp -d)
script/stage.sh --local-only --staging-root "$STAGING"
script/flows-rig.sh build --prefrontal-rev <rev> ... # basal at the same HEAD
script/flows-rig.sh place --from-stage "$STAGING/basal-<short sha>"
script/flows-rig.sh config
script/flows-rig.sh test # every case but the crash case, which is reported as not run
script/flows-rig.sh place # the rig's own build again, with the kill switch
script/flows-rig.sh test # the crash case too
rm -rf "$STAGING"The staged ck-basal has no kill switch, so the contract suite cannot run its crash case on those bytes. The evidence for a commit is both runs at that commit.
- First placement: disable basal in the daemon's config (
"enabled": falsein itssubc.jsoncentry, or remove the entry) and removeck-basalandck-basal-workerfrom~/.local/share/cortexkit/bin. basal's store,<data_home>/cortexkit/basal/store.db, holds only what basal wrote since; keep it for diagnosis or delete it with the binaries. - Later placements: with
--place, place-module.sh keeps the previous binary as~/.local/share/cortexkit/staging/<binary>.rollback-<timestamp>with its sidecar (the newest three per binary are kept), and the operator places it back with--older.--olderonly waives the currency arm: the rollback still passes every other arm, so its marker and control are chosen against the binary being rolled back. Roll back both binaries together: the module and the worker speak a protocol to each other and are built and tested as a pair. - Store: no card so far changes basal's store schema. A card that does must say so, and the operator places it with
--migrates <store>, which snapshots the store first; rolling that card back means restoring the binaries and the snapshot together. - Stage: an unused stage is removed with its directory. If it was declared current, point the
.currentfiles back at the previous stage (or remove them) so the gate never takes a superseded build.