Changelog
What changed in Marque, newest first. Each entry links the decision record that is its source of truth.
Entries are authored one file per change under
docs/changelog/ and assembled
here at build time — see its README
for the format.
2026-08-20 — The walking skeleton walks
M1's sentence, executed against a real database: submit a statement, store it, approve it, run it against a target, and the result and the statement land in a table. The implementation plan's exit criterion for the milestone is a test that runs those six steps and asserts the row changed — because every step can report success while the database does not change, and only the last assertion knows the difference.
It is not secure, and it says so whenever it does anything. M1 has no signing, no grammar, no identity and
no fence, which means an approval is a name the caller typed. Every command that touches anything
refuses to run without MARQUE_INSECURE_SKELETON=1, and prints a banner naming each of those
absences — because a banner that says "not secure" and stops is one people learn to skip. version
is the deliberate exception: inspecting a binary should not require agreeing to what it would do if
you ran it. M5 deletes the flag; the test that
asserts it is gone is written now, skipped with that reason, and greps the built binaries rather
than the source.
Added
- Three binaries that do something.
harbourmaster serverecords requests, approvals and reports;pilot executeruns one approved statement against a target and reports what happened;marque submit | approve | statusis the operator's client, generated from the same schema (EDR-0020). harbourmaster migrateis a command, and startup is not. Serving verifies the schema and refuses against one the binary does not match, rather than quietly changing it — migrating implicitly turns every deploy into a schema change nobody chose to run (EDR-0042).- The Pilot is the only thing that touches a target, and the only thing that holds a target
credential: it takes
--target-dsnand the control plane never sees it (EDR-0005). It runs one statement per invocation, because a long-lived Pilot with a queue runs statements with nobody watching, and M1 is not where that should first exist. - The Pilot asks the control plane whether it may run. A request the control plane calls pending is refused. Executing one anyway would make the Pilot the thing that authorises work, which is the arrangement the whole design exists to refuse.
What the outcome vocabulary is for
The Pilot's four outcomes are most of M1's value, because the outcome is what the control plane records and an outcome that lies is worse than no Pilot at all. Two of them look identical from the outside and are not:
- the server refused the commit — a deferred constraint fired, say — so the transaction definitively rolled back and nothing was applied;
- no answer to the commit arrived, so it may have been applied and the acknowledgement lost.
Collapsing them is wrong in both directions. Reporting a refused commit as indeterminate sends a
human to inspect a database that is provably unchanged; reporting a lost one as rolled_back tells
them a statement did not run when it may have. There is no retry
(EDR-0021), because replaying a write after a
failover applies a statement outside the accounting that was supposed to bound it.
The first version of that classification said "the server sent a message, so it refused". Then a
test took the connection away and watched it call a dead backend rolled_back: terminating a
backend sends 57P01, which is a server message. A refusal is now an error the server chose to
return, judged by severity alone — a first attempt also excluded SQLSTATE classes, and a
deferred constraint trigger that raises at commit rolls the transaction back while returning a
SQLSTATE the trigger itself chooses — P0001 by default — so the class carried no such meaning.
Two bugs worth the telling
Idempotency lost a race no sequential test could see. Submit and RecordExecution are keyed —
on the caller's key and on the attempt's nonce — and both were written as
INSERT … ON CONFLICT DO NOTHING with the reference read back in the same statement. That returns
nothing: eight goroutines on one key, five of them got sql: no rows in result set. Both are now
DO UPDATE with a no-op SET, which takes the row lock, waits, and returns the surviving row
whichever way the race went.
The reason is worth getting right, because the first version of this entry got it wrong and said
the concurrent row "cannot be seen until it commits". DO NOTHING does wait — measured at two
seconds — and a later statement sees the committed row without trouble. The fallback SELECT
was part of the same statement, and a statement runs on one snapshot taken before the wait began.
The bug was using two snapshots, not a visibility rule, and the wrong diagnosis would have sent
someone to add a retry.
A CHECK constraint cannot be DEFERRABLE. Only UNIQUE, PRIMARY KEY, FOREIGN KEY and
EXCLUDE can. Assumed while writing a test, then measured — which is this project's recurring
lesson and the reason the previous entry exists.
What this still does not do
Everything the milestone said it would not. No statement is parsed, so DROP TABLE and
UPDATE … WHERE id = 1 are the same to it. No approval is verified, no scope is applied, no
rehearsal is run, and nothing is signed. The tenant comes from configuration and the submitter is the
fixed string unauthenticated, because there is no identity to take either from
(EDR-0025), and every request records its
submitter as unauthenticated, which is the truth.
2026-08-20 — A schema, a migrator, and a rule that had quietly stopped being true
The implementation plan listed "The control-plane storage schema and its migration tooling" as decision debt owed before M1 closes. EDR-0042 discharges it — and found, on the way, that one of the corpus's mechanisms had been unachievable for five days without anyone noticing.
Fixed
-
EDR-0005's driver rule could not be implemented. It says the Harbourmaster "has no database driver for target engines linked in". But EDR-0013 fixes Marque's own state on PostgreSQL, and PostgreSQL is a target engine — one driver serves both, and the control plane must link it.
The sentence survived because it was never tested against reality: the Harbourmaster had no storage code, so it linked no drivers of any kind and the rule was true by vacancy. M1 is the milestone that ends that, which is why M1 is where the rule had to be re-expressed.
EDR-0042 replaces absence with import discipline — a driver confined to the two packages that need one, the Harbourmaster's store and the Pilot's adapter, by a check that asks the toolchain what each binary links, with no control-plane exception for an engine Marque does not store its own state in: when EDR-0026 arrives, a MySQL driver's only permitted home is the Pilot's adapter. PostgreSQL is the single weakened case, weakened only because EDR-0013 made it the control plane's own store.
The record is blunt about what that is not. It reads imports, not capability:
database/sqlregistration is process-wide, so once the store package registers a driver any package cansql.Openit without importing anything the rule can see. It buys "the capability arrives by a reviewed edit rather than by accident", and nothing stronger. EDR-0005's sentence is amended in place rather than left standing, because two accepted records contradicting each other is worse than either being wrong, andCLAUDE.md's invariant list is corrected for the same reason.
Added
- The migrator runs against a real PostgreSQL, behind
//go:build integrationsomake teststays offline, and run bymake test-integrationand a CI job. It watches the things only a database decides: that four concurrent migrators serialise to one applied row and leave no advisory lock; that the connection the migrator poisons carries neither itssearch_pathnor itslock_timeoutback to the pool; that a heldACCESS EXCLUSIVElock produces a bounded refusal rather than an indefinite wait; that the grants land and the runtime role owns nothing and holds noDELETE; that a decoyschema_migrationsin an earlier schema does not become the history; that an edited applied migration is refused; and that each closed vocabulary, the length bounds, the idempotency constraints and both composite foreign keys refuse what they say they refuse. Every one was chosen by deleting the code it covers and watching the test go red — and two of the tests failed that check because they were wrong, one planting a decoy function where only a decoy table could have worked, and one setting a deadline shorter than the timeout it was testing. - A tenant-partitioned schema from migration one, per
EDR-0025 —
tenant_idleading every index that matters and present in every unique constraint, filled from one configured development tenant while M1 has no identity to derive it from, and never a request field. - A migrator that refuses more than it applies. Embedded, numbered, forward-only, each migration
recorded with a content digest in the transaction that applies it, and the applied set required to
be an exact prefix of the embedded set. It is designed to run as an explicit command, with startup
verifying and refusing to serve on any mismatch — migrating implicitly at startup turns every
deploy into a schema change nobody chose to run. Neither caller exists yet:
MigrateandVerifyare functions the tests drive, and no binary links the store. The command and the startup check arrive with the services at M1 step 3. - One vocabulary borrowed, one decided, because a forward-only schema makes widening a column an
unnecessary migration.
requests.statetakes all seven of EDR-0038's values even though M1 produces four.executions.outcomecould not be borrowed: EDR-0011 names three outcomes, closes no set and settles no success token, so the record decides that column and says so.
Three things M1 gets wrong on purpose, named individually so a reader who sees one does not assume
the rest is right: approval is a row rather than a signature — carrying stage, so it is not the
flat shape
(EDR-0030 exists because flat was a
defect); requests.state is authoritative where EDR-0012
makes current state a disposable projection; and executions is a control-plane report whose nonce is a
report key and not a claim — EDR-0011's ledger is
Pilot-local, claimed before the statement runs, carries an incarnation, and is the fence itself.
None of that is in this table.
Where the Pilot keeps that ledger is undecided, and no record says — issue #34, due before M5. The target database is the wrong answer for a reason worth knowing: a role that can write a Marque-owned table is a role with more grants than the operator's statement needs, which is the opposite of what EDR-0005 is for.
This is the first record forced by storage implementation, and it shows — though not the first forced by implementation at all: EDR-0040 came out of trying to defeat a guard rather than reading it. A design read end to end looks consistent; a design you try to build tells you which of its sentences were never load-bearing.
2026-08-20 — The Dependabot security groups were inert, and an obligation was left where nobody would read it
Review of the Dependabot configuration landed after it merged, and found two things wrong with it. Recorded rather than quietly patched, because both are instances of failure modes this repository names explicitly.
Fixed
-
The three
security-updatesgroups could not do anything. They shape the pull requests GitHub's Dependabot security updates feature raises; they do not raise any. That feature was off for this repository — alerts were on, security updates were not — so the security-grouping portion of that change was behaviourally a no-op. The rest of it stands: scheduled version updates are grouped andpg_query_gois isolated — neither has fired yet, since the weekly schedule has not come round and the dependency does not exist. It is on now, verified through the API, which makes the claim true rather than weakening it. The config says the groups depend on that setting, so the next person to find them quiet has somewhere to look.The sharper part: the same change had deleted the Actions security group for being "inert while reading as coverage", and left three more in precisely that condition. The standard was right and was applied to one case out of four.
-
An obligation was stated on M2 and written only in
.github/dependabot.yml. The config said "the major-version upgrade needs a human watching releases, and M2 is where that obligation lands" — and the implementation plan said nothing about it. That isCLAUDE.mdrule 9, and the plan already carries the counter-example three sections earlier: "that obligation is written into M2's own exit criterion rather than left here, because whoever implements M2 will read M2."M2's numbered steps now carry it, as step 6:
pg_query_gouses semantic import versioning, so/v6and/v7are different module paths and Dependabot will never propose that upgrade at all. The first draft of this made it "a standing watch with a named owner" and told M2's exit criterion that this one item was a duty rather than a test — an exception to a rule the same page writes "without exception", which is the species of defect this entry exists to record. It is a scheduled check instead: compare the module path pinned ingo.modagainst the latest released major, fail when they diverge, and let M2's exit demand it be seen to fail like every other.
The two are worth telling apart. The inert groups depended on a repository setting no diff can show, so reading the merged result was the only way to find them. The rule-9 violation was visible in the original diff — a file assigning M2 an obligation while M2 went untouched — and was missed anyway, which is the more useful of the two lessons.
2026-08-20 — Dependabot, configured before the dependency it exists to guard
CLAUDE.md and EDR-0039 legislate a
rule about pg_query_go upgrades that had nothing to attach to: there was no .github/dependabot.yml
in the repository, so the rule described a configuration that did not exist. That is the
claim-without-its-mechanism failure this corpus is otherwise careful about, and it is cheapest to
close now — the dependency arrives at M2, and a guard that lands after the first bump is not a guard.
Added
.github/dependabot.yml, four update locations across three ecosystems:gomodat/and at/tools— two module graphs on purpose, so the linter's dependencies stay out of the service's —npmat/website, andgithub-actionsat/.- Each location groups its updates, and every group says what it applies to. One pull request per
directory per dependency is the pattern that produces a queue nobody reads. The
/toolsgraph is why this is not optional: buf, golangci-lint and goreleaser carry 537 indirect requirements between them. pg_query_gois excluded from the Go groups, so its bump arrives as its own pull request rather than one line inside a batch. The dependency does not exist yet; the entry is written first deliberately.
Fixed
- A group with no
applies-tocovers version updates only. The first draft of this file grouped everything and then claimed in its own comments that security fixes therefore arrived consolidated too. They would not have: security updates would have stayed ungrouped, and thepg_query_goexclusion would not have held for them either — which is the one case where isolating it matters most. Every group now declaresapplies-toexplicitly, including where the value is the default, and the three Go and npm locations carry a parallelsecurity-updatesgroup.
Actions deliberately have no security group, and that is a cost of SHA pinning. Dependabot does not raise alerts for an action pinned to a commit SHA, and a security update is driven by an alert — so the pinning enabled for #8, which is the right call and stays, means this ecosystem gets weekly version updates and no Dependabot security updates. A fourth group would have been inert while reading as coverage. Nobody had written that trade down; it is written down now.
Two limits are written into the file, because it reads stronger than it is.
The exclusion covers minor and patch bumps, not the major. pg_query_go uses semantic import
versioning: the module is …/pg_query_go/v6, and /v7 is a different module path. Dependabot does
not migrate Go imports across a major suffix, so it will never propose that upgrade at all — with or
without an ignore entry, which is why there is not one. The upgrade EDR-0039 cares about most is
therefore the one no tool will raise, and M2 inherits the obligation to watch releases by hand.
Dependabot cannot require review. allow_auto_merge is false for this repository, which disables
GitHub's native auto-merge — it does not stop an installed app or a workflow holding write permission
from calling the merge API. The invariant "a pg_query_go bump is read by a human" needs a
branch-protection rule with no automation bypass, and this file is not that rule.
2026-08-20 — The build now checks the vocabularies it used to trust you to remember
Three closed vocabularies were written down twice — once as a constant the build enforces, once as a
table in a README that people actually read — with nothing asserting the two agreed. CLAUDE.md
stated the coupling as a rule, "adding a tag means editing both", which is the honest admission that
nothing enforced it. This repository's own position is that a rule depending on someone remembering
it is not a rule, and there were three instances of the same hope.
Added
- The build parses each README table back out and compares it to the constant, failing with the value and the file to fix: "'in-flight' is enforced but undocumented — add it to docs/edrs/README.md". It covers the changelog tags, the EDR implementation states and the EDR statuses, by one mechanism rather than three — a fix for one that ignored the others would just have doubled the inconsistency.
- Order is compared too, for the two vocabularies where order is load-bearing: implementation
states run most-built to least and the roadmap reverses them, and the changelog filter bar renders
tags in array order. Statuses are a
Setwith no meaningful order and are compared as one. - Each table is located by an explicit
<!-- @vocabulary:… -->marker, not by guessing which table in the file is the vocabulary. A heuristic that silently matched the wrong table would be worse than no check, because it would pass while comparing nothing — so a table that loses its marker fails the build rather than quietly stopping being checked.
Fixed
docs/changelog/README.mdclaimed the constant was "the only place it is written down" while sitting directly above a second copy of it. The same sentence appeared as a comment inwebsite/build.mjs. Both now say the vocabulary lives in two places and name what keeps them together.- The EDR status vocabulary was the third instance, which the original report did not count. It is the same shape and is covered by the same mechanism.
Every failure mode was seen to fail before being relied on — a value removed from a README, a value added to one, the same values reordered, a marker deleted — each producing the intended message and each restoring cleanly to green.
That was not sufficient, twice. Review made the build pass while the vocabularies disagreed in seven distinct ways across two rounds — an annotated row the value pattern did not match; a duplicate marker shadowing the real table; a value listed twice, invisible to a membership test; a row with a leading space, which GFM permits; a row with no leading pipe, which GFM also permits; a value smuggled into the header row, which was skipped unconditionally; and a marker with a decoy table inside a fenced code block.
The root cause of the last four is one decision: the table was inferred by matching lines that
begin with |. That is not what a markdown table is, and the build already had a real parser
imported for other purposes. The check now reads the AST — a marker is only a marker if it is a
top-level HTML node, so a fence cannot hold one; the header is a row like any other and is checked
rather than skipped; and the shape of a row is a fact about the tree rather than a guess about the
text.
A third round found two more, and they are the reason a value is now matched against an explicit
lowercase pattern rather than trimmed: String.trim() strips U+FEFF and NBSP but leaves U+200B
alone, so an invisible character inside a value was accepted, rejected, or silently normalised
depending on which invisible character it was — three behaviours, none of them chosen. And a second
table immediately after the vocabulary one, which reads to a person as more of the same vocabulary,
went unchecked.
The lesson is not that the mutations were wrong. It is that they were chosen from the same mental model that wrote the code, so they tested the four failures its author had thought of — which is exactly how a guard ends up trusted for more than it does, and is the same defect one level up as the arrangement it replaces. All twelve directions now fail, and are recorded here so the next person to extend this knows which ones were not obvious.
2026-08-19 — One spelling for a scope, and canonicalisation that normalises nothing
The corpus agreed on the decision and disagreed on the encoding. Every record said a delegated row scope is checked by syntactic containment and never by entailment; they then wrote the field down two different ways, and specified a check that compares one against the other. This is the second time that shape has surfaced — the first was a record claiming another had gained a field it had not — and it is worth naming, because both times the disagreement was invisible to every reader who already knew what the design meant. Only someone reading the artefacts as an implementer would see it.
Added
- EDR-0041 picks the spelling. A fence is a list of
conjuncts everywhere; a relation is a
schemafield and arelationfield; an operation is lowercase. Four asymmetries are recorded as deliberate — including a vector'spredicatestaying a single string, because a vector's predicate and a grant's fence are composed rather than compared. - "Canonicalisation" in check 7 is defined, and it normalises nothing. It folds no whitespace, no
identifier case, no ordering and no Unicode form: two conjuncts are equal when their decoded
characters are equal. Deciding that two different texts mean the same predicate is the parser's
job, and routing a fence comparison through the grammar would let a
pg_query_goupgrade change what an already-signed delegation permits. (Statement canonicalisation, which EDR-0004 calls part of the security boundary, is a different question and is still open.) - A conjunct must parse standalone, and the Pilot checks it before composing. Wrapping an element
in parentheses is only sound if the element is one expression:
tier = 'sandbox') OR (1=1survives wrapping, erases the tier bound, and is identical to the artefact it came from.
Changed
- The structural rules are Pilot refusals, not authoring conventions. An empty fence array, a
duplicate conjunct, an empty-string conjunct and a malformed one are refused where they are
verified rather than where they are written — a rule the Harbourmaster enforces on itself is not a
rule. What they buy is stated narrowly: they are for the reader, not against an adversarial author,
who would simply omit the key, which is legitimately no row restriction.
"fence": []looks like a restriction and is none. - Seven records, the agents page and the conformance format were amended, each record with a dated changelog line. No record is superseded: every decision stands, and only its encoding was wrong.
- The grantor of a compiled delegation signs a list rather than a predicate. A legibility cost landing on the record whose whole subject is the person signing, so it is named there.
Fixed
- The fence SQL had reopened a fail-open the corpus already closed once.
EDR-0007's worked transaction reads
AND (fence) IS NOT TRUE, which is correct for one conjunct and wrong for two:ISbinds tighter thanAND, so(c1) AND (c2) IS NOT TRUEtestsc2alone and a row failingc1is never counted. The record now says what<fence>denotes — the bare conjunction, wrapped by the template — and the rule is in M5's exit criteria and inCLAUDE.md's invariant list rather than only in the record that introduced it. The session settings then failed in four further ways.standard_conforming_stringsandbackslash_quoteare read by the lexer, so aSETsent in the same message is inert while the GUC still reads back correct. And three kinds of code the Pilot does not compose can move a pin out from under the checks that follow: aBEFOREtrigger on the target, a deferred constraint trigger fired bySET CONSTRAINTS ALL IMMEDIATE, and the fence's own evaluation, since a conjunct may call a function. The pins are re-verified before every step that follows any of them, which bounds the damage without preventing it — a function that restores the setting on exit leaves every later check passing. - A relation had three spellings, not the two reported.
EDR-0037 had already split the field and named the second half
table. Every grant now reads{ "schema": …, "relation": … }. - EDR-0007 stopped contradicting itself. Its attenuation rule called the fence an array eleven lines below an example showing a string.
Six questions surfaced by writing the encoding down, and each is left open here because settling it
changes a rule a different record decided. An agent's effective fence is the union of three
conjunct sets, so it is tighter than its delegation's and an identity check refuses it for being
tighter (#20, Phase 3b). A wildcard relation is
spelled "*", which PostgreSQL also accepts as a real relation name
(#22, before the first grant carrying one is
signed). The signed display renders a fence and now
has a list to render (#23, M3). The subset version a
delegation is pinned to has no carrier on any artefact
(#24, M2). And what a conjunct may reference —
functions, casts to domains, subqueries, explicitly-qualified operators — is bounded by nothing at
all (#25, M5) — EDR-0007 rule 5 puts a fence
needing another relation outside the subset at submission, which is a different check at a
different time and detects nothing at composition. That one is largely pre-existing and
is the reason to say plainly that this record bounds a conjunct's shape and not its behaviour. And
break-glass lists the fence among the controls it leaves unchanged while no signed artefact on that
path carries one (#26, Phase 2).
Until the first is settled an agent has no fast path, which is the fail-closed answer and is written down so nobody reaches for the other one.
2026-08-17 — M0 closes: the toolchain, the schema, and the guards that hold them
Implementation has started. The first milestone of the implementation plan is the one that builds nothing a user can see and everything that makes the next seven cheaper — the toolchain, the release skeleton, and the checks that fail when a convention is broken rather than when someone notices.
Nothing here runs against a database. The roadmap is the honest account of that: 38 of 40 decision records remain unbuilt.
Added
- Three binaries —
marque,harbourmasterandpilot— as separate programs rather than one with a role flag, because four components with sharply different trust is the whole security argument (EDR-0001). - One schema, and clients generated from it.
proto/marque/v1with committed Go and Connect stubs,buf breakingon every pull request, and a build that rejects a method which does not declare whether repeating it is safe (EDR-0020). - EDR-0040 — a method's declared
behaviour may only strengthen.
buf breakingignores custom method options entirely, so a method could be reclassified from read-only to never-retry with every check green, while clients built against the old schema carried on retrying it. A separate check now compares every method against the base branch. - An implementation state on every record, and a page derived from it.
statussays what was decided;implementationsays what exists. They are orthogonal on purpose: a record is routinelyacceptedandnoneat once. - The conformance-vector harness.
testdata/conformance/statements.jsonis normative and currently empty; the format and its validator exist now so that the first vector to arrive is checked on arrival. The vectors and the grammar that runs them are M2. - A release skeleton that releases nothing. goreleaser builds a snapshot on a native runner for each of linux and darwin on amd64 and arm64, so a broken release matrix is discovered on a pull request rather than at version 0.1.
Changed
- The documentation site records what is built. Every record carries an
implementationfield the build refuses to let you omit, and/roadmap/groups records by it — derived from the frontmatter and nothing else, so there is no second list to fall out of step. - The docs workflow holds write permissions only on the job that deploys. The job that runs the site build — repository code, from the branch under test — no longer inherits the ability to mint an OIDC identity.
- Builds stamp the source's date rather than the wall clock, so the same commit produces the
same binary, and
SOURCE_DATE_EPOCHis honoured on both build paths.
Fixed
- A guard that reported success having compared nothing.
make breakingtreated "the base branch carries no schema" as a reason to exit 0, which was correct exactly once — while the schema was being introduced. Left in place, deletingbuf.yamlfrom the main branch would have silently disabled both the wire-contract check and the compatibility check. It is now a hard failure, and removing it is what let the check run against a real base for the first time.
2026-08-17 — Every decision record now says whether it is built
A record's status says what was decided. It has never said whether a line of it exists, and at
forty records that gap had become the most misleading thing on the site: forty accepted records
read as a system, and Marque is a design with one scaffolding milestone behind it.
Every record now carries a required implementation field recording what exists, and the site
gains a roadmap derived from those fields and from nothing else — no manifest and no
second list, for the same reason the changelog has no index.
The two axes are orthogonal on purpose. A record is routinely accepted and none at once, and
that is not a contradiction: the decision is settled, and not a line of it is written. Folding "not
built yet" into the status vocabulary was the obvious alternative and it is wrong here, because
proposed requires a proposed_until and fails the build once that date passes. It would have
armed a deadline timer on every settled decision that is merely waiting its turn in the
implementation plan.
The field is required, and the build rejects a record without one. That is the load-bearing part rather than a nicety. A derived page whose source field is optional under-reports the work outstanding the first time somebody omits the field — and a roadmap that under-reports is worse than no roadmap at all, because it is read as complete.
The first tally is 38 of 40 records with nothing implementing them, one partial and one
shipped. Every state was assigned by reading the code rather than the record, which is the only way
the field means anything: filled in from what a record claims, it would reproduce exactly the drift
it exists to detect.
Added
implementationon every record —shipped,partial,in-flightornone, a closed vocabulary ordered most-built to least.partialandin-flightmust also carry animplementation_notesaying which half is missing, or which branch it is on. A note is worth writing on anonetoo, wherever scaffolding exists that a reader would otherwise mistake for the decision — EDR-0039 says that the Makefile exportsCGO_ENABLED=1for it while no parser is linked in.- A roadmap page — grouped most outstanding first, with a tally, and a record's implementation drawn as an outlined badge beside its filled status badge so the two are never read as one.
- Two new build failures — a record with a missing or unknown
implementation, and apartialorin-flightrecord with no note.
Changed
- EDR-0020 is
partial— the schema, the annotation extension, the build failure for an unannotated method and the committed Go and Connect stubs exist;clients/ts/, the Pilot, Surveyor and relay schemas, typed errors, streams and a client interceptor honouringkeyedandunsafedo not. - EDR-0040 is
shipped— oneMethodBehaviourextension, the strengthen-only comparison and theidempotency_levelagreement check, all enforced in CI. Its own Scope section puts client-side retry behaviour outside the record, so the absent interceptor is not a missing half of it.
2026-08-17 — Three CI tiers, named, with the reason each stops where it does
The implementation plan said "CI green on all supported platforms" while the test suite ran on two of the four. That is the shape of disagreement this repository is supposed to catch — a claim in prose with a mechanism that does not meet it — so it is settled rather than left standing.
Added
- Each test-tier runner now asserts which platform it is. The plan names the tier's platforms, so
something has to hold that true:
macos-latesthas already migrated from Intel to ARM once, and without the assertion the table would go quietly false while CI stayed green. It is the guard the snapshot job already had, for the same reason. - The repository requires actions to be pinned to a full-length commit SHA, enforced by GitHub rather than by review. Both workflows already complied, which is what made now the cheap moment: the setting applies at run time, so enabling it against a non-compliant workflow breaks CI instead of a pull request.
Changed
- "Green in CI" now names three tiers. Build-and-smoke on all four platforms; the test suite on one runner per operating system, as a sample rather than a proof; integration on linux/amd64, arriving with M1. Each tier carries the reason it stops where it does — and the test tier says plainly that it is a cost decision, because two attempts to justify it by argument were both false.
- M0's exit criterion reads against those tiers rather than against a phrase that meant whatever the reader assumed, and M1's integration test says where it runs. M2's now carries the obligation to widen the test tier, since M2 is where the parser becomes C and the person implementing it will read M2's criterion rather than a table three sections earlier.
make breakingnames both causes of an unusable base ref. It advised fetching more history, which is the wrong repair when the ref is the all-zeros SHA a branch creation produces, or the orphan a force-push leaves behind.
Fixed
- Every checkout in both workflows now discards the token. Three CI jobs kept it available for
the duration of the build. On a public repository with
contents: readthe exposure is close to nil; the reason to fix it is that a selectively-hardened workflow reads as though the jobs that set the flag needed it for something particular, and none of them do. - A latent trap in the Pages upload is now visible at the line that causes it.
upload-pages-artifactstopped including dotfiles at v4.0.0, and the pinned v5.0.0 is what added an input to override it. Nothing the site emits is hidden today, so this changes no behaviour — but the first.well-known/security.txtwould have been dropped from the artefact with the build green and the deploy successful.
2026-08-16 — An ideas page, and two things that are already refused
Six candidate features are now written down in one place — Ideas — with the reasoning that ranks them and the limits that make each honest.
Nothing on that page is decided, and it says so at the top. No record covers any of it; anything that gets built gets a decision record first. The page exists so a good idea is written once rather than re-derived in three conversations.
The six lean on what this design uniquely has: a corpus of what humans approved and why, a rehearsal engine, and per-relation write measurement.
- Shadow mode — enforce nothing, read the target's own audit, and report what last month would have looked like. Adoption is the top risk in Scope, and nobody switches on a control that gates production without knowing what it will cost them.
- Delegation mining — the logbook is a labelled dataset nobody else can build. Propose the delegation that would have covered 44 of the last 47 approvals, and show the 3 it would have missed, because the counterexamples are what make it a decision rather than a sales pitch.
- Compensating statements — Marque knows the rows and their prior values, so it can generate the revert. Only where the rows were bounded and captured, never where a cascade ran — and the write-set assertion is what makes that limit checkable rather than a warning in a doc.
- Cumulative blast-radius budgets — every budget today bounds one marque; nothing bounds the aggregate, so ten individually-reasonable approvals in a day are invisible.
- Evidence bundles — the anchoring work already done turns a compliance request into something a third party can verify without our cooperation.
- Migration-invalidated delegations — the machinery fingerprint already exists, so tell a grantor when a migration made their signed scope mean something different.
And two that are refused
Approve from Slack. Somebody will ask within a week. EDR-0036 already gives the answer: the signing surface must not be one the control plane renders, and a chat client is strictly worse because a third party renders it too. Notify in Slack, always; sign in Slack, never.
"Approve similar" / approval templates. Bulk approve wearing a disguise, and EDR-0024 refuses the undisguised version. If a shape is approvable repeatedly it is a standing order — which has parameter constraints, an expiry, a rate limit and a review. The template is the same convenience with none of the safety, and would be adopted faster precisely because it asks for nothing.
2026-08-16 — Emergency paths, and making "waiting for approval" a useful thing to be told
Two new records covering the part of the design that was consistently good at refusing and consistently poor at 3am.
Being told no is now useful
EDR-0038. A request is a shareable, watchable object. The refusal carries the reference, the escalation chain with names, who is being waited on right now, the measured rehearsal facts, a share link and how to watch it:
ERROR: outside your delegated scope; submitted for approval
DETAIL: req_01JB2Q9F3K8Z · 412 rows rehearsed, 0 outside fence
HINT: waiting on sam@acme.example (stage 1 of 2), then group:data-oncall
share: https://marque.acme.example/r/01JB2Q9F3K8Z
watch: marque watch req_01JB2Q9F3K8Z
The reference is an identifier, not a capability. People will paste it into shared channels, so holding it must grant nothing — and resolving one as an unentitled principal must 404 rather than 403, so the reference does not confirm its own existence.
marque requests is the work queue: pending and approved together, because "what am I waiting on"
and "what can I run now" are the same question ten minutes apart. Then marque run <ref> and
marque output <ref>. An approved marque nearing expiry unused is surfaced — the most annoying
failure available is waiting for something that already arrived.
Emergency paths
EDR-0037, and the framing matters: an emergency changes who is asked and how loudly, not what is checked.
--urgent notifies every stage at once instead of sequentially, pages instead of messaging, and
adds the target's emergency approvers. It never widens scope. Whether it may collapse a multi-stage
chain to one is a per-target setting, default off — the second stage exists precisely because the
first person's authority was insufficient, so collapsing it lets urgency manufacture authority nobody
granted, and left on by default everyone marks everything urgent within a month.
Break glass is a pre-granted scope that lies dormant. Someone grants you, by name, a scope you may use only by explicitly breaking the glass — typing a justification, confirming deliberately, and producing an authenticator assertion. Per-actor, and the shape is the deployment's to choose:
- "Theo may run any statement if he breaks glass."
- "Theo may run any statement in an emergency if a second holder co-signs."
- "Theo may run any
UPDATEonpublic.*in a break-glass scenario."
The design consequence worth noting: because the grant is a signed artefact, breaking the glass mints an ordinary fast-path marque (EDR-0029) and introduces no new verification case at all. The human signed the shape in advance, exactly as with a standing order. There is no code path that skips a check — which is the difference between an emergency path and a hole.
And it is loud by construction: at the moment the glass breaks, the deployment channel, the target's owners and every stage that would have been asked are notified, naming the person and quoting the justification; a distinct logbook entry; a console banner for as long as the marque lives; and a mandatory post-hoc review that suspends the grant if it goes unread. No configuration suppresses the notification — a deployment wanting quiet emergency access wants a standing credential and should say so.
Why an emergency path at all
Because a control with no emergency path has an undocumented one. An operator watching an outage with a fix in hand and no approver awake will route around anything that cannot answer them, and the thing they route around it with is a standing credential in a password manager — the exact failure this system exists to remove.
The hazard is not abuse; it is drift. The emergency path is faster, so it becomes the normal path. Every mechanism above targets drift rather than malice: loudness, mandatory review, short expiry, and a break-glass-rate-per-principal metric with a stated response — a grant used routinely is a delegation somebody should have written properly.
2026-08-16 — The last seventeen: specification completeness across fourteen records
The second panel's should-fix tail — seventeen items of specification completeness rather than design error, and the kind that becomes an implementer's guess. Fourteen records amended; no decision changed, so nothing superseded. Both panels' findings are now closed.
Where a rule was stated in one place and contradicted in another
- Attenuation still asked for entailment. A delegation had to carry "a fence at least as tight" — the undecidable predicate-containment check that EDR-0029 had already been rewritten to avoid, arriving once per hop in a chain, with the permissive approximation as the failure direction. It now uses the same rule: syntactic conjunct-set inclusion.
- Check 7 had no meaning for a standing order, whose artefact has no
fenceand no per-marque budget counterpart. It now says what it compares per artefact kind. And a delegation chain now ships whole, so a Pilot can verify attenuation at every hop rather than trusting it happened. - Long-lived artefacts had no signing moment. EDR-0030
fixed the temporal acceptance rule for marques with
roster_epoch; standing orders and compiled delegations — which a Pilot also verifies against roster keys, and which outlive marques — carried neither. Both now do.
Where a claim was slightly larger than its mechanism
- The compiler's output constraints do not bound meaning. They make a compilation ungroundable in
fabricated evidence — a literal it never received, a column that does not exist. They do not stop
injected schema evidence steering the compiler to
tier <> 'production'instead oftier = 'sandbox': a wider predicate that is perfectly well-formed. What bounds that is the grantor's signature on the compiled form and attenuation against their own authority, which is now what the record says. prosrcwas not the whole channel.BEGIN ATOMICbodies live inprosqlbodyand come back throughpg_get_function_sqlbody. The exclusion is now a closure invariant — a function that can return the value of an excluded column is itself excluded — because a list of names goes stale and a rule does not.- The role-escalation findings omitted four ways to become powerful:
pg_read_all_data,pg_write_all_data,BYPASSRLS, andCREATEROLE(which on PostgreSQL 15 and earlier lets a role grant itself membership of any non-superuser role). Plus a writable schema on the resolution path — the introspection counterpart of EDR-0007'ssearch_pathpin.
Relation identity, finally stated in full
TOAST relations enter the write set whenever a value crosses the toast threshold, which is data-dependent — so without a rule a rehearsal passes and the execution aborts the first time somebody's value happens to be large enough. They are excluded as storage for an in-scope relation; inheritance children resolve like partitions; and a relation the mapping cannot resolve aborts rather than being assumed benign.
Five residuals added to SECURITY.md
The security page invites researchers to test its claims, so it now states what the records state:
fast-path volume is unbounded against a compromised control plane; the revocation list is signed by
the component whose compromise it exists to remediate; the write-set assertion is blind to TRUNCATE
and to writes on a separate session; a transform provider is trusted for statement content; and
catalog introspection is a read channel over object definitions. The object-scope claim is also
corrected from "the delegation's" to "the marque's", which EDR-0033
moved.
And the deferred row that read as deferring a shipped feature
"Multiple approvals per marque" implied escalation chains were deferred — they ship in the first release and already put several signatures on one marque. What is actually deferred is threshold approvals within a single stage: the payload encodes it, the review UX for collecting a second signature at one stage does not exist.
2026-08-16 — Twenty-two amendments, and the failure mode that produced most of them
The second panel finished: 63 findings, 58 surviving verification, and its synthesis found that two thirds of its own report was already closed by the previous three commits. What remained was 22 must-fixes, every one an amendment — no supersessions, no new records, because none of them reversed a decision.
The failure mode
The synthesis named something worth quoting, because it is now this corpus's dominant defect:
a new record states an obligation on an older one — "EDR-0010's report gains
write_set", "this joins the list in EDR-0004", "EDR-0026's capability table gains…" — and the obligation is not discharged.
Three of those had shipped. Each was written in good faith by a record that believed it had changed
another one, and none had. Two were caught only because a third adversarial read went looking.
CLAUDE.md now carries a rule: if you write
that another record gains a field, a row or a rule, edit that record in the same change — and grep
your own diff for gains, joins the list, is added to before pushing.
Records whose Decision contradicted their own TL;DR
The worst class, because an implementer builds from the Decision section:
- EDR-0030 restructured
approvalsinto per-stage thresholds in its TL;DR and left the verification steps specifying the flat encoding it had replaced — so building the verifier from the Decision reproduced exactly the defect the restructure fixed. - EDR-0004, the record a reader is sent to first, still said the Pilot verifies "given the deployment's JWKS … the subject against the authenticated caller" — verbatim the construction EDR-0031 exists to close, and the check EDR-0032 replaced.
- EDR-0011 step 1 still checked the caller
against
sub, the dependency that broke offline execution for the caller.
Genuine residuals
- A rebuilt Pilot starts at genesis. Making the roster's epoch high-water mark durable fixed a
restart, not a re-deployment: a fresh container or restored volume begins at genesis, the genesis
roster chains validly to the pinned root, and a compromised control plane walks it forward
reinstating every retired key — the exact rollback EDR-0031's May-not table says is impossible. Now
bound to the Pilot incarnation, with a
min_epochfloor pinned at deployment. pg_stat_xact_all_tablesis not what I said it was. Those are the backend's pending, session-scoped counters on a flush throttle, and connections are pooled — so back-to-back executions could read the previous one's relations. The check is now a delta captured atBEGINand beforeCOMMIT, which is exact regardless.- The write-set assertion is blind to
TRUNCATE(it zeroes counters rather than incrementing them) and to writes on a separate session. Both are now stated, with thepg_relation_filenode()detector for the first. - The machinery fingerprint had nowhere to live. It was bound inside the
analysisdigest — which is one-way, is never delivered to the Pilot, and does not exist at all on a fast-path marque. Now its own signed payload field. - "Narrow or veto, never widen" was overclaimed. The three mechanisms in
EDR-0028 enforce containment within the
submitter's authority; they do not enforce narrowing. A transform rewriting
WHERE id = 42toid = 43stays in scope and passes every check. The record now says so, and says what a checkable version would require. - Break-glass reversion could not be signed. An automatic reversion has no signers, so it could not produce the k-of-n policy epoch EDR-0036 requires. Both epochs are now pre-signed at apply time.
- Recovery under the roster is heavier than the old ceremony. With no live approver keys, no epoch can be signed at all — so recovery means a new genesis roster and re-pinning every Pilot by re-deployment, not reopening a window.
Propagation
EDR-0003, EDR-0006 and scope.md still carried the execution-freshness clause
EDR-0035 was written to remove — and
since the records win over the synthesis pages, the authoritative text still shipped the failure that
record documents. require_execution_presence existed in one record and in no schema. EDR-0023
contradicted EDR-0015 on the critical signing default. EDR-0008 had no objects field although
EDR-0033 sources the fast-path reference set from it. EDR-0005 was the only amended record in the
corpus with no dated changelog line — rule 1, broken in the most visible place.
All closed.
2026-08-16 — A second panel: an authentic signature over a payload nobody saw
A second expert panel reviewed the records the first never saw — EDR-0028 through 0035 — plus the nineteen amendments the first review prompted. It found something that defeats the design after it had already been reviewed once and hardened, and the fix is EDR-0036.
The finding
The console is static assets served from the same origin as the API — that is, by the Harbourmaster. A WebAuthn assertion attests user presence and consent to tap; it attests nothing about the payload, because the challenge is an opaque 32-byte digest.
So a compromised control plane serves modified console assets, renders one marque on screen, and calls the authenticator with the digest of a different one.
The signature that comes back is genuine. The key is on the roster. approvals.required is satisfied.
cnf.jkt verifies. Every offline check added by EDR-0029, 0030, 0031 and 0032 passes perfectly —
on a payload no human ever saw. The approver limb is not forged; it is induced.
Two companion findings shared the root cause. approvals.required is composed by the Harbourmaster
before any signature exists, so the adversary authors the requirement it is bound by. And cnf.jkt
had no anchored source at all — EDR-0034
reached for "the enrolled principal set distributed with it", a phrase with no schema, no author, no
k-of-n rule and no genesis root anywhere in 35 records. That one sentence quietly reopened the
extraction oracle that record was written to close.
The lesson
A signature is only as good as three things: the key set, the payload the signer actually saw, and the policy that payload claims to satisfy. EDR-0031 anchored the first. Anchoring one of three is not a third of the property — it is none of it.
Galling, because the design had already got this right twice elsewhere. EDR-0016 makes a grantor sign the compilation, not the sentence, precisely because a human must sign the artefact they can read. EDR-0028 takes the digest after transformation for the same reason. Neither insight was carried to the surface that renders the payload.
Changed
- Policy becomes an anchored artefact, co-signed by k approvers and epoch-chained like the roster. A Pilot recomputes the approval requirement and refuses a marque whose payload disagrees.
- The payload carries a signed
display— a canonical rendering shown verbatim to the signer and re-renderable by an auditor, so a substitution is detectable from the artefact alone. - The roster becomes the principal roster. Entries gain a capability, so operator and agent keys
ride the same anchored artefact and
cnf.jktresolves against something real. criticalsigning leaves the browser.signing_surface: localis now a setting distinct fromrequire_envelope, defaultingcriticalto the installed CLI, which renders exactly what it signs.
A regression of ours, corrected
Making webauthn the required envelope on critical targets — added a day earlier — was worse than
what it replaced. It forced the highest-consequence approvals into the browser, the one surface the
control plane serves, and excluded the locally-installed CLI. Envelope governs the key; surface
governs who renders. They are orthogonal and were conflated.
The cost of the fix is real and is not hidden: critical approvals are no longer possible from a
phone, which is exactly what EDR-0024 was optimised for.
The browser was never a sound signing surface against an adversary who serves it.
Four more criticals from the same panel
Round two also found four execution-layer defects, all confirmed and all now closed:
search_pathwas never pinned. PostgreSQL resolves unqualified relations, functions and operators through it, so a fence written astier = 'sandbox'can be redefined by anyone able to create an object in an earlier schema. The session now pinssearch_path = pg_catalogand a fence containing an unqualified non-builtin reference is refused at compile time.- A
DEFERRABLE INITIALLY DEFERREDconstraint trigger fires atCOMMIT— after the write-set assertion has read a clean write set — so its writes landed inside the committed transaction unchecked, by a mechanism designed to defer until commit.SET CONSTRAINTS ALL IMMEDIATEnow runs immediately before the check. - The write-set assertion could not tell "nothing was written" from "the write was not counted".
Both read zero, so
track_countsbeing off silently degraded the strongest containment in the design into a no-op reporting success. It now calibrates against the statement's ownRETURNINGcount and refuses if the counters disagree. - A transform provider would have broken the standing-order fast path, because
EDR-0029 requires the Pilot to recompute
template + bindingoffline and matchreq. Transforms no longer run there; tenant scoping on a standing order belongs in the signed template.
Plus two majors: a fence may now reference only columns of the target relation (REPEATABLE READ
protects rows this transaction writes, not a tenant row a concurrent transaction changes), and
Marque's role must not own the logbook table — an owner can grant itself anything, which would
have made the withheld DELETE grant decorative.
The rest of round two's survivors
Fourteen more, all now closed. Three changed a security property rather than a specification:
approvalscollapsed a conjunction into a disjunction. A flatrequired: 2over a flateligible: [sam, group:data-oncall]is not a stage-preserving encoding of EDR-0019's chain — it was satisfiable offline by two members of data-oncall with no signature from Sam at all, on a chain whose first stage was Sam. The block now mirrors the chain's stages, each threshold met by distinct principals from that stage's own set.- Withholding a roster silently extended key validity. With no
next_updateand no stated failure action, a Pilot on a stale roster kept honouring keys retired in an epoch it never saw. It now refuses past the bound, so withholding degrades to denial of service. The epoch high-water mark must also be durable — a memory-only one resets on every deploy, which is the rollback defence gone. - The WebAuthn envelope checked the challenge and nothing else. No
type, noorigin, no rpIdHash, and no ceremony domain separation — so an assertion was replayable from another origin, or an enrolment assertion replayable as an approval. The whole W3C §7.2 set is now specified.
And: "within" is decidable for a fence (syntactic identity — predicate containment is undecidable
and a semantic reading would approximate in the permissive direction); the revocation list's revoked
array is typed, because the fast path verifies artefacts and a list of marque ids cannot revoke the
standing order that keeps minting them; require_key_backing splits from require_envelope, since
es256 covers both a Secure Enclave key and the file fallback; and a 40001 abort now has a state to
land in (aborted_not_applied), which EDR-0007 had
been claiming without one.
Two corrections of overstatement, both ours: the compromised-control-plane residual counted rate
limits and budgets that the compromised component itself enforces — fast-path volume is unbounded
against it, and that is now said; and EDR-0033 claimed EDR-0026's capability table had gained
fence.write_set, which it had not.
2026-08-16 — The rest of the panel's must-fixes: a trust anchor, a fence that failed open, and a read channel nobody had bounded
The expert panel's full result: 109 findings, 98 surviving both an attempt to refute them and a search of the corpus for whether they were already addressed. Its verdict called the corpus "unusually healthy for a design-stage artefact", with what survived concentrating in three places. All seven must-fixes are now closed.
The one that mattered most
EDR-0031 — the Pilot was learning which keys are approvers from the Harbourmaster. Verification was "against the deployment's JWKS", served from the control-plane origin, and enrolment countersignatures were checked by the Harbourmaster too. So a compromised control plane could generate a keypair, publish it as an enrolled approver, and sign both limbs — making the two-signature design a detective control rather than a preventive one against exactly the adversary it was built for. It also silently undid EDR-0029 and EDR-0030, written days earlier to close earlier findings.
The enrolled set is now a co-signed, epoch-chained roster — signed by k already-enrolled approver device keys, never by the control plane, verified back to a genesis root each Pilot pins out of band at deployment, with monotonic epochs so a stale roster cannot be replayed and digests written to the logbook's external anchor.
Added
- EDR-0032 — three bindings other
records asserted and the payload never carried.
cnf.jktso the caller proves possession directly to the Pilot: offline execution had been built entirely for the marque and not at all for the person holding it, whose access token expires one lifetime into the outage. Plustenant(so cross-tenant confusion fails signature verification, as EDR-0025 always claimed) andpilot(so a budget of one cannot be spent once on each of two Pilots). - EDR-0033 — a fourth fence
check.
DELETE FROM accounts WHERE id = 42with anON DELETE CASCADEchild returns one row, satisfiesmax_rows = 1, passes the fence post-assert, and destroys millions of rows in a table no delegation names.RETURNINGreports only the target relation, and referential actions are not gated by the invoking role's privileges. The transaction's per-relation write counts are now read before commit and asserted against the declared object scope — which catches cascades, triggers and rewritten targets alike, rather than enumerating them. - EDR-0034 —
RehearseandIntrospecthad no stated caller check, so a compromised control plane had an exact-count oracle over every target via rolled-back rehearsals. Every Pilot method now verifies a submitter signature; the control plane relays, it does not authorise. - EDR-0035 — requiring a fresh
interactive authentication to execute against a
criticaltarget broke offline execution on exactly the targets the playbook holds break-glass marques for, and locked agents out of the flow escalation exists to serve. Freshness belongs to the approval.
Changed
- EDR-0007's fence SQL was unsound in two independent
ways, and the decision is unchanged while the encoding was wrong.
NOT (tier = 'sandbox')does not count a row whosetieris NULL —NOT NULLis NULL andWHEREadmits only TRUE — so a NULL-fenced row passed the pre-check, the post-assert and the row count with no concurrency involved at all. Every comparison is nowIS NOT TRUE. And no isolation level was named, soBEGINgot READ COMMITTED and the pre-check and the statement took different snapshots; the execution transaction is now REPEATABLE READ. - EDR-0028 described the Surveyor as a
verifyprovider with outcomesveto/refer. Its outcomes areconforms/refer, andvetois precisely the power EDR-0017 says it must never have — an implementer building from that table would have shipped a Surveyor that can deny. - EDR-0005's "no database access" is corrected to "no credential and no ability to commit a change; a bounded, quota'd, target-visible read channel remains", in the record and in both compromise tables.
What this says about the process
Three of these — the trust anchor, the NULL fence, the cascade — are cases where the design stated a property confidently and the mechanism did not deliver it. None was found by writing more records; all were found by an adversarial read with a specific expert lens pointed at a specific claim. The records being the source of truth is what made that possible: a claim and its mechanism sit in the same file, and can be checked against each other.
2026-08-16 — A method's declared behaviour may only ever strengthen
EDR-0020 made every API method declare whether
repeating it is safe, and made an unannotated method fail the build. That enforced the declaration as
present. It said nothing about the declaration changing underneath clients that had already
compiled it — and buf breaking cannot help, because its rules compare field numbers, names and
types and ignore custom method options entirely.
So a method could be reclassified from read-only to must-never-be-retried with every check green, and a client built against the previous schema would carry on retrying it. EDR-0040 closes that, and states the rule as one question: does this change make the cached retry policy of an already-built client unsafe?
Added
- A declaration may only strengthen.
safeto not-safe,naturaltokeyedorunsafe,keyedtounsafe, or moving the field a key travels in, are each breaking changes. A method whose behaviour genuinely changes gets a new name, exactly as a field whose meaning changes gets a new number. Enforced against the base branch on every pull request. - The three annotations became one.
MethodBehaviourcarriessafe,idempotencyandidempotency_fieldtogether, so they cannot be set half-way — and because extension numbers in the in-house range are not globally unique, one extension is one chance to collide with an adopter's own options instead of three. safenow reaches the generated client. It requires the standardidempotency_level = NO_SIDE_EFFECTS, which is the option Connect's generator actually reads. Honouringkeyedandunsafeneeds an interceptor, which arrives with the first real client; EDR-0040 says so rather than implying the loop is closed.
Changed
- The status text is honest again. The README, the introduction and the site's home page said implementation had not started. It has — at the M0 scaffolding milestone, described in the implementation plan. Nothing runs against a database yet.
- A build stamps the source's date, not the wall clock, so building the same commit twice
produces the same binary.
SOURCE_DATE_EPOCHwins where a distribution sets it.
2026-08-16 — An implementation plan, and the parser the grammar rests on
Phase 0 closes with the two things that were missing between a design and a build: what parses a statement, and in what order the rest gets made.
EDR-0039 — the checkable grammar is
parsed by PostgreSQL's own parser. Seven records leaned on "the checkable grammar" without naming
what implements it; it was the most load-bearing unnamed component in the design. It is
libpg_query — the server's real grammar — in every component that parses, rather than a
re-implementation that can disagree with the server about what a statement does.
The record is careful about where that matters, because overstating it would justify the wrong
thing: the CLI and the proxy are untrusted and everything they decide is re-checked, so a
client-side parser error produces a refusal rather than a breach. Soundness needs the real grammar
only in the Harbourmaster and the Pilot. Using it on the client too is a maintenance argument — one
allowlist instead of two kept in step — and it is stated as one. The cost is CGO_ENABLED=1 in every
binary, including the one that ships to laptops, which means native release runners instead of
cross-compilation. That is the largest cost of the decision and it is named as such.
The sharpest new obligation: a parser upgrade is a reviewed change on the order of a schema migration. A newer libpg_query can parse a statement the previous one could not, which silently widens the checkable subset — and therefore widens what an already-signed delegation permits, which EDR-0007 forbids.
Implementation plan. Eight milestones from an empty repository to the Phase 1 exit criterion, each with the test that proves it. It is shaped by three choices: Phase 1 ends at a local PostgreSQL and deploys nowhere; it is built by one person alongside other work, so steps are sized to finish in a sitting and sequenced strictly; and the grammar is built second, because six of the eight milestones stand on it.
Two things in it worth naming here. The walking skeleton is deliberately insecure and contained by construction — it refuses to start without an explicit environment variable, and the milestone that deletes that variable also un-skips a test asserting the path is gone. And every milestone's exit criterion must be a test that has been seen to fail for the right reason, because a guard nobody has watched fail is a guard nobody knows works.
There is also a decision debt list: four decisions Phase 1 will force, each of which becomes a record before the milestone that needs it closes.
2026-08-16 — Expert review found two holes in the signature scheme, and both are now closed
An expert panel reviewed the design — eight independent lenses, each finding checked twice, once by someone trying to refute it and once by someone searching the whole corpus for whether it was already addressed. Two critical findings survived both, and both were real.
They shared a root cause: the fast paths — standing-order invocation, a delegation match, a
Surveyor conforms — mint a marque with no human present, and no record ever said what fills the
human half of a two-signature artefact on those paths.
Added
- The fast-path authority chain — the marque's
payload gains an
authblock naming the artefact that authorised it, and that artefact travels with the marque. A standing order and a compiled delegation are each already human-signed, so on a fast path the human signed the shape rather than the instance, and the Pilot verifies that artefact offline: its signatures, its digest, the template rebound with the supplied parameters againstreq, each parameter against its constraint, and that the marque's limits are within the artefact's. - A marque states its own approval requirement
— JWS signature entries are independent, so any holder could delete an approver signature and the
rest would still verify. "At least one approver signature" could not tell a stripped two-approver
marque from a legitimate single-approver one, which meant a two-person rule was unenforceable
offline — in exactly the control-plane-down case the design values most.
approvals.required,eligibleand the escalation-chain digest now live inside the payload every signature covers.
Changed
- EDR-0003 and EDR-0008 contradicted each other and neither acknowledged it: one required a fresh interactive authentication to sign a marque and said no workload principal could ever satisfy it, the other minted marques with no human in the loop. Freshness is now scoped explicitly to producing a human approver signature — assembling a reference to a signature that already exists is not signing.
- The compromise boundary was overstated in four places. SECURITY.md, the architecture page, the playbook and CLAUDE.md all claimed a compromised control plane "cannot cause any statement to execute". The accurate form is "cannot cause a statement to execute whose shape no human signed", and all four now say that.
- A residual is named rather than hidden.
invokersmay resolve through identity-provider groups, which an offline Pilot cannot check — so a compromised control plane could invoke a genuine standing order as a principal of its choosing, bounded to that order's approved shape, parameter constraints, budget and rate limits. Standing orders oncriticaltargets must now name principals directly, and the compromise tables list the residual.
Why this is in the changelog
Because the interesting part is not that the gaps existed — it is that the design claimed a property it did not yet deliver, in four documents, and only an adversarial read found it. The records are the source of truth precisely so that a claim and its mechanism can be checked against each other.
2026-08-16 — The panel's should-fix tail: nineteen records amended, and four choices settled
The remaining 29 findings from the expert panel — specification completeness rather than design error, but the kind that turns into an implementer's guess. Nineteen records are amended; no decision changed, so none is superseded.
Four of them were genuine choices rather than omissions, and were settled deliberately.
The four choices
- The revocation list stays control-plane-served. Moving it to an independent origin would have
made offline execution unconditional; keeping it avoids new infrastructure. So the asterisk is now
stated wherever the offline claim appears: an issued marque executes offline for as long as the
Pilot's revocation list is fresh, and past that only under
revocation.policy: grace. The list also gains a defined shape — signedissued_at, monotonicsequence,next_update, and a refusal to accept a lower sequence than one already held. - Tier-B jurors use distinct providers where a deployment has more than one, and where it does not, the delegation is visibly marked as running a correlated panel and audited at a higher rate. The word "independent" is gone: three calls to one model narrow careless error, not correlated injection, and it is the outer bound that holds against the latter.
- The delegation compiler sees distinct column values only for columns classified non-sensitive. This is the only path by which production data reaches a model whose output becomes a candidate authority artefact. Elsewhere it gets names and types, cannot ground a value inference, and refuses — so compilation degrades exactly where the data is sensitive.
- Policy gains
require_envelope, andcriticaltargets default to hardware. A file-backed platform key cannot approve the highest-consequence changes unless someone explicitly acknowledges it.
The corrections worth knowing about
- Catalog introspection was justified by the wrong reason. The record said it was bounded by "the
role's own privileges — the database decides what the catalog shows". On PostgreSQL most of
pg_catalogis world-readable, so the role does no work there: the reviewed allowlist is the control. The allowlist is now column-aware,pg_proc.prosrcis excluded, and where a meta-command has aninformation_schemaequivalent Marque prefers it because those views are privilege-filtered. - "Rehearsal identity" was self-contradictory. A rehearsal under a read-mostly grant cannot measure a write, which is the entire point of rehearsing. It runs under the request's own role; the connection discipline is what makes it safe.
lock_timeoutbounds waiting, not holding. A rehearsal that acquires a lock then runs a slow second statement blocks production writers for the difference. There is now a total transaction budget and an out-of-band watchdog.- The logbook's kind list had gone stale against four later records while being presented as complete. It is explicitly illustrative now, with the registry living with the schema so adding a kind is a wire-contract change a reviewer sees.
- An agent chose its own
on_behalf_of. It must now name a human holding an active delegation to that agent, or its enrolled owner — and that human is notified at task open with a one-action disown. Otherwise "its human" was an assertion by the party being supervised. - The declared-scope anomaly signal is honestly labelled: it bounds accidental blast radius and is not a compromise detector. An agent under an attacker's control declares narrowly and looks exemplary.
Also
Chain verification proves no rewrite, not no fabrication — the playbook now says so, and
requires reconciling the window since the last anchor against the Pilots' ledgers and the target's own
audit. marque psql's shell-out refusal is stated as a capability rather than a list of names. Bulk
data movement and non-transactional statements are named in the deferred table rather than implied to
be covered. The prior-art entry for Bytebase concedes its ad-hoc approve-and-expire loop before
naming the real delta, and dynamic credential brokers are added beside it.
2026-08-15 — The statement pipeline opens to providers that may narrow or veto
Deployments need statement handling Marque should not hard-code: injecting a tenant constraint, mapping a column name across a migration in flight, synthesising a value, or asking another system whether a change freeze is on. Those are organisation-specific and the list is open-ended, so they become an extension point rather than features.
An extension point in an authorisation system is also where a security property usually goes to die, so this one is bounded by construction.
Added
- The statement pipeline and provider SPI —
every statement, from every surface, moves through one named pipeline. Out-of-process providers
may join
transform(rewrite the statement) andverify(inspect and veto, possibly asynchronously), plus a veto-onlypre_executeand a no-vetoobserve.
The rule
A provider may narrow or veto. It can never widen, permit, or replace a check.
Three mechanisms enforce that instead of asking for it:
- The digest is taken after transformation, so the marque signs what will actually run and a human approves the rewritten statement with the original shown beside it.
- Scope and fence re-run on the transformed statement, so a rogue or buggy transform is bounded by the submitter's own authority — it cannot reach beyond what they already had.
- There is no stage at which a provider can disable the fence, the magnitude assertion, marque verification, the role or the logbook.
The line for deciding what may become a provider: if disabling it would let something run that otherwise could not, it is not a provider.
Reconciled with EDR-0007
EDR-0007 refuses to conjoin a delegation's predicate
into an operator's WHERE, because silently narrowing a statement produces a partially-applied
change nobody reviewed. Constraint injection is that same operation — and it is allowed here because
it is visible: the transform's output is what gets digested, displayed, approved and signed. The
fence could not offer that, because it runs after approval, inside the transaction.
On fast paths where no human sees the individual request, the review has still happened: providers are declared in reviewed configuration, once, exactly as a standing order is.
Two subtleties worth knowing
- Transforms run once and their output is frozen. Otherwise value synthesis is a correctness bug: a provider injecting a timestamp would produce different text at rehearsal and at execution, so the rehearsed statement would not be the executed one.
- A failed transform fails the request. Never "skip and continue" — a skipped tenant-scoping transform is a data breach, and skipping is exactly what a tired operator would configure.
Moved onto the SPI
The analyser and the Surveyor both become providers. Neither loses anything: the analyser was already
authority-free, and the Surveyor's two outcomes were already exactly veto and refer.
2026-08-15 — Eight design records: the API, connections, a local proxy, keys, the console, tenancy and engines
The second design batch fills in the surfaces the first one deferred. Two of these reverse or sharpen earlier positions, and both say so.
Added
- One schema generates every client — a single
protobuf definition, Connect transport so the console calls the API directly from a browser with no
proxy, and every method annotated
safe/keyed/unsafe. Generated clients read those annotations, so anunsafemethod is never auto-retried by a client whose author had not read EDR-0011. - Connections, identity and read routing —
pooled, dynamically-credentialled connections; RDS/Aurora and Cloud SQL IAM authentication; and
per-operator database identity, where the auth token is minted for a database user derived from
the human, so the target's own audit names them independently of Marque's logbook. Also: a
failover-aware driver wrapper with transparent retry disabled on writes — a driver that
silently replays a write after failover applies a statement outside the execution fence's
accounting, so a failover must surface as
indeterminaterather than as a quiet retry. - The local proxy brokers every statement —
marque sqland a loopback proxy emulating the PostgreSQL wire protocol, so psql and existing tools work unchanged. It forwards no bytes: every statement is parsed, scoped, fenced, executed through a Pilot and logged. - Approver keys, enrolment and recovery — WebAuthn in the browser, platform key store in the CLI, and the rule that closes the obvious attack: enrolling an additional approver key requires a second, already-enrolled approver. Otherwise the shortest path to approving anything is stealing a session and enrolling your own key.
- The console is for deciding — and has no bulk approve, no saved approvals and no risk badge. Bulk refuse is offered; the asymmetry is the point.
- Tenants are partitioned from day one — the tenant comes from the authenticated principal and never a request field, and each tenant gets its own logbook hash chain and its own control-plane signing key, so a cross-tenant bug fails signature verification instead of returning someone else's data.
- Be psql first, then be better than psql —
marque psqlemulates psql's flags, meta-commands and output formats so it can be aliased in place. It forces a decision nobody had made:\dtis a catalog query, so catalog introspection becomes a named statement class — read-only, restricted to an allowlist of catalog relations (relations, not schemas, because an extension can add to a schema), run under the role's own privileges with no approval, and logged in aggregate. - A second engine is a capability matrix — what each engine can actually enforce is declared and published, and a control an engine cannot support is marked unavailable rather than silently weakened.
Changed
- "Not a SQL client" is no longer a non-goal. It was reasoning about exploration wrongly applied to the interface. The interface and the control are separable: a statement arriving over a socket gets the same parse, scope decision, fence and logbook entry as one arriving over gRPC. The non-goal is now "not a pass-through tunnel", which is the thing that actually matters.
- PostgreSQL is named as the first engine everywhere, with others following behind a published capability matrix rather than a flag.
The uncomfortable finding
MySQL does not port cleanly, and EDR-0026 says
so rather than discovering it later. It has no RETURNING, so the fence post-assert that catches
an UPDATE moving a row out of scope needs a locking pre-select of primary keys — which means a
row fence on MySQL requires the table to have one. Its statement-timeout variable applies to
read-only SELECTs only, so bounding a write needs a lock timeout plus an external watchdog. And its
DDL implicitly commits, so DDL cannot be rehearsed at all.
None of that is fatal. All of it changes what a MySQL target can be trusted to enforce, and an operator granting a delegation on one is told which controls they actually have.
2026-08-15 — Agents become a first-class submitter, and delegations can be written in English
Marque now targets a second use case directly: giving an agent production access without giving it a credential. An agent submits as itself on behalf of a named human, runs what is inside a scope it partly declares for its own task, and escalates everything else to that human rather than failing.
Alongside it, a delegation can be written as a sentence and compiled into an enforceable scope. Four new decision records cover both, and the security question they raise — how close may a model get to authority? — is answered structurally rather than behaviourally.
Added
- Agents are submitters under an intersected scope
— what an agent may do without asking is
operator policy ∩ its human's delegation ∩ the scope the agent declared for this task. The third term is the novel one: an agent knows what this run needs, declares it, and is held to it, which turns over-declaration into a visible anomaly rather than an invisible risk. See the new Agents page. - Escalation is a chain — out-of-scope work is referred, not refused. Stage one for an agent is always its own principal; later stages come from policy. Every stage is a human, each contributes only the authority it holds, and a timeout never approves.
- Written delegations are compiled —
"Sam can update
settingson sandbox accounts, up to 100 rows" is compiled by a model into a structured scope. The grantor signs the compilation, not the sentence, and enforcement runs entirely on the compilation. Ambiguity, missing schema evidence and unbounded scopes are refused rather than guessed at. - A model may choose a route, never widen a bound — for the clauses that genuinely will not compile, a new principal (the Surveyor) judges conformance per request, inside the human-signed bound, with a unanimous three-way panel, exactly two possible answers, default-refer on any doubt, ingress quotas, a mandatory sampled human audit that can automatically suspend the fast path, and a polled kill switch. It ships off.
- An operator playbook — duties, the signals that separate a working deployment from a quietly broken one, and procedures including suspending an agent, turning off surveying, working an incident with the control plane down, and responding to each kind of compromise.
Changed
- EDR-0009 gained a scope section rather than being superseded. Its decision is unchanged — the analyst still holds no authority and produces no verdict — but it now says explicitly that it governs the Leadsman rather than every model in the system, and points at the separate, bounded principal that does gate.
- The architecture, introduction, scope and cast pages carry the agent surface, and the cast gained the Surveyor with a note on why it and the Leadsman are deliberately different temperaments.
- Scope gained agent gateways and in-framework human-in-the-loop approval as prior art, three new risks, and a phase for the agent surface.
The invariant worth arguing with
A model can never create authority a human did not sign. The analyst holds none; a compiled delegation is signed by its grantor; a conformance judgment only ever chooses between two paths that both end in a human-granted scope, with the deterministic fence and magnitude assertions still running underneath. The worst a model error achieves is failing to escalate something already inside a signed scope.
That is a bound, not an elimination, and EDR-0017 says so plainly — which is why surveying is off by default and why its sampled audit is mandatory rather than advisory.
2026-08-15 — Marque begins: fifteen decision records and a docs site
Marque is a broker for statements run against production data stores: submit a statement, have it analysed, have a human with authority sign a scoped and expiring grant, then run exactly that statement under exactly that role. This is the first public state of the project — the design, written down before any of it is built.
Nothing is implemented. The scope page says what is in the first release, what is deliberately deferred, and what already exists in this space.
Added
- Fifteen decision records covering the whole design, from the plane split to the fence that enforces a delegated row scope. The four worth reading first are EDR-0001 for the shape, EDR-0004 and EDR-0005 for the security argument, and EDR-0007 for the hardest problem in the system.
- An architecture page synthesising the records, including the table that is the design's real argument: what each component's compromise does and does not buy an attacker.
- A cast list — Harbourmaster, Pilot, Leadsman, Tender — naming what each component is for and, more usefully, what each would never do.
- This changelog, one file per entry so that two changes on the same day cannot conflict.
Decided
- A marque carries two signatures, the approver's own device key and the control plane's. Neither party can produce a valid grant alone, so compromising the server yields the ability to ask and nothing more — EDR-0004.
- Delegated row scope is never proved and never silently applied. Predicate entailment over SQL is undecidable, and rewriting a statement to fit a scope produces a partially-applied change nobody reviewed. Instead the scope is a transactional fence that aborts and reports how many rows fell outside it — EDR-0007.
- The analyser holds no authority, and no setting grants it any. It writes prose beside facts that came from a parser and a rehearsal, with no risk score and no recommendation — because a score is the shape people automate against — EDR-0009.
- One bootstrap URL is the entire client configuration. A deployment publishes its own issuers, endpoints, Pilots and capabilities — EDR-0002.