feat(blueprints): saying why a blueprint is untrusted (T79b) - #71
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Roadmap task T79b, phase 8 — design in
docs/superpowers/specs/2026-09-02-t79b-why-a-blueprint-is-untrusted-design.md.
T78a wrote a boolean, and the daemon knew more than a boolean. A blueprint that arrived with no
signature and one whose signature did not verify both reached a person as one sentence —
untrusted: nothing vouches for it, and nothing will— while only the second is the event thegallery key exists to catch. The difference survived in the daemon's log and nowhere a client could
read it. Found by T79a's acceptance run and left alone there rather than widening that task into
mixengine-proto.Trust is still decided once. Nothing here re-checks a signature or moves a
trustedcolumn;this is a reason beside an answer that was settled when the row was written.
What changed
mixengine-proto:SignatureCheck(verified/missing/rejected) onBlueprintSummaryand
BlueprintPlan, besidetrustedrather than instead of it.mixengine-core:Trust— one value the write side settles, which derives both columns, so theanswer and the reason cannot be set apart. Migration
0015addsblueprints.signature.mixengine-daemon:vouched_foranswersTrustinstead ofbool. It is the function thatalready knew all three cases and threw two away.
mixengine-cli: three sentences where there was one — at import, in theTRUSTcolumn(
signed/unsigned/mismatched), and in the question asked before a[scaffold]commandruns.
Three decisions worth reading
The reason had to be a column.
blueprint.listreads rows, so a test asserting only whatimportreturned would stay green with the migration broken; the daemon test reads the listing backfor that reason.
ON CONFLICT DO UPDATEis where a stale reason would have come from. Withoutsignature = excluded.signature, re-importing an unsigned file over a verified row leavestrusted = 0besidesignature = 'verified'. Its test was checked by removing that line andwatching it go red.
No fourth variant for "signed by another key." The only thing that could tell it from "signed by
the gallery and then edited" is the key id inside the
.minisig, which is not authenticated —whoever edits the file edits the key id with it. So the sentence is "a signature came with it, and
it is not the gallery's", which is true of all three failures the verifier folds together, including
a colleague's own key and a corrupt
.minisig.The migration backfills only the knowable half: an
importedrow that is trusted can only have comefrom a signature that verified; an untrusted one is either of the other two, and stays NULL rather
than guessed.
Pinning the reason into
ScaffoldConsentwas considered and declined —commandis already pinnedand both untrusted kinds sit behind one flag — with the case that would reopen it written into the
design's D9.
Verified on a machine
Against the sandbox home T79a left behind, which holds rows written before this task, so the
migration ran on real data rather than an empty table:
laravel(imported, trusted) backfilled toverified→signed;bareandedited(imported,untrusted) stayed NULL →
untrusted, not a guess.nothing came with it to vouch for it; with a file that is not asignature →
a signature came with it, and it is not the gallery's; with the gallery's ownpublished pair →
signed by the gallery key; with one byte appended to those signed bytes →untrusted again.
signed/unsigned/mismatched, and still does after the daemon isrestarted — the half that proves the column.
Not covered by a test, and why
The
Verifiedarm is unreachable from the test suite: the daemon checks against the compiled-ingallery key and its secret half lives in the packaging repository's Actions secrets. It is proved
the way T79a proved it, with a published pair from the
blueprintsrelease tag. The[scaffold]question is only asked on a real apply, so its sentence was extracted into a pure function and
tested there.