carlos / platform Public

objstore: a first grant clears another seed's mark (Plan 5 Task 4 review)

The first grant of a record whose declaration carries a seed's mark now
clears the mark in the same write as the grant block, unless the grant
is for that seed (GrantInFor). A mark therefore survives only on a store
its own seed granted, so a seed that crashed between declaring and
granting cannot later tear down a store another path made (spec §4
Storage). GrantIn is GrantInFor with no seed, so every grant path
(store grant, store provision, scan grant, instance create) gets the
clearing. A record with no mark is written exactly as before.

The seeded instance store passes its seed through GrantInFor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 70ba1670f4c09b6589acf3ec91c041d214f57071 parent 1434d9d
4 files changed, +142 −7
  • internal/console/instanceprivatestore.go +2 −2
  • internal/console/instanceprivatestore_test.go +7 −3
  • internal/objstore/walk.go +19 −2
  • internal/objstore/walk_seed_test.go +114 −0
diff --git a/internal/console/instanceprivatestore.go b/internal/console/instanceprivatestore.go
index 5f4216cb3..6400fb6fe 100644
--- a/internal/console/instanceprivatestore.go
+++ b/internal/console/instanceprivatestore.go
@@ -309,8 +309,8 @@ func (s *Server) instanceStoreProvisionFor(ctx context.Context, w http.ResponseW
return nil, false, nil
}
}
- out, err := objstore.GrantIn(walkCtx, effectStore, effectProvisioner, s.cfg.StoreDeployment,
- pub, acct.Sqid, app, env, e.Decision.By, objstore.GrantSourceAccountApproval, "", time.Now())
+ out, err := objstore.GrantInFor(walkCtx, effectStore, effectProvisioner, s.cfg.StoreDeployment,
+ pub, acct.Sqid, app, env, e.Decision.By, objstore.GrantSourceAccountApproval, "", seedID, time.Now())
if err != nil && seedID != "" && redeclare {
// A seed's declaration must not outlive a grant that wrote nothing:
// another path could grant that record later and keep the mark, and
diff --git a/internal/console/instanceprivatestore_test.go b/internal/console/instanceprivatestore_test.go
index dc57ed082..9737f7308 100644
--- a/internal/console/instanceprivatestore_test.go
+++ b/internal/console/instanceprivatestore_test.go
@@ -260,7 +260,9 @@ func TestInstanceStoreProvisionLeavesItsDeclarationOnAFailedGrant(t *testing.T)
}
// Without a seed, a declared-only record keeps its author and timestamp,
-// exactly as before: only a seeded create re-declares.
+// exactly as before: only a seeded create re-declares. A seed's mark on it
+// does not survive the grant: the store is this create's, not the seed's,
+// and the seed's finish must never tear it down.
func TestInstanceStoreProvisionKeepsAnExistingDeclaration(t *testing.T) {
fx, _, acct := seededStoreFixture(t, "code-unseeded-keep")
at := time.Now().UTC().Add(-time.Hour).Truncate(time.Second)
@@ -270,7 +272,9 @@ func TestInstanceStoreProvisionKeepsAnExistingDeclaration(t *testing.T) {
if !ok {
t.Fatalf("provision refused: %d %s", w.Code, w.Body.String())
}
- if out.Declared == nil || out.Declared.By != "alice@keymail.dev" || !out.Declared.At.Equal(at) || out.Declared.Seed != seedB {
- t.Fatalf("declaration = %+v, want the existing one untouched", out.Declared)
+ for _, rec := range []*objstore.Record{out, canaryStore(t, fx)} {
+ if rec.Declared == nil || rec.Declared.By != "alice@keymail.dev" || !rec.Declared.At.Equal(at) || rec.Declared.Seed != "" || rec.Grant == nil {
+ t.Fatalf("declaration = %+v, want the existing one with the seed's mark cleared by the grant", rec.Declared)
+ }
}
}
diff --git a/internal/objstore/walk.go b/internal/objstore/walk.go
index b8d47b846..a56051502 100644
--- a/internal/objstore/walk.go
+++ b/internal/objstore/walk.go
@@ -198,8 +198,16 @@ func claimRawGrant(ctx context.Context, s release.Store, p Provisioner, d Deploy
// IAM principal and delivered bundle are all its own. Every property Grant's
// doc comment states holds here unchanged — the environment is another
// coordinate of WHICH estate, never a change to how the walk behaves.
+//
+// seed is the seed id this grant is for (GrantInFor), "" for every other
+// grant. The FIRST grant of a record whose declaration carries another
+// seed's mark (Declared.Seed) clears the mark in the same write as the
+// grant block: the store is then this grant's, not the seed's, and the
+// seed's finish must never tear it down (seeded-instances spec §4
+// Storage). A mark therefore survives only on a store its own seed
+// granted. A record with no mark is written exactly as before.
func claimRawGrantIn(ctx context.Context, s release.Store, p Provisioner, d Deployment,
- sealPub *ecdh.PublicKey, acct, app, env, actor, source, adoptBucket string, now time.Time) (*Record, error) {
+ sealPub *ecdh.PublicKey, acct, app, env, actor, source, adoptBucket, seed string, now time.Time) (*Record, error) {
// 1. resolve + write the grant block
rec, etag, err := GetIn(ctx, s, acct, app, env)
if err != nil {
@@ -249,6 +257,9 @@ func claimRawGrantIn(ctx context.Context, s release.Store, p Provisioner, d Depl
return nil, err
}
if rec.Grant == nil {
+ if rec.Declared.Seed != "" && rec.Declared.Seed != seed {
+ rec.Declared.Seed = ""
+ }
rec.Grant = &GrantBlock{Backend: d.Backend, Bucket: bucket, Source: source, At: now, By: actor}
if etag, err = PutIn(ctx, s, acct, app, env, rec, etag); err != nil {
return nil, err
@@ -902,8 +913,14 @@ func Grant(ctx context.Context, s release.Store, p Provisioner, d Deployment, se
return
}
func GrantIn(ctx context.Context, s release.Store, p Provisioner, d Deployment, sealPub *ecdh.PublicKey, acct, app, env, actor, source, adoptBucket string, now time.Time) (claimResult0 *Record, claimResult1 error) {
+ return GrantInFor(ctx, s, p, d, sealPub, acct, app, env, actor, source, adoptBucket, "", now)
+}
+
+// GrantInFor is GrantIn for a seeded create's store: seed is the seed id
+// whose mark the grant keeps. Every other grant is GrantIn, seed "".
+func GrantInFor(ctx context.Context, s release.Store, p Provisioner, d Deployment, sealPub *ecdh.PublicKey, acct, app, env, actor, source, adoptBucket, seed string, now time.Time) (claimResult0 *Record, claimResult1 error) {
claimResult1 = release.WithAppMutation(ctx, s, acct, app, "objstore.GrantIn", func(claimContext context.Context) error {
- claimResult0, claimResult1 = claimRawGrantIn(claimContext, s, p, d, sealPub, acct, app, env, actor, source, adoptBucket, now)
+ claimResult0, claimResult1 = claimRawGrantIn(claimContext, s, p, d, sealPub, acct, app, env, actor, source, adoptBucket, seed, now)
return claimResult1
})
return
diff --git a/internal/objstore/walk_seed_test.go b/internal/objstore/walk_seed_test.go
new file mode 100644
index 000000000..03b666432
--- /dev/null
+++ b/internal/objstore/walk_seed_test.go
@@ -0,0 +1,114 @@
+package objstore
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// A seed's mark (Declared.Seed) survives only on a store its own seed
+// granted. Any other first grant of a marked record clears the mark in the
+// same write as the grant block, so a seed's finish can never tear down a
+// store another path made (seeded-instances spec §4 Storage).
+func TestFirstGrantClearsAnotherSeedsMark(t *testing.T) {
+ const seedA, seedB = "sd_aaaaaaaaaaaaaaaaaaaaaaaaaa", "sd_bbbbbbbbbbbbbbbbbbbbbbbbbb"
+ for name, c := range map[string]struct {
+ mark, grantFor, want string
+ }{
+ "an unseeded grant": {seedB, "", ""},
+ "another seed's grant": {seedB, seedA, ""},
+ "the marking seed's grant": {seedB, seedB, seedB},
+ } {
+ t.Run(name, func(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ _, pub := testSealKeypair(t)
+ at := time.Now().UTC().Add(-time.Hour)
+ decl := &Declared{EnvPrefix: DefaultEnvPrefix, At: at, By: "member@example.com", Seed: c.mark}
+ if _, err := PutIn(ctx, s, "bab", "shop", "canary", &Record{Declared: decl}, ""); err != nil {
+ t.Fatal(err)
+ }
+ rec, err := GrantInFor(ctx, s, NewMemProvisioner("eu-west-1"), testDeployment(), pub,
+ "bab", "shop", "canary", "op", GrantSourceOperator, "", c.grantFor, time.Now().UTC())
+ if err != nil {
+ t.Fatal(err)
+ }
+ stored, _, err := GetIn(ctx, s, "bab", "shop", "canary")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, r := range []*Record{rec, stored} {
+ if r.Declared.Seed != c.want {
+ t.Fatalf("Declared.Seed = %q, want %q", r.Declared.Seed, c.want)
+ }
+ if r.Declared.By != decl.By || !r.Declared.At.Equal(at) || r.Declared.EnvPrefix != decl.EnvPrefix {
+ t.Fatalf("declaration = %+v, want only the mark changed from %+v", r.Declared, decl)
+ }
+ }
+ })
+ }
+}
+
+// A re-run over a store its seed already granted is a repair of that
+// seed's estate, not an adoption: the mark stays.
+func TestGrantRerunKeepsTheGrantingSeedsMark(t *testing.T) {
+ const seedB = "sd_bbbbbbbbbbbbbbbbbbbbbbbbbb"
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ _, pub := testSealKeypair(t)
+ p := NewMemProvisioner("eu-west-1")
+ decl := &Declared{EnvPrefix: DefaultEnvPrefix, At: time.Now().UTC(), By: "member@example.com", Seed: seedB}
+ if _, err := PutIn(ctx, s, "bab", "shop", "canary", &Record{Declared: decl}, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := GrantInFor(ctx, s, p, testDeployment(), pub, "bab", "shop", "canary", "op", GrantSourceOperator, "", seedB, time.Now().UTC()); err != nil {
+ t.Fatal(err)
+ }
+ rec, err := GrantIn(ctx, s, p, testDeployment(), pub, "bab", "shop", "canary", "op", GrantSourceOperator, "", time.Now().UTC())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if rec.Declared.Seed != seedB {
+ t.Fatalf("Declared.Seed after a re-run = %q, want %q", rec.Declared.Seed, seedB)
+ }
+}
+
+// A record with no mark is granted exactly as before: its declaration
+// block comes out byte-identical, and no seed key appears.
+func TestGrantLeavesAnUnmarkedDeclarationAsItWas(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ _, pub := testSealKeypair(t)
+ decl := &Declared{EnvPrefix: "SHOP_FILES", At: time.Now().UTC().Add(-time.Hour), By: "member@example.com"}
+ if _, err := PutIn(ctx, s, "bab", "shop", "canary", &Record{Declared: decl}, ""); err != nil {
+ t.Fatal(err)
+ }
+ before, _ := json.Marshal(decl)
+ if _, err := GrantIn(ctx, s, NewMemProvisioner("eu-west-1"), testDeployment(), pub, "bab", "shop", "canary", "op", GrantSourceOperator, "", time.Now().UTC()); err != nil {
+ t.Fatal(err)
+ }
+ raw, _, err := s.Get(ctx, KeyIn("bab", "shop", "canary"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var stored struct {
+ Declared json.RawMessage `json:"declared"`
+ }
+ if err := json.Unmarshal(raw, &stored); err != nil {
+ t.Fatal(err)
+ }
+ var compact bytes.Buffer
+ if err := json.Compact(&compact, stored.Declared); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(compact.Bytes(), before) {
+ t.Fatalf("declaration after the grant = %s, want %s", compact.Bytes(), before)
+ }
+ if bytes.Contains(raw, []byte(`"seed"`)) {
+ t.Fatalf("record = %s, want no seed key", raw)
+ }
+}