@consiliency/agent-board-schema 1.12.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/fixtures/already-applied.json +124 -0
  2. package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
  3. package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
  4. package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
  5. package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
  6. package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
  7. package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
  8. package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
  9. package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
  10. package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
  11. package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
  12. package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
  13. package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
  14. package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
  15. package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
  16. package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
  17. package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
  18. package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
  19. package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
  20. package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
  21. package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
  22. package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
  23. package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
  24. package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
  25. package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
  26. package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
  27. package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
  28. package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
  29. package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
  30. package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
  31. package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
  32. package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
  33. package/package.json +1 -1
  34. package/schema-manifest.json +282 -3
  35. package/src/tooling.test.mjs +113 -2
  36. package/tests/agent_board_actorspoof.test.sql +349 -0
  37. package/tests/agent_board_agentsdir.test.sql +154 -0
  38. package/tests/agent_board_authbound_boundary.test.sql +9 -1
  39. package/tests/agent_board_claimgate_enforce.test.sql +464 -0
  40. package/tests/agent_board_claimgate_predicate.test.sql +360 -0
  41. package/tests/agent_board_claimkey.test.sql +229 -0
  42. package/tests/agent_board_consent_backfill.test.sql +48 -0
  43. package/tests/agent_board_consent_emission.test.sql +601 -0
  44. package/tests/agent_board_consent_gating.test.sql +489 -0
  45. package/tests/agent_board_consent_substrate.test.sql +720 -0
  46. package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
  47. package/tests/agent_board_delegation_enforcement.test.sql +55 -6
  48. package/tests/agent_board_delegation_policies.test.sql +107 -1
  49. package/tests/agent_board_fedsession.test.sql +6 -4
  50. package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
  51. package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
  52. package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
  53. package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
  54. package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
  55. package/tests/agent_board_fleet_policy.test.sql +2 -2
  56. package/tests/agent_board_grants_hygiene.test.sql +408 -0
  57. package/tests/agent_board_identity_providers.test.sql +9 -0
  58. package/tests/agent_board_inbox_reads.test.sql +29 -12
  59. package/tests/agent_board_inbox_rpcs.test.sql +44 -34
  60. package/tests/agent_board_invitepolicy.test.sql +205 -0
  61. package/tests/agent_board_leaveidem.test.sql +194 -0
  62. package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
  63. package/tests/agent_board_mint_harden.test.sql +303 -0
  64. package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
  65. package/tests/agent_board_obsgate.test.sql +178 -0
  66. package/tests/agent_board_obsgate2.test.sql +153 -0
  67. package/tests/agent_board_obsgate3.test.sql +170 -0
  68. package/tests/agent_board_obsgate4.test.sql +196 -0
  69. package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
  70. package/tests/agent_board_pubexec_class.test.sql +53 -0
  71. package/tests/agent_board_rejectsem.test.sql +271 -0
  72. package/tests/agent_board_resigning_refresh.test.sql +1 -1
  73. package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
  74. package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
  75. package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
  76. package/tests/agent_board_rolereset.test.sql +351 -0
  77. package/tests/agent_board_roomdeleg.test.sql +5 -3
  78. package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
  79. package/tests/agent_board_roomlink.test.sql +25 -9
  80. package/tests/agent_board_rowid.test.sql +1274 -0
  81. package/tests/agent_board_signature_rls_reads.test.sql +3 -3
  82. package/tests/agent_board_signed_write_gateway.test.sql +3 -3
  83. package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
  84. package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
  85. package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
  86. package/tests/agent_board_signing_store.test.sql +6 -6
  87. package/tests/fixtures/isolation-substrate.sql +93 -0
  88. package/tests/fixtures/test-prereqs.sql +33 -0
  89. package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
  90. package/tests/isolation/expected/claimkey_renew_race.out +38 -0
  91. package/tests/isolation/expected/consent_approval_lock.out +43 -0
  92. package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
  93. package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
  94. package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
  95. package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
  96. package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
  97. package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
  98. package/tests/isolation/specs/consent_approval_lock.spec +98 -0
  99. package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
  100. package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
  101. package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
  102. package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
  103. package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
  104. package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
@@ -68,9 +68,9 @@ begin
68
68
  v_author,
69
69
  'primary',
70
70
  1,
71
- repeat('A', 43),
71
+ public.test_pubkey('sigrls-author-key'),
72
72
  'raw_ed25519_base64url',
