@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.
- package/fixtures/already-applied.json +124 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/package.json +1 -1
- package/schema-manifest.json +282 -3
- package/src/tooling.test.mjs +113 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_fedsession.test.sql +6 -4
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +29 -12
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +25 -9
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- 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
|
-
|
|
71
|
+
public.test_pubkey('sigrls-author-key'),
|
|
72
72
|
'raw_ed25519_base64url',
|
|
73
|
-
|
|
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 =
|
|
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
|
-
|
|
60
|
+
public.test_pubkey('swg-primary'),
|
|
61
61
|
'raw_ed25519_base64url',
|
|
62
|
-
|
|
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',
|
|
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,
|
|
28
|
-
|
|
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,
|
|
29
|
-
|
|
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
|
-
|
|
32
|
+
public.test_pubkey('proof-rpcs-a'),
|
|
33
33
|
'raw_ed25519_base64url',
|
|
34
|
-
|
|
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
|
-
|
|
103
|
+
public.test_pubkey('proof-rpcs-b'),
|
|
104
104
|
'raw_ed25519_base64url',
|
|
105
|
-
|
|
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
|
-
|
|
53
|
+
public.test_pubkey('sigstore-primary'),
|
|
54
54
|
'raw_ed25519_base64url',
|
|
55
|
-
|
|
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
|
-
|
|
74
|
+
public.test_pubkey('sigstore-secondary'),
|
|
75
75
|
'raw_ed25519_base64url',
|
|
76
|
-
|
|
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
|
-
|
|
98
|
+
public.test_pubkey('sigstore-unproven'),
|
|
99
99
|
'raw_ed25519_base64url',
|
|
100
|
-
|
|
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"
|