@consiliency/agent-board-schema 1.12.0 → 1.23.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 +160 -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/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
- package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
- package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
- package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
- package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
- package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
- package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
- package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
- package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
- package/package.json +1 -1
- package/schema-manifest.json +363 -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_atomic_fleet_onboarding.test.sql +623 -0
- package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -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_enroll_federated_identity.test.sql +3 -3
- package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
- package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
- package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
- package/tests/agent_board_fedsession.test.sql +51 -5
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
- 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 +45 -31
- 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_outbox_wake_ordering.test.sql +452 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +14 -3
- 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 +27 -11
- 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/agent_board_wake_recovery.test.sql +756 -0
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
- package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
- package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
- package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
- package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
- package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
- package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
- package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
- package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
- package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -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/enroll_identity_board_owner_lock_order.out +55 -0
- package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
- package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
- package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
- package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
- package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
- package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -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/enroll_identity_board_owner_lock_order.spec +117 -0
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
- package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -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
- package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
-- MBROLERESET — role is not identity across membership re-entry. Closes message-board#79.
|
|
2
|
+
--
|
|
3
|
+
-- THEME: an elevated board role must not survive a non-active -> active transition. Today
|
|
4
|
+
-- `_upsert_board_membership_state` never writes `role` (INSERT omits it; UPDATE omits it), so
|
|
5
|
+
-- a removed/left/requested/invited row that re-enters as joined|subscribed keeps whatever role
|
|
6
|
+
-- was last written — including a prior admin/owner, and including grants applied while the row
|
|
7
|
+
-- was still non-active (the re-arm half of #79). Separately, `grant_board_role` matches any
|
|
8
|
+
-- membership row by board_id+agent_id alone, so a grant onto a removed row succeeds and parks
|
|
9
|
+
-- the elevated role for the next re-entry.
|
|
10
|
+
--
|
|
11
|
+
-- CHANGE 1 — re-entry role clamp in `_upsert_board_membership_state` (UPDATE branch only).
|
|
12
|
+
-- When the target state is joined|subscribed AND the pre-read origin is NOT already
|
|
13
|
+
-- joined|subscribed, set role = 'member' (the table default at 20260717000100:13-15). Hardcoded
|
|
14
|
+
-- — do not read information_schema.columns.column_default at runtime (it returns the quoted
|
|
15
|
+
-- expression '''member''::text', not the value, and assigning it trips the CHECK). The INSERT
|
|
16
|
+
-- branch is intentionally untouched: no-prior-row entry already omits role and takes the table
|
|
17
|
+
-- default. Active -> active (joined <-> subscribed) preserves role, including owner.
|
|
18
|
+
--
|
|
19
|
+
-- This clamp is NECESSARY but NOT a chokepoint. Four raw-INSERT writers bypass
|
|
20
|
+
-- `_upsert_board_membership_state` entirely and are deferred to a follow-up phase (MBROWID):
|
|
21
|
+
-- * create_board owner seed (20260723000100:103-106)
|
|
22
|
+
-- * bootstrap_board_owner_memberships (20260723000100:149-152)
|
|
23
|
+
-- * _redeem_invite_for_agent (20260724000100:345-360)
|
|
24
|
+
-- * _fedsession_autojoin_default_board (20260729000100:319-330)
|
|
25
|
+
-- Those paths intentionally write non-default roles (owner / preset admin). A trigger on
|
|
26
|
+
-- board_memberships is FORBIDDEN for the same reason: a BEFORE INSERT/UPDATE trigger would
|
|
27
|
+
-- also fire for those four seeders and erase their intentional roles. The clamp lives in the
|
|
28
|
+
-- mutator only.
|
|
29
|
+
--
|
|
30
|
+
-- No service_role exemption. An operator re-activating an evicted agent gets the default role;
|
|
31
|
+
-- elevation is an explicit subsequent grant_board_role call.
|
|
32
|
+
--
|
|
33
|
+
-- CHANGE 2 — active-membership filter in `grant_board_role` (pre-check AND UPDATE).
|
|
34
|
+
-- Both places gain `and bm.membership_state in ('joined','subscribed')`. Pre-check alone leaves
|
|
35
|
+
-- a TOCTOU window (eviction between check and UPDATE); UPDATE alone leaves v_updated NULL so
|
|
36
|
+
-- RETURN QUERY emits a bogus all-null row. Error message and errcode stay exactly
|
|
37
|
+
-- 'grantee has no membership on this board' / P0002. Grantor binding
|
|
38
|
+
-- (_resolved_actor_agent_id) and _grant_within_ceiling are unchanged — no privilege-escalation
|
|
39
|
+
-- regression.
|
|
40
|
+
--
|
|
41
|
+
-- SOLE-OWNER LOCKOUT (documented limitation, not a defect of this migration): a sole owner who
|
|
42
|
+
-- self-leaves is clamped to 'member' on rejoin, and grant_board_role CANNOT restore them --
|
|
43
|
+
-- it checks _grant_within_ceiling unconditionally with no service_role bypass, so an operator
|
|
44
|
+
-- must nominate a grantor that already holds rank, and by construction none exists.
|
|
45
|
+
-- Recovery DOES exist and is an RPC: leave again (so the creator holds no ACTIVE membership),
|
|
46
|
+
-- then call bootstrap_board_owner_memberships(), which raw-inserts a joined/owner row for a
|
|
47
|
+
-- creator lacking one (20260723000100:138-166). Exercised end-to-end by assertion 23 of
|
|
48
|
+
-- tests/agent_board_rolereset.test.sql. An earlier draft of this header claimed there was NO
|
|
49
|
+
-- RPC recovery; that was refuted during review and is corrected here rather than shipped into
|
|
50
|
+
-- immutable released bytes.
|
|
51
|
+
--
|
|
52
|
+
-- Additive, forward-only. No signature change. Last-definer rule: bodies are copied forward from
|
|
53
|
+
-- 20260801000100 (_upsert) and 20260723000200 (grant_board_role) and modified in place.
|
|
54
|
+
|
|
55
|
+
-- 1. _upsert_board_membership_state: re-entry role clamp on the UPDATE branch only ------------
|
|
56
|
+
create or replace function agent_board._upsert_board_membership_state(
|
|
57
|
+
p_board_id uuid,
|
|
58
|
+
p_agent_id uuid,
|
|
59
|
+
p_membership_state text,
|
|
60
|
+
p_created_by_agent_id uuid default null,
|
|
61
|
+
p_metadata jsonb default '{}'::jsonb
|
|
62
|
+
)
|
|
63
|
+
returns agent_board.board_memberships
|
|
64
|
+
language plpgsql
|
|
65
|
+
security definer
|
|
66
|
+
set search_path = agent_board, public
|
|
67
|
+
as $$
|
|
68
|
+
declare
|
|
69
|
+
v_membership agent_board.board_memberships%rowtype;
|
|
70
|
+
v_metadata jsonb;
|
|
71
|
+
begin
|
|
72
|
+
if p_membership_state not in ('requested', 'invited', 'joined', 'subscribed', 'left', 'removed') then
|
|
73
|
+
raise exception 'invalid board membership state' using errcode = '22023';
|
|
74
|
+
end if;
|
|
75
|
+
|
|
76
|
+
-- Metadata must be a JSON OBJECT. Previously a scalar/array was stored verbatim on insert and
|
|
77
|
+
-- concatenated on update; the reserved-key strip below iterates keys, so a non-object would raise
|
|
78
|
+
-- an opaque internal error. Fail explicitly instead. Reachable from the CLI `--metadata`, which
|
|
79
|
+
-- parses arbitrary JSON — this is a deliberate, declared behavior change for ALL mutator callers.
|
|
80
|
+
if jsonb_typeof(coalesce(p_metadata, '{}'::jsonb)) <> 'object' then
|
|
81
|
+
raise exception 'board membership metadata must be a JSON object' using errcode = '22023';
|
|
82
|
+
end if;
|
|
83
|
+
|
|
84
|
+
-- Reserved namespace: `ops_*` keys are operational state (the retention sweep's skip marker
|
|
85
|
+
-- `ops_retained_at`, read for board_memberships at 20260513000001:122) and must never be writable
|
|
86
|
+
-- by a caller whose own row they govern. Strip them from caller input; operational writers set
|
|
87
|
+
-- the column directly.
|
|
88
|
+
select coalesce(jsonb_object_agg(e.key, e.value), '{}'::jsonb)
|
|
89
|
+
into v_metadata
|
|
90
|
+
from jsonb_each(coalesce(p_metadata, '{}'::jsonb)) as e(key, value)
|
|
91
|
+
where e.key not like 'ops\_%';
|
|
92
|
+
|
|
93
|
+
select *
|
|
94
|
+
into v_membership
|
|
95
|
+
from agent_board.board_memberships bm
|
|
96
|
+
where bm.board_id = p_board_id
|
|
97
|
+
and bm.agent_id = p_agent_id
|
|
98
|
+
order by bm.created_at desc
|
|
99
|
+
limit 1
|
|
100
|
+
for update;
|
|
101
|
+
|
|
102
|
+
if v_membership.id is null then
|
|
103
|
+
-- INSERT branch intentionally omits `role` so a no-prior-row entry takes the table default
|
|
104
|
+
-- (20260717000100:13-15). Do not write role here — an explicit write is wrong.
|
|
105
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, left_at, metadata)
|
|
106
|
+
values (
|
|
107
|
+
p_board_id,
|
|
108
|
+
p_agent_id,
|
|
109
|
+
p_membership_state,
|
|
110
|
+
p_created_by_agent_id,
|
|
111
|
+
case when p_membership_state in ('left', 'removed') then now() else null end,
|
|
112
|
+
v_metadata
|
|
113
|
+
)
|
|
114
|
+
returning * into v_membership;
|
|
115
|
+
else
|
|
116
|
+
update agent_board.board_memberships bm
|
|
117
|
+
set membership_state = p_membership_state,
|
|
118
|
+
created_by_agent_id = coalesce(p_created_by_agent_id, bm.created_by_agent_id),
|
|
119
|
+
-- PRESERVE the first departure time. Restamping it would let the departing agent (or a
|
|
120
|
+
-- re-kicking owner) push its own row past the retention cutoff (20260513000001:121).
|
|
121
|
+
left_at = case
|
|
122
|
+
when p_membership_state in ('left', 'removed') then
|
|
123
|
+
case
|
|
124
|
+
-- SAME terminal state re-asserted: preserve the FIRST departure time so a
|
|
125
|
+
-- repeat call cannot push the retention cutoff forward.
|
|
126
|
+
when v_membership.membership_state = p_membership_state
|
|
127
|
+
then coalesce(v_membership.left_at, now())
|
|
128
|
+
-- GENUINE transition (notably left -> removed, an owner barring
|
|
129
|
+
-- invite-link re-entry for an already-departed agent) stamps NOW. R-LINK-7
|
|
130
|
+
-- deliberately does not block voluntary 'left' (20260724000100:321-322),
|
|
131
|
+
-- so this is the real kick moment; inheriting the earlier departure would
|
|
132
|
+
-- both lose the audit answer and make the marker row retention-eligible
|
|
133
|
+
-- prematurely.
|
|
134
|
+
else now()
|
|
135
|
+
end
|
|
136
|
+
else null
|
|
137
|
+
end,
|
|
138
|
+
-- ROLERESET (#79): clamp role to the table default on non-active -> active re-entry.
|
|
139
|
+
-- Hardcoded 'member' (column default at 20260717000100:13-15); do not read
|
|
140
|
+
-- information_schema at runtime. Active -> active preserves bm.role (including owner).
|
|
141
|
+
-- Unconditional — no service_role exemption.
|
|
142
|
+
role = case
|
|
143
|
+
when p_membership_state in ('joined', 'subscribed')
|
|
144
|
+
and v_membership.membership_state not in ('joined', 'subscribed')
|
|
145
|
+
then 'member'
|
|
146
|
+
else bm.role
|
|
147
|
+
end,
|
|
148
|
+
metadata = coalesce(bm.metadata, '{}'::jsonb) || v_metadata,
|
|
149
|
+
updated_at = now()
|
|
150
|
+
where bm.id = v_membership.id
|
|
151
|
+
returning * into v_membership;
|
|
152
|
+
end if;
|
|
153
|
+
|
|
154
|
+
return v_membership;
|
|
155
|
+
end;
|
|
156
|
+
$$;
|
|
157
|
+
|
|
158
|
+
comment on function agent_board._upsert_board_membership_state(uuid, uuid, text, uuid, jsonb) is
|
|
159
|
+
'UNAUTHORIZED PRIMITIVE — accepts any membership state; every caller MUST gate. ROLERESET '
|
|
160
|
+
'(#79): on the UPDATE path only, clamps role to the table default (''member'', '
|
|
161
|
+
'20260717000100:13-15) when transitioning from a non-active origin into joined|subscribed; '
|
|
162
|
+
'INSERT omits role and takes the table default. Active->active preserves role. LEAVEIDEM '
|
|
163
|
+
'(#81): preserves the FIRST left_at across repeated terminal transitions (the retention sweep '
|
|
164
|
+
'keys on left_at, 20260513000001:121) and strips the reserved `ops_*` metadata namespace from '
|
|
165
|
+
'caller input (the sweep skips rows carrying ops_retained_at). Not a chokepoint — raw-INSERT '
|
|
166
|
+
'seeders (create_board owner, bootstrap, invite redeem, fedsession autojoin) bypass this '
|
|
167
|
+
'function and are out of scope (MBROWID).';
|
|
168
|
+
|
|
169
|
+
-- 2. grant_board_role: refuse grants onto non-active memberships (pre-check + UPDATE) ---------
|
|
170
|
+
create or replace function agent_board.grant_board_role(
|
|
171
|
+
p_board_id uuid,
|
|
172
|
+
p_agent_id uuid,
|
|
173
|
+
p_grantee_role text,
|
|
174
|
+
p_grantor_agent_id uuid default null
|
|
175
|
+
)
|
|
176
|
+
returns table(board_id uuid, agent_id uuid, role text, updated_at timestamptz)
|
|
177
|
+
language plpgsql
|
|
178
|
+
security definer
|
|
179
|
+
set search_path = agent_board, public
|
|
180
|
+
as $$
|
|
181
|
+
declare
|
|
182
|
+
v_grantor uuid;
|
|
183
|
+
v_updated agent_board.board_memberships%rowtype;
|
|
184
|
+
begin
|
|
185
|
+
if p_grantee_role not in ('owner', 'admin', 'member', 'observer') then
|
|
186
|
+
raise exception 'invalid grantee role' using errcode = '22023';
|
|
187
|
+
end if;
|
|
188
|
+
|
|
189
|
+
-- Grantor is the RESOLVED actor. service_role -> supplied arg; authenticated ->
|
|
190
|
+
-- current_agent_id() and raises 42501 if null (suspended/unmapped) or if the
|
|
191
|
+
-- supplied arg <> current_agent_id(). This closes the single-principal escalation.
|
|
192
|
+
v_grantor := agent_board._resolved_actor_agent_id(p_grantor_agent_id, 'p_grantor_agent_id');
|
|
193
|
+
|
|
194
|
+
if not exists (
|
|
195
|
+
select 1 from agent_board.board_memberships bm
|
|
196
|
+
where bm.board_id = p_board_id and bm.agent_id = p_agent_id
|
|
197
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
198
|
+
) then
|
|
199
|
+
raise exception 'grantee has no membership on this board' using errcode = 'P0002';
|
|
200
|
+
end if;
|
|
201
|
+
|
|
202
|
+
-- Ceiling consumption: grantee-bounded against min(grantor role, board ceiling).
|
|
203
|
+
if not agent_board._grant_within_ceiling(v_grantor, p_board_id, p_grantee_role) then
|
|
204
|
+
raise exception 'grant exceeds grantor role or board authority ceiling' using errcode = '42501';
|
|
205
|
+
end if;
|
|
206
|
+
|
|
207
|
+
update agent_board.board_memberships bm
|
|
208
|
+
set role = p_grantee_role,
|
|
209
|
+
updated_at = now()
|
|
210
|
+
where bm.board_id = p_board_id
|
|
211
|
+
and bm.agent_id = p_agent_id
|
|
212
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
213
|
+
returning * into v_updated;
|
|
214
|
+
|
|
215
|
+
-- CR finding: the state predicate above closes the unauthorized WRITE, but on its own it
|
|
216
|
+
-- CREATES a false-success path. If an eviction commits between the pre-check and this
|
|
217
|
+
-- UPDATE (the TOCTOU window the mirrored predicate exists for), the UPDATE matches zero
|
|
218
|
+
-- rows, v_updated stays NULL, and the RETURN QUERY below would emit ONE ALL-NULL ROW with
|
|
219
|
+
-- no error -- a caller that checks "did I get a row back?" reads that as a completed grant.
|
|
220
|
+
-- Fail closed with the same P0002 the pre-check raises, so the serial and racing cases are
|
|
221
|
+
-- indistinguishable to the caller.
|
|
222
|
+
if not found then
|
|
223
|
+
raise exception 'grantee has no membership on this board' using errcode = 'P0002';
|
|
224
|
+
end if;
|
|
225
|
+
|
|
226
|
+
return query
|
|
227
|
+
select v_updated.board_id, v_updated.agent_id, v_updated.role, v_updated.updated_at;
|
|
228
|
+
end;
|
|
229
|
+
$$;
|
|
230
|
+
|
|
231
|
+
comment on function agent_board.grant_board_role(uuid, uuid, text, uuid) is
|
|
232
|
+
'Live board-role grant chokepoint. Grantor is the resolved actor (_resolved_actor_agent_id: '
|
|
233
|
+
'service_role uses the supplied arg, authenticated uses current_agent_id() and fails closed on '
|
|
234
|
+
'null/mismatch). Requires an ACTIVE grantee membership (joined|subscribed; P0002 otherwise) — '
|
|
235
|
+
'predicate on both the pre-check and the UPDATE (TOCTOU). Then enforces '
|
|
236
|
+
'_grant_within_ceiling(grantor, board, grantee_role) before setting board_memberships.role. '
|
|
237
|
+
'ROLERESET (#79): non-active rows cannot be re-armed with an elevated role.';
|