73
- 'sha256:' || repeat('Q', 43),
73
+ public.test_key_fingerprint('sigrls-author-key'),
74
74
  'active',
75
75
  'proven'
76
76
  )
@@ -105,7 +105,7 @@ begin
105
105
  from agent_board.v_thread_feed
106
106
  where message_id = v_message
107
107
  and verification_status = 'verified'
108
- and key_fingerprint = 'sha256:' || repeat('Q', 43)
108
+ and key_fingerprint = public.test_key_fingerprint('sigrls-author-key')
109
109
  and key_version = 1
110
110
  and algorithm = 'Ed25519';
111
111
 
@@ -57,9 +57,9 @@ begin
57
57
  v_agent,
58
58
  'primary',
59
59
  1,
60
- repeat('A', 43),
60
+ public.test_pubkey('swg-primary'),
61
61
  'raw_ed25519_base64url',
62
- 'sha256:' || repeat('A', 43),
62
+ public.test_key_fingerprint('swg-primary'),
63
63
  'active',
64
64
  'proven'
65
65
  )
@@ -84,7 +84,7 @@ begin
84
84
  'key_id', 'primary',
85
85
  'key_version', 1,
86
86
  'algorithm', 'Ed25519',
87
- 'key_fingerprint', 'sha256:' || repeat('A', 43),
87
+ 'key_fingerprint', public.test_key_fingerprint('swg-primary'),
88
88
  'verification_status', 'verified',
89
89
  'payload_digest', 'sha256:' || repeat('d', 64),
90
90
  'signature_digest', 'sha256:' || repeat('e', 64),
@@ -24,8 +24,8 @@ begin
24
24
  insert into agent_board.agent_signing_keys(
25
25
  agent_id, key_id, key_version, public_key, public_key_format, fingerprint)
26
26
  values (
27
- v_agent, 'primary', 1, repeat('A', 43), 'raw_ed25519_base64url',
28
- 'sha256:' || repeat('Z', 43));
27
+ v_agent, 'primary', 1, public.test_pubkey('pcap-primary'), 'raw_ed25519_base64url',
28
+ public.test_key_fingerprint('pcap-primary'));
29
29
 
30
30
  set local role service_role;
31
31
 
@@ -25,8 +25,8 @@ begin
25
25
  insert into agent_board.agent_signing_keys(
26
26
  agent_id, key_id, key_version, public_key, public_key_format, fingerprint)
27
27
  values (
28
- v_agent, 'primary', 1, repeat('A', 43), 'raw_ed25519_base64url',
29
- 'sha256:' || repeat('Z', 43))
28
+ v_agent, 'primary', 1, public.test_pubkey('reap-chal-primary'), 'raw_ed25519_base64url',
29
+ public.test_key_fingerprint('reap-chal-primary'))
30
30
  returning id into v_key;
31
31
 
32
32
  insert into agent_board.agent_signing_key_challenges(
@@ -29,9 +29,9 @@ begin
29
29
  v_agent,
30
30
  'primary',
31
31
  1,
32
- repeat('A', 43),
32
+ public.test_pubkey('proof-rpcs-a'),
33
33
  'raw_ed25519_base64url',
34
- 'sha256:' || repeat('Z', 43)
34
+ public.test_key_fingerprint('proof-rpcs-a')
35
35
  );
36
36
 
37
37
  set local role service_role;
@@ -100,9 +100,9 @@ begin
100
100
  v_agent,
101
101
  'primary',
102
102
  1,
103
- repeat('B', 43),
103
+ public.test_pubkey('proof-rpcs-b'),
104
104
  'raw_ed25519_base64url',
105
- 'sha256:' || repeat('Y', 43)
105
+ public.test_key_fingerprint('proof-rpcs-b')
106
106
  );
107
107
 
108
108
  set local role service_role;
@@ -50,9 +50,9 @@ begin
50
50
  v_agent,
51
51
  'primary',
52
52
  1,
53
- repeat('A', 43),
53
+ public.test_pubkey('sigstore-primary'),
54
54
  'raw_ed25519_base64url',
55
- 'sha256:' || repeat('A', 43),
55
+ public.test_key_fingerprint('sigstore-primary'),
56
56
  'active',
57
57
  'proven'
58
58
  )
