console: adopt a manifest owner only at account birth — the manifest is not write-once
Review catch, and correct. The safety argument for owner adoption rested on the account manifest being write-once, but that is a convention of MintAccount's create-only Put, not an enforced invariant: release.Store.Put takes ifMatch from the caller, the deployment bucket has no Object Lock, and the host IAM role holds unconditional s3:PutObject across it with nothing excluding accounts/*/account.json. So a bucket-writer could rewrite a VACANT account's manifest naming themselves, and the next materialize — which runs on every console start and before every `console member` call — would grant them owner over a live account. A real widening of the trust model: forging a channel pointer needs CARLOS_RELEASE_KEY, which most bucket-writers do not hold; this path needed no key at all. The transactional vacancy check did not cover it, because the account was legitimately vacant and the manifest is what changed afterwards. Fix is structural, not another check: the owner is adopted only on the pass where EnsureAccount reports it created the row, never against an account the console has already seen. Safe because -owner is required at create time, so manifests are born with their owner. The trade-off is documented where it will be read — an owner added to a manifest later is deliberately NOT adopted, and `console member -add` is the path for that. Also adds the coupling test: once an owner exists, a removed owner cannot be re-granted only because lastOwnerGuard stops the account ever returning to vacancy. That was leaned on implicitly and never exercised; now a future change to member removal or account deletion cannot silently reopen the path. Drops Store.HasOwner, which the reverted short-circuit no longer needs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6 files changed,
+257
−80
accounts.go+13 −2docs/superpowers/specs/2026-08-02-multi-account-design.md+24 −5internal/console/account_test.go+0 −3internal/console/materialize.go+56 −37internal/console/materialize_test.go+150 −15internal/console/store.go+14 −18
diff --git a/accounts.go b/accounts.go| index 061a7ffcf..586e4f288 100644 |
| --- a/accounts.go |
| +++ b/accounts.go |
| @@ -143,8 +143,19 @@ func runAccountsCreate(args []string) { |
| // Said out loud because the ordering is not obvious and the operator has |
| // just been bitten by its absence: the membership appears when the console |
| // next reads the bucket, not when this command returns. |
| - fmt.Printf("\n%s becomes an owner when the console next materializes accounts from the\n"+ |
| - "bucket (on start, and before every `console member` command). Nothing else to run.\n", ownerAddr) |
| + // |
| + // "the FIRST time" is exact, not loose wording. The console adopts a |
| + // manifest's owner only on the pass that first creates its own row for the |
| + // account, because the manifest is not write-once — anyone with bucket write |
| + // access can rewrite it, and adopting on later passes would turn that into a |
| + // standing authority grant needing no signing key. Editing this field |
| + // afterwards will NOT move ownership; `console member -add` is the verb for |
| + // that, and it is box-side for exactly that reason. |
| + fmt.Printf("\n%s becomes an owner the first time the console materializes this account\n"+ |
| + "from the bucket (on start, and before every `console member` command). Nothing\n"+ |
| + "else to run. Editing the manifest's owner LATER grants nothing — use\n"+ |
| + "`console member -add <address> %s -role owner` to change ownership after this.\n", |
| + ownerAddr, sqid) |
| } |
| func runAccountsList(args []string) { |
diff --git a/docs/superpowers/specs/2026-08-02-multi-account-design.md b/docs/superpowers/specs/2026-08-02-multi-account-design.md| index bb2b23804..3cc0ae6db 100644 |
| --- a/docs/superpowers/specs/2026-08-02-multi-account-design.md |
| +++ b/docs/superpowers/specs/2026-08-02-multi-account-design.md |
| @@ -63,11 +63,30 @@ were migrated into had no members — and membership *is* authorization, so |
| the operator could not open either of them in the console. The manifest now |
| carries the intended first owner and the console's materialization pass |
| turns it into an owner membership. The field is **additive**: absent means |
| -ownerless, which is what every earlier manifest is and stays. It fills a |
| -**vacancy only** — an account that already has an owner is left untouched, |
| -because the manifest is written once and re-read on every console start, and |
| -an instruction re-applied forever is a standing grant, not a creation |
| -record. `-owner` is required on `accounts create`. |
| +ownerless, which is what every earlier manifest is and stays. `-owner` is |
| +required on `accounts create`, so manifests are born carrying their owner. |
| + |
| +**Adopted at birth only.** The console applies `owner` on the pass that first |
| +creates its own row for the account, and never again. The manifest is **not |
| +write-once**: `MintAccount` claims it with a create-only `Put`, but that is a |
| +convention of one code path, not a property of the object — `Store.Put` takes |
| +its `ifMatch` from the caller, the bucket has no Object Lock, and the host IAM |
| +role holds unconditional `s3:PutObject` across it. Adopting on any later pass |
| +would make rewriting that object an authority grant: name yourself owner of a |
| +vacant account, wait for the next console start or `console member` call, and |
| +you own a live account. That is a genuine widening — forging a channel pointer |
| +needs `CARLOS_RELEASE_KEY`, this would need no key at all. |
| + |
| +The trade-off is accepted and explicit: **an owner added to a manifest after |
| +first materialization is not adopted**, and an account that materialized |
| +ownerless stays ownerless. Changing ownership is `console member -add |
| +<address> <sqid> -role owner` — box-side authority, which is the point. |
| + |
| +Once an owner exists, they cannot be silently re-granted after removal |
| +either, and that rests on `lastOwnerGuard`: the account can never return to |
| +vacancy, so there is nothing for a re-read to fill. That coupling is |
| +exercised end to end by a test, so a future change to member removal or |
| +account deletion cannot quietly reopen the path. |
| This is also why the owner lives in the bucket rather than in a flag that |
| tells the operator to run `console member -add` afterwards: the carlos binary |
diff --git a/internal/console/account_test.go b/internal/console/account_test.go| index 6120d6415..9da1ff54e 100644 |
| --- a/internal/console/account_test.go |
| +++ b/internal/console/account_test.go |
| @@ -379,9 +379,6 @@ func TestAdoptManifestOwnerOnlyEverFillsAVacancy(t *testing.T) { |
| if _, err := st.EnsureAccount(3, "bac", "Kass", now); err != nil { |
| t.Fatal(err) |
| } |
| - if owned, err := st.HasOwner(3); err != nil || owned { |
| - t.Fatalf("HasOwner(fresh) = %v %v, want false", owned, err) |
| - } |
| adopted, err := st.AdoptManifestOwner(3, "paul@keymail.dev", now) |
| if err != nil || !adopted { |
| t.Fatalf("AdoptManifestOwner = %v %v, want it to fill the vacancy", adopted, err) |
diff --git a/internal/console/materialize.go b/internal/console/materialize.go| index a77a7e9f1..69f5df4e1 100644 |
| --- a/internal/console/materialize.go |
| +++ b/internal/console/materialize.go |
| @@ -52,27 +52,15 @@ func MaterializeAccounts(ctx context.Context, s release.Store, st *Store) (int, |
| } |
| // Cheap short-circuit, and the reason a steady-state pass does no |
| // writes and no object reads at all: the console usually already knows |
| - // every account, AND somebody owns it. |
| + // every account. |
| // |
| - // "and somebody owns it" is not belt-and-braces — it is the upgrade path |
| - // for a live failure. The flagship materialized kass's and keymail's |
| - // accounts with a console that had no notion of a manifest owner, so |
| - // their rows exist with no members and the operator could not open |
| - // either. Short-circuiting on existence alone would leave exactly those |
| - // two accounts stranded forever, and fix only accounts created after the |
| - // deploy. |
| - existing, err := st.AccountBySqid(sqid) |
| - if err != nil { |
| + // This is ALSO the security boundary for the owner field, so it must |
| + // stay an unconditional "already known, stop here". See materializeOwner |
| + // for why an account the console has seen before is never revisited. |
| + if a, err := st.AccountBySqid(sqid); err != nil { |
| return n, err |
| - } |
| - if existing != nil { |
| - owned, err := st.HasOwner(existing.ID) |
| - if err != nil { |
| - return n, err |
| - } |
| - if owned { |
| - continue |
| - } |
| + } else if a != nil { |
| + continue |
| } |
| body, _, err := s.Get(ctx, key) |
| if err != nil { |
| @@ -93,21 +81,20 @@ func MaterializeAccounts(ctx context.Context, s release.Store, st *Store) (int, |
| if created.IsZero() { |
| created = time.Now() |
| } |
| - if existing == nil { |
| - added, err := st.EnsureAccount(m.ID, m.Sqid, m.Name, created) |
| - if err != nil { |
| - return n, err |
| - } |
| - if !added { |
| - // The sqid was free but the id was not: some other row already |
| - // holds it. Loud, because two accounts cannot share an id and one |
| - // of these two is wrong. |
| - log.Printf("console: did not materialize %s: account id %d is already taken here", key, m.ID) |
| - continue |
| - } |
| - log.Printf("console: materialized account %s (%s) from the bucket", m.Sqid, m.Name) |
| - n++ |
| + added, err := st.EnsureAccount(m.ID, m.Sqid, m.Name, created) |
| + if err != nil { |
| + return n, err |
| + } |
| + if !added { |
| + // The sqid was free but the id was not: some other row already |
| + // holds it. Loud, because two accounts cannot share an id and one |
| + // of these two is wrong. |
| + log.Printf("console: did not materialize %s: account id %d is already taken here", key, m.ID) |
| + continue |
| } |
| + log.Printf("console: materialized account %s (%s) from the bucket", m.Sqid, m.Name) |
| + n++ |
| + // ONLY here — on the pass that created the row, never on a later one. |
| materializeOwner(st, key, m, created) |
| } |
| return n, nil |
| @@ -115,12 +102,44 @@ func MaterializeAccounts(ctx context.Context, s release.Store, st *Store) (int, |
| // materializeOwner turns the manifest's `owner` into the account's first owner |
| // membership — the console half of the fix for `carlos accounts create` making |
| -// unreachable accounts (see Store.AdoptManifestOwner for the production |
| -// failure this comes from). |
| +// unreachable accounts (see Store.AdoptManifestOwner for the production failure |
| +// this comes from). |
| +// |
| +// IT RUNS EXACTLY ONCE PER ACCOUNT: on the pass that first creates the console's |
| +// row, and never again. That is the whole security argument for the field, and |
| +// it is deliberately structural rather than a check inside this function. |
| +// |
| +// The reason is that the manifest is NOT write-once. MintAccount claims the |
| +// object with a create-only Put, but that is a convention of one code path, not |
| +// a property of the object: release.Store.Put takes its ifMatch from the caller, |
| +// the deployment bucket has no Object Lock, and the host IAM role holds |
| +// unconditional s3:PutObject across the bucket with nothing excluding |
| +// accounts/*/account.json. Anyone who can write to the bucket can rewrite a live |
| +// account's manifest. |
| +// |
| +// If adoption happened on any pass, that rewrite would BE an authority grant: |
| +// name yourself owner of a vacant account, wait for the next console start or |
| +// `console member` invocation — both run this pass — and you own a live account |
| +// and everything deployed in it. That is a real widening of the trust model, not |
| +// something bucket write access already implied: forging a channel pointer needs |
| +// CARLOS_RELEASE_KEY, which most bucket-writers do not hold, and this path needs |
| +// no key at all. AdoptManifestOwner's transactional vacancy check does not cover |
| +// it either — it is TOCTOU-safe, but the account was LEGITIMATELY vacant and it |
| +// is the manifest that changed afterwards. |
| +// |
| +// Adopting only at birth is safe precisely because `-owner` is now REQUIRED on |
| +// `carlos accounts create`: manifests are born carrying their owner, so there is |
| +// no legitimate case of one arriving later. |
| +// |
| +// THE TRADE-OFF, stated plainly: an owner added to a manifest AFTER the account |
| +// first materialized is deliberately NOT adopted, however well-meant the edit. |
| +// Editing the bucket is not how you grant console authority; `console member |
| +// -add <address> <sqid> -role owner` is, and it is box-side for exactly this |
| +// reason. The same applies to an account that materialized ownerless from a |
| +// pre-`owner` manifest — it stays ownerless until an operator says otherwise. |
| // |
| // Identity and authority stay SEPARATE decisions, which is why this runs after |
| -// the account row rather than inside EnsureAccount: an old manifest with no |
| -// owner materializes exactly as it always did, and a manifest with an owner |
| +// the account row rather than inside EnsureAccount: a manifest with an owner |
| // keymail could never authenticate still yields a perfectly good account. One |
| // unusable string must not hide an account from the console entirely. |
| // |
diff --git a/internal/console/materialize_test.go b/internal/console/materialize_test.go| index 59a4000a6..1206fbc76 100644 |
| --- a/internal/console/materialize_test.go |
| +++ b/internal/console/materialize_test.go |
| @@ -1,6 +1,7 @@ |
| package console |
| import ( |
| + "encoding/json" |
| "testing" |
| "time" |
| @@ -174,33 +175,142 @@ func TestMaterializeGivesANewAccountTheManifestOwner(t *testing.T) { |
| } |
| } |
| -// The upgrade path, and the one the flagship is actually in: the account was |
| -// materialized by a console that had no idea manifests could name an owner, so |
| -// the row exists and nobody owns it. The catch-up must reach it — otherwise the |
| -// fix only helps accounts created after the deploy, and the two that prompted |
| -// it stay unreachable. |
| -func TestMaterializeAdoptsTheOwnerOfAnAccountItAlreadyKnows(t *testing.T) { |
| +// THE MANIFEST IS NOT WRITE-ONCE, and the owner is only adopted at BIRTH. |
| +// |
| +// `MintAccount` claims the object with a create-only Put, but that is a |
| +// convention of one code path, not a property of the object: release.Store.Put |
| +// takes its ifMatch from the caller, the deployment bucket has no Object Lock, |
| +// and the host IAM role holds unconditional s3:PutObject over the whole bucket |
| +// with nothing excluding accounts/*/account.json. So anyone who can write to |
| +// the bucket can REWRITE a live account's manifest. |
| +// |
| +// If materialization adopted an owner on any pass, that rewrite would be an |
| +// authority grant: name yourself owner of a vacant account, wait for the next |
| +// console start or `console member` invocation — both run this pass — and you |
| +// hold owner over a live account with its apps in it. That is a genuine |
| +// widening of the trust model, not something bucket write access already |
| +// implied: forging a channel pointer needs CARLOS_RELEASE_KEY, which most |
| +// bucket-writers do not have, and this path needs no key at all. |
| +// |
| +// The transactional vacancy check in AdoptManifestOwner does not help here. It |
| +// is TOCTOU-safe, but the account was LEGITIMATELY vacant; the manifest is what |
| +// changed afterwards. |
| +// |
| +// So adoption happens once, when the console first creates the account row, and |
| +// never again. Safe now that -owner is required at create time: manifests are |
| +// born with their owner. |
| +func TestMaterializeNeverAdoptsAnOwnerAddedToAManifestAfterTheFact(t *testing.T) { |
| st := openTestStore(t) |
| rel := release.NewDirStore(t.TempDir()) |
| ctx := t.Context() |
| now := time.Unix(1700000000, 0) |
| - id, sqid, err := domains.MintAccount(ctx, rel, "Keymail", "paul@keymail.dev", now) |
| - if err != nil { |
| + // A legitimately vacant account: an old manifest, from before `owner`. |
| + if err := WriteAccountManifest(ctx, rel, 42, domains.Sqid(42), "Kass", now); err != nil { |
| t.Fatal(err) |
| } |
| - // Materialized by the old code: a row, no members. |
| - if _, err := st.EnsureAccount(id, sqid, "Keymail", now); err != nil { |
| + if n, err := MaterializeAccounts(ctx, rel, st); err != nil || n != 1 { |
| + t.Fatalf("materialized %d (%v), want 1", n, err) |
| + } |
| + if members, _ := st.Members(42); len(members) != 0 { |
| + t.Fatalf("Members = %+v, want the account vacant to begin with", members) |
| + } |
| + |
| + // Somebody with plain bucket write access — no signing key — rewrites the |
| + // manifest naming themselves. |
| + rewriteManifest(t, rel, 42, "attacker@keymail.dev") |
| + // This pass runs on every console start and before every `console member`. |
| + if _, err := MaterializeAccounts(ctx, rel, st); err != nil { |
| t.Fatal(err) |
| } |
| + if role, _ := st.Role(42, "attacker@keymail.dev"); role != "" { |
| + t.Errorf("a rewritten manifest granted %q over a live account — bucket write became "+ |
| + "console authority, with no signing key involved", role) |
| + } |
| + if members, _ := st.Members(42); len(members) != 0 { |
| + t.Errorf("Members = %+v, want the account still vacant", members) |
| + } |
| +} |
| - // Nothing NEW is materialized — the count is accounts, not memberships — |
| - // but the vacancy is filled. |
| - if n, err := MaterializeAccounts(ctx, rel, st); err != nil || n != 0 { |
| - t.Fatalf("materialized %d (%v), want 0 new accounts", n, err) |
| +// The same rule stated the other way, and the case an operator will actually |
| +// hit: an owner added to a manifest AFTER the account first materialized is |
| +// deliberately not adopted, however well-meant. The operator path for that is |
| +// `console member -add <address> <sqid> -role owner`, which is box-side |
| +// authority rather than bucket-side. |
| +func TestMaterializeLeavesALateManifestOwnerToTheOperator(t *testing.T) { |
| + st := openTestStore(t) |
| + rel := release.NewDirStore(t.TempDir()) |
| + ctx := t.Context() |
| + now := time.Unix(1700000000, 0) |
| + |
| + if err := WriteAccountManifest(ctx, rel, 42, domains.Sqid(42), "Kass", now); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := MaterializeAccounts(ctx, rel, st); err != nil { |
| + t.Fatal(err) |
| + } |
| + rewriteManifest(t, rel, 42, "paul@keymail.dev") |
| + if _, err := MaterializeAccounts(ctx, rel, st); err != nil { |
| + t.Fatal(err) |
| + } |
| + if role, _ := st.Role(42, "paul@keymail.dev"); role != RoleOwner { |
| + // The box-side verb is what fills it, and it still works. |
| + if err := st.AddMember(42, "paul@keymail.dev", RoleOwner, now); err != nil { |
| + t.Fatal(err) |
| + } |
| + } else { |
| + t.Error("a late manifest owner was adopted — the manifest is not write-once, " + |
| + "so that is a standing grant anyone with bucket write can issue") |
| + } |
| +} |
| + |
| +// THE COUPLING TEST. Once an owner exists, the only thing standing between "the |
| +// operator removed them" and "the manifest puts them back" is that the account |
| +// never becomes vacant again — and that is enforced by lastOwnerGuard, which |
| +// lives in member removal, not here. This diff leans on that guard implicitly; |
| +// exercising it end to end means a future change to removal (or to account |
| +// deletion) cannot silently reopen the adoption path. |
| +func TestARemovedOwnerIsNeverRegrantedByTheManifest(t *testing.T) { |
| + st := openTestStore(t) |
| + rel := release.NewDirStore(t.TempDir()) |
| + ctx := t.Context() |
| + now := time.Unix(1700000000, 0) |
| + |
| + id, _, err := domains.MintAccount(ctx, rel, "Kass", "paul@keymail.dev", now) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := MaterializeAccounts(ctx, rel, st); err != nil { |
| + t.Fatal(err) |
| } |
| if role, _ := st.Role(id, "paul@keymail.dev"); role != RoleOwner { |
| - t.Errorf("role = %q, want owner — an already-materialized ownerless account was left stranded", role) |
| + t.Fatalf("role = %q, want the manifest owner adopted at birth", role) |
| + } |
| + |
| + // Link one: the account cannot be emptied. Removing the last owner is |
| + // refused, so there is no vacancy for a re-read to fill. |
| + if err := st.RemoveMember(id, "paul@keymail.dev"); err == nil { |
| + t.Fatal("removed the last owner — the account is now vacant and the manifest can refill it") |
| + } |
| + |
| + // Link two: with ownership transferred, the original CAN be removed — and |
| + // must stay removed across every subsequent pass, forever. |
| + if err := st.AddMember(id, "successor@keymail.dev", RoleOwner, now); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := st.RemoveMember(id, "paul@keymail.dev"); err != nil { |
| + t.Fatal(err) |
| + } |
| + for i := 0; i < 3; i++ { |
| + if _, err := MaterializeAccounts(ctx, rel, st); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if role, _ := st.Role(id, "paul@keymail.dev"); role != "" { |
| + t.Errorf("the removed owner came back as %q — the manifest is behaving as a standing grant", role) |
| + } |
| + if role, _ := st.Role(id, "successor@keymail.dev"); role != RoleOwner { |
| + t.Errorf("successor role = %q, want owner", role) |
| } |
| } |
| @@ -273,6 +383,31 @@ func TestMaterializeLeavesAnOwnerlessManifestOwnerless(t *testing.T) { |
| } |
| } |
| +// rewriteManifest overwrites a live account manifest with one naming owner — |
| +// what a bucket-writer does, and the thing MintAccount's create-only Put does |
| +// NOT prevent anyone else from doing. The etag is read and passed because the |
| +// DirStore emulates compare-and-swap; S3 has no such constraint at all, so a |
| +// caller holding s3:PutObject simply writes the object. |
| +func rewriteManifest(t *testing.T, rel release.Store, id int64, owner string) { |
| + t.Helper() |
| + ctx := t.Context() |
| + sqid := domains.Sqid(id) |
| + key := domains.AccountManifestKey(sqid) |
| + _, etag, err := rel.Get(ctx, key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + body, err := json.Marshal(domains.AccountManifest{ |
| + ID: id, Sqid: sqid, Name: "Kass", Owner: owner, CreatedAt: time.Unix(1700000000, 0).UTC(), |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := rel.Put(ctx, key, body, etag); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| // A deployment with no bucket configured at all still has to work: this is |
| // best-effort catch-up, not a precondition. |
| func TestMaterializeWithNoStoreIsANoOp(t *testing.T) { |
diff --git a/internal/console/store.go b/internal/console/store.go| index 2917b4a2e..e4c3e08ec 100644 |
| --- a/internal/console/store.go |
| +++ b/internal/console/store.go |
| @@ -545,17 +545,6 @@ func (s *Store) EnsureAccount(id int64, sqid, name string, created time.Time) (a |
| return n > 0, err |
| } |
| -// HasOwner reports whether an account has anybody who can administer it. It is |
| -// the cheap local check MaterializeAccounts makes before deciding whether it |
| -// needs to read a manifest out of the bucket at all — in steady state every |
| -// account has an owner, so the catch-up pass does no object reads. |
| -func (s *Store) HasOwner(accountID int64) (bool, error) { |
| - var n int |
| - err := s.db.QueryRow(`SELECT COUNT(*) FROM memberships WHERE account_id = ? AND role = ?`, |
| - accountID, RoleOwner).Scan(&n) |
| - return n > 0, err |
| -} |
| - |
| // AdoptManifestOwner turns an account manifest's `owner` field into the owner |
| // membership — the console half of the fix for a PRODUCTION failure, found by |
| // running the migration on the flagship and not by reading this code. |
| @@ -574,13 +563,20 @@ func (s *Store) HasOwner(accountID int64) (bool, error) { |
| // console and every box, now carries the intended owner, and this is where the |
| // console reads it. |
| // |
| -// It fills a VACANCY and nothing else. The manifest is written once at creation |
| -// and re-read on every console start, so treating it as an instruction would |
| -// make it a standing grant: an address removed by an operator would come back |
| -// on the next restart, which is precisely the shape of the app-backfill bug |
| -// this same production run turned up. So: an account that already has ANY owner |
| -// is left completely alone, and only an ownerless one is filled. Returns |
| -// whether it wrote anything. |
| +// It fills a VACANCY and nothing else. The manifest is re-read on every console |
| +// start, so treating it as an instruction would make it a standing grant: an |
| +// address removed by an operator would come back on the next restart, which is |
| +// precisely the shape of the app-backfill bug this same production run turned |
| +// up. So: an account that already has ANY owner is left completely alone, and |
| +// only an ownerless one is filled. Returns whether it wrote anything. |
| +// |
| +// This is the SECOND of two independent guards, and deliberately not the load |
| +// bearing one. The manifest is not write-once — it can be rewritten by anyone |
| +// with bucket write access — so the caller only ever invokes this on the pass |
| +// that first creates the account row (see materializeOwner). That structural |
| +// rule is what stops a rewritten manifest granting authority; this vacancy |
| +// check is what stops a race between the row insert and this write, and what |
| +// keeps the function safe to reason about on its own. |
| // |
| // The named address may already be a plain member (an accepted invite always |
| // grants member) — then they are promoted, rather than the account gaining a |