Review pass: there were four retractions, not three, and one paragraph got the reasoning wrong
Fable returned REQUEST CHANGES on b19f0fa with nine findings. All were re-verified against the spec before applying. Seven are fixed here; two are answered in the branch discussion. The blocking one: a fourth retraction from da52b09 was missed, two bullets above one the first commit edited. The Identity section still carried "a claim may ride an assertion only if an instance would behave identically given a hostile value" — wording section 5.4 explicitly retracts as false — and then offered "A wrong theme is ugly" as support, which is the spec's own counter-example against it. The bullet below it cited the retracted test by name. Worse, only half the rule was ever here: the spec's test is two-sided, and side (b) is the half that blocks "which documents you can see", the proposal most likely to arrive looking reasonable. Both sides are now stated, with the retracted wording quoted as a do-not-restore. The note added by the first commit misstated the mechanism it existed to preserve. Section 3.3's argument is a team-set fingerprint plus one outside fact, not "one known membership identifies the row" — which is generally false, since one membership in a large team identifies nobody. It also borrowed the address retraction's reason for the "unidentified" one, and pointed at 5.1 where the spec points at 3.2. Replaced with a two-line footnote, which is the right length for a file whose job is to compress the spec rather than relitigate its history. "Sharing is membership" appears nowhere in the spec and section 6 rules the opposite: kind is member or guest, "because a row implying an org membership someone does not have is a lie the directory tells on the org's behalf". "The home does not hibernate" had never reached this file at all. It is the highest-consequence omission of the sweep, because hibernation is the platform default and section 2.1 says it would otherwise apply silently — an agent deploying from this file alone ships a hibernating identity authority. Smaller: gate 1 was missing its store objects and seeded corpus; step 1 lists three gates, not two; "from the first commit" was false at source, since the skeleton opens no database and carries none of them; expiry lost "against the threat model", which is doing work because widening does cost something absent single-use jti; and the log rule dropped job status messages, the sink an implementer of the drain hits first. Not fixed here, and said in the discussion instead: b19f0fa carries a Reviewed-By: Fable trailer for a review that had not happened at the time, and this branch cannot follow a cadence that says merge to a main this remote does not have. Docs only; no behaviour change. All four .amadan/ci.d gates green — none of which reads this file, so they verify nothing about this change. 🤖 Generated with Claude Opus 5, at the direction of Paul Campbell.
Reviewed-By: Fable 5.1 (REQUEST CHANGES on b19f0fa; discussion 1) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPUHaNUtTvE7f1VpFWZUHY
1 file changed,
+43
−23
AGENTS.md+43 −23
diff --git a/AGENTS.md b/AGENTS.md| index 9bce75c..2ecd506 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -25,18 +25,19 @@ learns is how many people exist, what their teams are called, and that some |
| person belongs to a particular set of them.** No document. No cell. No |
| readable address. |
| -**"Readable" is load-bearing, and so is "some person".** The first draft |
| -said "never stores your address", which spec §0 records as false: a magic |
| -link must survive a restart between send and click, so an address is at |
| -rest *somewhere* — sealed, enumerated and short-lived (§5.1), not absent. |
| -It said "some unidentified person"; §3.3 retracts that too, because one |
| -known membership identifies the row and reveals the rest. Do not restore |
| -either absolute — each is a claim the schema cannot keep. |
| - |
| -Two tests enforce it from the first commit: a plaintext grep over the raw |
| -SQLite file and WAL, and a no-address-at-rest test asserting the schema has |
| -no address column and that a full sign-in plus directory upsert **plus |
| -rekey** leaves no readable address anywhere, **WAL included**. |
| +*Two absolutes were argued and lost, and stay lost:* **"never stores your |
| +address"** — a magic link's address is at rest between send and click, |
| +sealed (§3.2), not absent — and **"some unidentified person"**: a team-set |
| +fingerprint plus one outside fact names the row (§3.3). |
| + |
| +Three gates enforce it from the first commit that opens a database (§12, |
| +and §13 step 1 lists all three): a **plaintext grep** over the raw SQLite |
| +file and WAL **plus this app's store objects**, against a seeded corpus of |
| +known addresses; a **no-address-at-rest** test asserting the schema has no |
| +address column and that a full sign-in plus directory upsert **plus rekey** |
| +leaves no readable address anywhere, **WAL included**; and |
| +**no-content-in-logs** (gate 9). The skeleton opens no database and so |
| +carries none of them yet. |
| --- |
| @@ -52,7 +53,9 @@ improvement. |
| team in an app. What you may *do* there is the instance's business, |
| decided from its own grants. |
| 3. **Never federate.** Data does not cross an instance boundary. Sharing is |
| - membership. |
| + a grant on the instance — member or guest — never a copy. §6 keeps the |
| + two apart deliberately: "a row implying an org membership someone does |
| + not have is a lie the directory tells on the org's behalf". |
| 4. **Never become a support console.** Reading someone's data to help them |
| is precisely what the one rule forbids. |
| @@ -105,22 +108,36 @@ quietly ignored). |
| relying party. This is a permanent constraint with its own test, not a |
| naming choice. |
| - Assertions carry **identity and display preferences, never |
| - authorization**. A claim may ride an assertion only if an instance would |
| - behave identically given a hostile value. A wrong theme is ugly; a wrong |
| - role is a breach. |
| + authorization**. The boundary test (§5.4) is **two-sided**: a claim may |
| + ride the assertion only if **(a)** the instance makes no trust decision |
| + based on it, validating against a **closed enumerated set** and treating a |
| + failed match as *absent* rather than as the raw value; **and (b)** the |
| + home already holds it for its own purposes, so carrying it adds nothing to |
| + what the home knows. Theme passes both. A role fails (a). **"Which |
| + documents you can see" passes (a) and fails (b)** — it hands the home a |
| + document-level graph it has no business holding, and it is the proposal |
| + most likely to arrive looking reasonable. |
| +- **Do not restore the earlier one-sided wording** ("the instance would |
| + behave identically given a hostile value"). §5.4 records why: it is false |
| + even for theme, since a hostile value gives an ugly page and an ugly page |
| + *is* different behaviour — a test its own example fails is a gap someone |
| + smuggles through — and it assumed the validation that makes it safe, since |
| + a free-string theme interpolated into CSS is an injection, not an ugly |
| + page. |
| - Assertion expiry is **120 seconds, not "seconds"** — sized to worst-case |
| cold wake plus clock skew, because the exchange request *is* what wakes a |
| hibernating instance and a tighter window makes the first sign-in to any |
| - cold instance fail and loop. Widening it costs nothing: **single-use `jti` |
| - is what stops replay, not brevity.** The long-lived thing is the session |
| + cold instance fail and loop. Widening it costs nothing **against the |
| + threat model**, because **single-use `jti` is what stops replay**, not |
| + brevity. The long-lived thing is the session |
| the instance mints. |
| - Every assertion names the **app and instance host** it is for. Without |
| audience binding, team A's assertion replays against team B, and a |
| deployment has thousands of siblings to replay against. |
| - **Signed identity claims are trusted for identity; every other claim must |
| - be cosmetically failable.** The subject itself fails the "hostile value |
| - changes nothing" test — a hostile subject is account takeover — so the |
| - test must exempt identity by construction or it proves too much and gets |
| + be cosmetically failable.** The subject flagrantly fails (a) — a hostile |
| + subject is account takeover — so the test must exempt identity by |
| + construction or it proves too much and gets |
| argued around. |
| - **Any "prune when all consumers have caught up" is unbounded in a |
| hibernating fleet.** A dormant instance runs no timers to advance its |
| @@ -146,8 +163,8 @@ quietly ignored). |
| instance makes one compromise an offline dictionary attack against the |
| whole suite directory. |
| - Migrations are additive-only. |
| -- **No address, team name, or queue payload in any log line, error string or |
| - metric label.** Platform-held logs are storage we cannot grep. |
| +- **No address, team name, or queue payload in any log line, error string, |
| + job status message or metric label.** Platform-held logs are storage we cannot grep. |
| **Frontend** |
| - Write `class="rst-*"`. The bare-attribute grammar is ruled but not |
| @@ -166,6 +183,9 @@ quietly ignored). |
| - A 200 is not a deploy verification. Only `X-Carlos-Version` on the |
| canonical host, after the watch. |
| - A promote is not a deploy. Until the process cycles, the old binary serves. |
| +- **The home does not hibernate.** It is the one app on the estate where |
| + cold-wake latency is a suite-wide outage. Stated because hibernation is |
| + the platform default and would otherwise apply silently. |
| - **A backup you have never restored is a hope, not a backup.** Losing this |
| database loses credentials, which do not rebuild. The restore drill runs |
| before launch. |