@@ -71,9 +71,9 @@ begin
71
71
  v_agent,
72
72
  'secondary',
73
73
  1,
74
- repeat('B', 43),
74
+ public.test_pubkey('sigstore-secondary'),
75
75
  'raw_ed25519_base64url',
76
- 'sha256:' || repeat('B', 43)
76
+ public.test_key_fingerprint('sigstore-secondary')
77
77
  );
78
78
  raise exception 'duplicate key_version unexpectedly succeeded';
79
79
  exception
@@ -95,9 +95,9 @@ begin
95
95
  v_agent,
96
96
  'unproven-active',
97
97
  2,
98
- repeat('C', 43),
98
+ public.test_pubkey('sigstore-unproven'),
99
99
  'raw_ed25519_base64url',
100
- 'sha256:' || repeat('C', 43),
100
+ public.test_key_fingerprint('sigstore-unproven'),
101
101
  'active',
102
102
  'unproven'
103
103
  );
@@ -0,0 +1,93 @@
1
+ -- Minimal Supabase substrate for the two-session ISOLATION harness only.
2
+ --
3
+ -- The pgTAP runner applies the agent_board bundle onto the DB that `supabase
4
+ -- start` provisions (54322/postgres), which already carries GoTrue's `auth`
5
+ -- schema, the `extensions` schema, pgcrypto, and the base roles. The isolation
6
+ -- harness instead builds a THROWAWAY template database (specs COMMIT across
7
+ -- sessions and cannot share the live DB), so it must self-provision the same
8
+ -- substrate the migrations assume.
9
+ --
10
+ -- Rather than replay GoTrue's full ~2000-line schema (webauthn/saml/oauth/mfa +
11
+ -- cross-schema net/role dependencies that drift with every GoTrue bump), this
12
+ -- reproduces ONLY the surface the agent_board migrations actually touch:
13
+ --
14
+ -- * auth.users FK target for agents.auth_user_id / owner_user_id, and
15
+ -- written by provision/ensure_github_user with 11 columns.
16
+ -- * auth.identities written by ensure_github_user; looked up by (provider,
17
+ -- provider_id).
18
+ -- * auth.uid()/role() read request.jwt.claim.* — the identity the RPCs gate on.
19
+ -- * extensions.pgcrypto extensions.digest / extensions.gen_random_bytes.
20
+ --
21
+ -- auth.sessions is intentionally omitted: it appears in the migrations only in a
22
+ -- comment, never in SQL. Base ROLES (service_role/authenticated/anon) are NOT
23
+ -- created here — they are cluster-global on the Supabase server this template is
24
+ -- built on (both local `supabase start` and CI), so migration GRANTs resolve.
25
+ --
26
+ -- If a future migration touches an auth column/table not modelled here, the apply
27
+ -- fails LOUDLY (a missing-column/relation error) — the signal to extend this file,
28
+ -- never a silent skip. Keep it minimal and driven by real usage.
29
+
30
+ create schema if not exists auth;
31
+ create schema if not exists extensions;
32
+
33
+ create extension if not exists pgcrypto with schema extensions;
34
+ create extension if not exists "uuid-ossp" with schema extensions;
35
+
36
+ -- GoTrue's auth.users, reduced to the columns the bundle inserts/reads. Everything
37
+ -- is nullable or defaulted so the canonical 11-column provision insert succeeds.
38
+ create table if not exists auth.users (
39
+ id uuid primary key,
40
+ instance_id uuid,
41
+ email text,
42
+ email_confirmed_at timestamptz,
43
+ encrypted_password text,
44
+ created_at timestamptz default now(),
45
+ updated_at timestamptz default now(),
46
+ raw_app_meta_data jsonb,
47
+ raw_user_meta_data jsonb,
48
+ aud text,
49
+ role text,
50
+ -- read-safety padding for common GoTrue columns a future path might SELECT
51
+ phone text,
52
+ confirmed_at timestamptz,
53
+ last_sign_in_at timestamptz,
54
+ is_sso_user boolean default false,
55
+ is_anonymous boolean default false
56
+ );
57
+
58
+ create table if not exists auth.identities (
59
+ id uuid primary key,
60
+ user_id uuid references auth.users(id) on delete cascade,
61
+ provider_id text,
62
+ provider text,
63
+ identity_data jsonb,
64
+ last_sign_in_at timestamptz,
65
+ created_at timestamptz default now(),
66
+ updated_at timestamptz default now(),
67
+ unique (provider, provider_id)
68
+ );
69
+
70
+ -- The identity helpers the bundle's RLS/RPCs gate on. Copied VERBATIM from the
71
+ -- live Supabase auth schema (supabase/postgres:17.6) so the harness exercises the
72
+ -- exact identity resolution production uses: coalesce the flattened
73
+ -- `request.jwt.claim.<key>` GUC with the `request.jwt.claims` JSON blob, so either
74
+ -- form the runtime sets resolves the caller.
75
+ create or replace function auth.uid() returns uuid
76
+ language sql stable
77
+ as $$
78
+ select
79
+ coalesce(
80
+ nullif(current_setting('request.jwt.claim.sub', true), ''),
81
+ (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
82
+ )::uuid
83
+ $$;
84
+
85
+ create or replace function auth.role() returns text
86
+ language sql stable
87
+ as $$
88
+ select
89
+ coalesce(
90
+ nullif(current_setting('request.jwt.claim.role', true), ''),
91
+ (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role')
92
+ )::text
93
+ $$;
@@ -82,3 +82,36 @@ $$;
82
82
 
83
83
  REVOKE ALL ON FUNCTION public.ensure_github_user(text, text, text, text) FROM PUBLIC;
84
84
  GRANT EXECUTE ON FUNCTION public.ensure_github_user(text, text, text, text) TO service_role;
85
+
86
+ -- ---------------------------------------------------------------------------
87
+ -- Deterministic consistent (public_key, fingerprint) pair generators for pgTAP
88
+ -- fixtures. MINTHARDEN (#138) installs a BEFORE INSERT OR UPDATE trigger on
89
+ -- agent_board.agent_signing_keys asserting fingerprint = raw-byte derivation of
90
+ -- public_key for raw_ed25519_base64url keys. Fixtures that seed keys directly must
91
+ -- therefore use a CANONICAL key whose fingerprint MATCHES. These helpers derive a
92
+ -- distinct canonical 43-char base64url raw-Ed25519 key from a seed (public_key =
93
+ -- base64url(sha256(seed))) and the matching raw-byte fingerprint
94
+ -- (sha256:base64url(sha256(raw_bytes))) — byte-identical to agent_board._raw_key_fingerprint
95
+ -- WITHOUT depending on it (so this file still loads before the migrations). Seed on a
96
+ -- distinct value per row (its key/agent uuid or a unique label) to avoid UNIQUE collisions.
97
+ CREATE OR REPLACE FUNCTION public.test_pubkey(p_seed text)
98
+ RETURNS text
99
+ LANGUAGE sql
100
+ IMMUTABLE
101
+ AS $$
102
+ SELECT rtrim(translate(encode(extensions.digest(convert_to(p_seed, 'UTF8'), 'sha256'), 'base64'), '+/', '-_'), '=');
103
+ $$;
104
+
105
+ CREATE OR REPLACE FUNCTION public.test_key_fingerprint(p_seed text)
106
+ RETURNS text
107
+ LANGUAGE sql
108
+ IMMUTABLE
109
+ AS $$
110
+ SELECT 'sha256:' || rtrim(translate(encode(
111
+ extensions.digest(extensions.digest(convert_to(p_seed, 'UTF8'), 'sha256'), 'sha256'),
112
+ 'base64'), '+/', '-_'), '=');
113
+ $$;
114
+
115
+ REVOKE ALL ON FUNCTION public.test_pubkey(text) FROM PUBLIC;
116
+ REVOKE ALL ON FUNCTION public.test_key_fingerprint(text) FROM PUBLIC;
117
+ GRANT EXECUTE ON FUNCTION public.test_pubkey(text), public.test_key_fingerprint(text) TO service_role;
@@ -0,0 +1,36 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: sa_begin sa_acq sb_begin sb_acq sa_commit sb_commit chk
4
+ step sa_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = 'c0a70000-0000-4000-8000-0000000000aa';
5
+ step sa_acq:
6
+ select resource_key
7
+ from agent_board.acquire_claim(null, 'file', 'cg-iso-sa', now() + interval '1 hour', null, null);
8
+
9
+ resource_key
10
+ ------------
11
+ cg-iso-sa
12
+ (1 row)
13
+
14
+ step sb_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = 'c0a70000-0000-4000-8000-0000000000aa';
15
+ step sb_acq:
16
+ select resource_key
17
+ from agent_board.acquire_claim(null, 'file', 'cg-iso-sb', now() + interval '1 hour', null, null);
18
+ <waiting ...>
19
+ step sa_commit: COMMIT;
20
+ step sb_acq: <... completed>
21
+ ERROR: ownerless active-claim quota reached (50 per agent)
22
+ step sb_commit: COMMIT;
23
+ step chk:
24
+ select count(*)::int as active_file_claims
25
+ from agent_board.claims
26
+ where agent_id = 'c0a70000-0000-4000-8000-0000000000bb'
27
+ and resource_kind = 'file'
28
+ and status = 'active'
29
+ and released_at is null
30
+ and lease_expires_at > now();
31
+
32
+ active_file_claims
33
+ ------------------
34
+ 50
35
+ (1 row)
36
+
@@ -0,0 +1,38 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: ra_begin ra_renew rb_begin rb_renew ra_commit rb_commit chk
4
+ step ra_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
5
+ step ra_renew:
6
+ select resource_key
7
+ from agent_board.renew_claim('00000000-0000-4000-8000-0000000000c1', now() + interval '2 hours');
8
+
9
+ resource_key
10
+ ------------
11
+ fpRace
12
+ (1 row)
13
+
14
+ step rb_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
15
+ step rb_renew:
16
+ select resource_key
17
+ from agent_board.renew_claim('00000000-0000-4000-8000-0000000000c2', now() + interval '2 hours');
18
+ <waiting ...>
19
+ step ra_commit: COMMIT;
20
+ step rb_renew: <... completed>
21
+ resource_key
22
+ ------------
23
+ fpRace
24
+ (1 row)
25
+
26
+ step rb_commit: COMMIT;
27
+ step chk:
28
+ select resource_key
29
+ from agent_board.claims
30
+ where lease_token in ('00000000-0000-4000-8000-0000000000c1','00000000-0000-4000-8000-0000000000c2')
31
+ order by resource_key;
32
+
33
+ resource_key
34
+ ------------
35
+ fpRace
36
+ fpRace
37
+ (2 rows)
38
+
@@ -0,0 +1,43 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: a_begin a_resolve w_begin w_leave a_commit w_commit chk
4
+ step a_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
5
+ step a_resolve:
6
+ select membership_state
7
+ from agent_board.resolve_board_request(
8
+ '115b0000-0000-4000-8000-000000000001',
9
+ '115a0000-0000-4000-8000-000000000002',
10
+ 'joined',
11
+ '115a0000-0000-4000-8000-000000000001');
12
+
13
+ membership_state
14
+ ----------------
15
+ joined
16
+ (1 row)
17
+
18
+ step w_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
19
+ step w_leave:
20
+ select membership_state
21
+ from agent_board.update_board_membership(
22
+ '115b0000-0000-4000-8000-000000000001',
23
+ '115a0000-0000-4000-8000-000000000002',
24
+ 'left');
25
+ <waiting ...>
26
+ step a_commit: COMMIT;
27
+ step w_leave: <... completed>
28
+ membership_state
29
+ ----------------
30
+ left
31
+ (1 row)
32
+
33
+ step w_commit: COMMIT;
34
+ step chk:
35
+ select membership_state
36
+ from agent_board.board_memberships
37
+ where id = '115c0000-0000-4000-8000-0000000000aa';
38
+
39
+ membership_state
40
+ ----------------
41
+ left
42
+ (1 row)
43
+
@@ -0,0 +1,37 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: m_begin m_sub b_begin b_boot m_commit b_commit chk
4
+ step m_begin: BEGIN;
5
+ step m_sub:
6
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_by_agent_id)
7
+ values ('105b0000-0000-4000-8000-000000000001', '105a0000-0000-4000-8000-000000000001',
8
+ 'subscribed', 'member', '105a0000-0000-4000-8000-000000000001');
9
+
10
+ step b_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
11
+ step b_boot:
12
+ select board_id, owner_agent_id
13
+ from agent_board.bootstrap_board_owner_memberships()
14
+ where board_id = '105b0000-0000-4000-8000-000000000001';
15
+ <waiting ...>
16
+ step m_commit: COMMIT;
17
+ step b_boot: <... completed>
18
+ board_id |owner_agent_id
19
+ ------------------------------------+------------------------------------
20
+ 105b0000-0000-4000-8000-000000000001|105a0000-0000-4000-8000-000000000001
21
+ (1 row)
22
+
23
+ step b_commit: COMMIT;
24
+ step chk:
25
+ select result_state, event_kind, provenance
26
+ from agent_board.board_membership_events
27
+ where board_id = '105b0000-0000-4000-8000-000000000001'
28
+ and agent_id = '105a0000-0000-4000-8000-000000000001'
29
+ and event_kind = 'operator_repair'
30
+ order by seq desc
31
+ limit 1;
32
+
33
+ result_state|event_kind |provenance
34
+ ------------+---------------+-------------------
35
+ subscribed |operator_repair|operator-designated
36
+ (1 row)
37
+
@@ -0,0 +1,40 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: ra_begin ra_reclaim rb_begin rb_revoke ra_commit rb_commit chk
4
+ step ra_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
5
+ step ra_reclaim:
6
+ select owner_agent_id
7
+ from agent_board.reclaim_fleet_trust_domain(
8
+ 'd2000000-0000-4000-8000-000000000001', 'a2000000-0000-4000-8000-000000000003',
9
+ 'a2000000-0000-4000-8000-000000000001', 'iso operator domain reclaim');
10
+
11
+ owner_agent_id
12
+ ------------------------------------
13
+ a2000000-0000-4000-8000-000000000003
14
+ (1 row)
15
+
16
+ step rb_begin:
17
+ BEGIN;
18
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
19
+ SET LOCAL "request.jwt.claim.sub" = 'b2000000-0000-4000-8000-000000000002';
20
+
21
+ step rb_revoke:
22
+ select revocation_event_id
23
+ from agent_board.revoke_fleet_subject(
24
+ 'trust_domain', 'd2000000-0000-4000-8000-000000000001', 'a2000000-0000-4000-8000-000000000002',
25
+ 'iso squatter domain revoke', jsonb_build_object('redacted', true));
26
+ <waiting ...>
27
+ step ra_commit: COMMIT;
28
+ step rb_revoke: <... completed>
29
+ ERROR: trust domain is not owned by current_agent_id()
30
+ step rb_commit: COMMIT;
31
+ step chk:
32
+ select owner_agent_id, lifecycle_state
33
+ from agent_board.fleet_trust_domains
34
+ where id = 'd2000000-0000-4000-8000-000000000001';
35
+
36
+ owner_agent_id |lifecycle_state
37
+ ------------------------------------+---------------
38
+ a2000000-0000-4000-8000-000000000003|active
39
+ (1 row)
40
+
@@ -0,0 +1,80 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: ra_begin ra_react rb_begin rb_revoke ra_commit rb_commit chk
4
+ step ra_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
5
+ step ra_react:
6
+ select lifecycle_state
7
+ from agent_board.reactivate_fleet_machine(
8
+ '60000000-0000-4000-8000-000000000001', 'a0000000-0000-4000-8000-000000000001', 'iso reactivate');
9
+
10
+ lifecycle_state
11
+ ---------------
12
+ active
13
+ (1 row)
14
+
15
+ step rb_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
16
+ step rb_revoke:
17
+ select revocation_event_id
18
+ from agent_board.revoke_fleet_subject(
19
+ 'machine', '60000000-0000-4000-8000-000000000001', 'a0000000-0000-4000-8000-000000000001',
20
+ 'iso re-revoke', jsonb_build_object('redacted', true));
21
+ <waiting ...>
22
+ step ra_commit: COMMIT;
23
+ step rb_revoke: <... completed>
24
+ revocation_event_id
25
+ ------------------------------------
26
+ e0000000-0000-4000-8000-000000000001
27
+ (1 row)
28
+
29
+ step rb_commit: COMMIT;
30
+ step chk:
31
+ select fm.lifecycle_state,
32
+ (select denial_class
33
+ from agent_board.evaluate_fleet_sender_policy(
34
+ 'a0000000-0000-4000-8000-000000000002', 'a0000000-0000-4000-8000-000000000003',
35
+ fm.id, null, 'send_direct_message', null, false)) as denial_class
36
+ from agent_board.fleet_machines fm
37
+ where fm.id = '60000000-0000-4000-8000-000000000001';
38
+
39
+ lifecycle_state|denial_class
40
+ ---------------+---------------
41
+ revoked |machine_revoked
42
+ (1 row)
43
+
44
+
45
+ starting permutation: ra_begin ra_react rb_begin rb_react ra_commit rb_commit chk
46
+ step ra_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
47
+ step ra_react:
48
+ select lifecycle_state
49
+ from agent_board.reactivate_fleet_machine(
50
+ '60000000-0000-4000-8000-000000000001', 'a0000000-0000-4000-8000-000000000001', 'iso reactivate');
51
+
52
+ lifecycle_state
53
+ ---------------
54
+ active
55
+ (1 row)
56
+
57
+ step rb_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
58
+ step rb_react:
59
+ select lifecycle_state
60
+ from agent_board.reactivate_fleet_machine(
61
+ '60000000-0000-4000-8000-000000000001', 'a0000000-0000-4000-8000-000000000001', 'iso second reactivate');
62
+ <waiting ...>
63
+ step ra_commit: COMMIT;
64
+ step rb_react: <... completed>
65
+ ERROR: fleet machine is not revoked; nothing to reactivate
66
+ step rb_commit: COMMIT;
67
+ step chk:
68
+ select fm.lifecycle_state,
69
+ (select denial_class
70
+ from agent_board.evaluate_fleet_sender_policy(
71
+ 'a0000000-0000-4000-8000-000000000002', 'a0000000-0000-4000-8000-000000000003',
72
+ fm.id, null, 'send_direct_message', null, false)) as denial_class
73
+ from agent_board.fleet_machines fm
74
+ where fm.id = '60000000-0000-4000-8000-000000000001';
75
+
76
+ lifecycle_state|denial_class
77
+ ---------------+------------
78
+ active |
79
+ (1 row)
80
+
@@ -0,0 +1,40 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: ra_begin ra_reclaim rb_begin rb_revoke ra_commit rb_commit chk
4
+ step ra_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
5
+ step ra_reclaim:
6
+ select owner_agent_id
7
+ from agent_board.reclaim_fleet_machine(
8
+ '61000000-0000-4000-8000-000000000001', 'a1000000-0000-4000-8000-000000000003',
9
+ 'a1000000-0000-4000-8000-000000000001', 'iso operator reclaim');
10
+
11
+ owner_agent_id
12
+ ------------------------------------
13
+ a1000000-0000-4000-8000-000000000003
14
+ (1 row)
15
+
16
+ step rb_begin:
17
+ BEGIN;
18
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
19
+ SET LOCAL "request.jwt.claim.sub" = 'b1000000-0000-4000-8000-000000000002';
20
+
21
+ step rb_revoke:
22
+ select revocation_event_id
23
+ from agent_board.revoke_fleet_subject(
24
+ 'machine', '61000000-0000-4000-8000-000000000001', 'a1000000-0000-4000-8000-000000000002',
25
+ 'iso squatter revoke', jsonb_build_object('redacted', true));
26
+ <waiting ...>
27
+ step ra_commit: COMMIT;
28
+ step rb_revoke: <... completed>
29
+ ERROR: machine is not owned by current_agent_id()
30
+ step rb_commit: COMMIT;
31
+ step chk:
32
+ select owner_agent_id, lifecycle_state
33
+ from agent_board.fleet_machines
34
+ where id = '61000000-0000-4000-8000-000000000001';
35
+
36
+ owner_agent_id |lifecycle_state
37
+ ------------------------------------+---------------
38
+ a1000000-0000-4000-8000-000000000003|active
39
+ (1 row)
40
+
@@ -0,0 +1,77 @@
1
+ # CLAIMGATE/SHADOW (#133) — the ownerless per-agent active-claim QUOTA is transactionally
2
+ # serialized. The agent already holds (quota - 1) = 49 active 'file' claims. Two sessions then
3
+ # each acquire ONE MORE ownerless claim on DISTINCT keys concurrently. The row-level uniqueness
4
+ # index does NOT serialize them (different keys), so a naive count-before-insert would let BOTH
5
+ # observe 49 (< 50) and BOTH insert — 51 active claims, quota bypassed.
6
+ #
7
+ # The shipped acquire_claim takes an actor-scoped pg_advisory_xact_lock BEFORE the count, so the
8
+ # two acquisitions by the same actor SERIALIZE: session sb BLOCKS on sa's lock until sa commits,
9
+ # then counts 50 (>= 50) and is DENIED (errcode 54000). Final state: exactly 50 active claims.
10
+ #
11
+ # WHAT THIS PINS (a TRACE): sb's acquire prints `<waiting ...>` on sa's advisory lock; after sa
12
+ # commits, sb COMPLETES with `ERROR: ownerless active-claim quota reached`. The checker sees 50.
13
+ #
14
+ # MUTATION SIGNAL (verified in the PR): remove the pg_advisory_xact_lock line from acquire_claim
15
+ # -> sb no longer waits, both count 49 and both INSERT, no error line, and the checker sees 51.
16
+ # The golden diverges on both the missing `<waiting ...>` and the 50-vs-51 count.
17
+ #
18
+ # Sessions run as `authenticated` (NOT service_role — the operator carve-out exempts service_role
19
+ # from the quota), with request.jwt.claim.sub selecting the quota agent.
20
+
21
+ setup
22
+ {
23
+ -- SHADOW ships the ownerless guards OFF by default; this spec pins the quota ENFORCEMENT path,
24
+ -- so enable the guard flag first (the state an operator/ENFORCE reaches post-census).
25
+ insert into agent_board.claimgate_enforcement_flags(scope_key, enabled) values ('ownerless_guards', true)
26
+ on conflict (scope_key) do update set enabled = true;
27
+ insert into auth.users (id) values ('c0a70000-0000-4000-8000-0000000000aa');
28
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
29
+ values ('c0a70000-0000-4000-8000-0000000000bb', 'agent', 'agent', 'cg-quota-iso', 'CG Quota Iso', 'active',
30
+ 'c0a70000-0000-4000-8000-0000000000aa', 'c0a70000-0000-4000-8000-0000000000aa');
31
+ -- seed (quota - 1) active file claims for the agent.
32
+ insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
33
+ select 'c0a70000-0000-4000-8000-0000000000bb', 'file', 'cg-iso-seed-' || g, now() + interval '1 hour'
34
+ from generate_series(1, agent_board._claimgate_ownerless_quota() - 1) g;
35
+ }
36
+
37
+ teardown
38
+ {
39
+ delete from agent_board.claims where agent_id = 'c0a70000-0000-4000-8000-0000000000bb';
40
+ delete from agent_board.agents where id = 'c0a70000-0000-4000-8000-0000000000bb';
41
+ delete from auth.users where id = 'c0a70000-0000-4000-8000-0000000000aa';
42
+ delete from agent_board.claimgate_enforcement_flags where scope_key = 'ownerless_guards';
43
+ }
44
+
45
+ # sa acquires the 50th (=quota) claim: it takes the actor advisory lock, counts 49 (< 50), inserts,
46
+ # and holds the lock until commit.
47
+ session "sa"
48
+ step "sa_begin" { BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = 'c0a70000-0000-4000-8000-0000000000aa'; }
49
+ step "sa_acq" {
50
+ select resource_key
51
+ from agent_board.acquire_claim(null, 'file', 'cg-iso-sa', now() + interval '1 hour', null, null);
52
+ }
53
+ step "sa_commit" { COMMIT; }
54
+
55
+ # sb acquires a DIFFERENT-key claim: its acquire blocks on sa's advisory lock, then on sa commit
56
+ # counts 50 (>= 50) and raises the quota error.
57
+ session "sb"
58
+ step "sb_begin" { BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = 'c0a70000-0000-4000-8000-0000000000aa'; }
59
+ step "sb_acq" {
60
+ select resource_key
61
+ from agent_board.acquire_claim(null, 'file', 'cg-iso-sb', now() + interval '1 hour', null, null);
62
+ }
63
+ step "sb_commit" { COMMIT; }
64
+
65
+ # checker: exactly 50 active file claims survive (sa's succeeded, sb's was denied).
66
+ session "chk"
67
+ step "chk" {
68
+ select count(*)::int as active_file_claims
69
+ from agent_board.claims
70
+ where agent_id = 'c0a70000-0000-4000-8000-0000000000bb'
71
+ and resource_kind = 'file'
72
+ and status = 'active'
73
+ and released_at is null
74
+ and lease_expires_at > now();
75
+ }
76
+
77
+ permutation "sa_begin" "sa_acq" "sb_begin" "sb_acq" "sa_commit" "sb_commit" "chk"