@consiliency/agent-board-schema 1.59.0 → 1.63.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 (73) hide show
  1. package/README.md +80 -0
  2. package/fixtures/already-applied.json +16 -0
  3. package/migrations/20261010000100_agent_board_own_post_not_delivered.sql +637 -0
  4. package/migrations/20261011000100_agent_board_session_identities.sql +1964 -0
  5. package/migrations/20261012000100_agent_board_sidowner.sql +209 -0
  6. package/migrations/20261014000100_agent_board_grant_target_role.sql +218 -0
  7. package/package.json +1 -1
  8. package/schema-manifest.json +39 -3
  9. package/src/tooling.test.mjs +9 -2
  10. package/tests/agent_board_addressing.test.sql +3 -1
  11. package/tests/agent_board_atomic_fleet_onboarding.test.sql +13 -4
  12. package/tests/agent_board_consent_emission.test.sql +30 -11
  13. package/tests/agent_board_consent_gating.test.sql +16 -2
  14. package/tests/agent_board_delegate_role_class.test.sql +1 -1
  15. package/tests/agent_board_delegation_cap_ceiling.test.sql +26 -15
  16. package/tests/agent_board_delegation_enforcement.test.sql +1 -1
  17. package/tests/agent_board_enroll_resumption.test.sql +22 -4
  18. package/tests/agent_board_enroll_token_single_use.test.sql +18 -3
  19. package/tests/agent_board_federated_lock_order_class.test.sql +2 -2
  20. package/tests/agent_board_fk_lock_edges.test.sql +74 -14
  21. package/tests/agent_board_grant_target_role.test.sql +1118 -0
  22. package/tests/agent_board_inbox_rpcs.test.sql +5 -2
  23. package/tests/agent_board_invite_no_downgrade.test.sql +10 -8
  24. package/tests/agent_board_ledger_authority.test.sql +70 -12
  25. package/tests/agent_board_membership_current_row_class.test.sql +9 -1
  26. package/tests/agent_board_mint_fleet_agent_session.test.sql +2 -2
  27. package/tests/agent_board_mint_harden.test.sql +38 -27
  28. package/tests/agent_board_own_post_not_delivered.test.sql +490 -0
  29. package/tests/agent_board_rolereset.test.sql +8 -2
  30. package/tests/agent_board_roomdeleg_lane_b.test.sql +6 -6
  31. package/tests/agent_board_rowid.test.sql +3 -1
  32. package/tests/agent_board_session_identity_approval_revocation.test.sql +334 -0
  33. package/tests/agent_board_session_identity_backfill.test.sql +82 -0
  34. package/tests/agent_board_session_identity_backfill_refusal.test.sql +14 -0
  35. package/tests/agent_board_session_identity_behavior_classes.test.sql +176 -0
  36. package/tests/agent_board_session_identity_class.test.sql +118 -0
  37. package/tests/agent_board_session_identity_fingerprint.test.sql +271 -0
  38. package/tests/agent_board_session_identity_labels_limits.test.sql +205 -0
  39. package/tests/agent_board_session_identity_lease.test.sql +235 -0
  40. package/tests/agent_board_session_identity_lifetime.test.sql +230 -0
  41. package/tests/agent_board_session_identity_limits.test.sql +208 -0
  42. package/tests/agent_board_session_identity_lineage.test.sql +64 -0
  43. package/tests/agent_board_session_identity_link.test.sql +201 -0
  44. package/tests/agent_board_session_identity_mint.test.sql +233 -0
  45. package/tests/agent_board_session_identity_panel_r1.test.sql +194 -0
  46. package/tests/agent_board_session_identity_panel_r2.test.sql +174 -0
  47. package/tests/agent_board_session_identity_protected_actions.test.sql +156 -0
  48. package/tests/agent_board_session_identity_revoke_closure.test.sql +131 -0
  49. package/tests/agent_board_sidowner_leases.test.sql +107 -0
  50. package/tests/agent_board_sidowner_owner_required.test.sql +97 -0
  51. package/tests/agent_board_sidowner_read_class.test.sql +171 -0
  52. package/tests/agent_board_sidowner_reads.test.sql +136 -0
  53. package/tests/agent_board_signer_lifecycle.test.sql +26 -4
  54. package/tests/agent_board_signing_challenge_pending_cap.test.sql +1 -1
  55. package/tests/agent_board_v2_invariants.test.sql +4 -4
  56. package/tests/agent_board_write_enforcement_class.test.sql +74 -15
  57. package/tests/catalog/function_execute_privileges.json +218 -2
  58. package/tests/catalog/p_metadata_parameters.json +98 -2
  59. package/tests/isolation/expected/grant_target_role_race.out +348 -0
  60. package/tests/isolation/expected/session_identity_limits.out +165 -0
  61. package/tests/isolation/expected/session_identity_links.out +235 -0
  62. package/tests/isolation/expected/session_identity_reclaim.out +253 -0
  63. package/tests/isolation/expected/session_identity_renew_revoke.out +115 -0
  64. package/tests/isolation/expected/session_identity_renew_revoke_key_gate.out +138 -0
  65. package/tests/isolation/expected/session_identity_tombstone_onboarding.out +247 -0
  66. package/tests/isolation/specs/grant_target_role_race.spec +207 -0
  67. package/tests/isolation/specs/session_identity_limits.spec +81 -0
  68. package/tests/isolation/specs/session_identity_links.spec +80 -0
  69. package/tests/isolation/specs/session_identity_reclaim.spec +125 -0
  70. package/tests/isolation/specs/session_identity_renew_revoke.spec +84 -0
  71. package/tests/isolation/specs/session_identity_renew_revoke_key_gate.spec +94 -0
  72. package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +108 -0
  73. package/tests/seeds/20261011000100_agent_board_session_identities.sql.pre.sql +47 -0
@@ -0,0 +1,209 @@
1
+ -- SIDOWNER: safe, scoped session-identity inspection. These reads deliberately
2
+ -- expose only the normalized state an owner/ancestor needs to diagnose and approve.
3
+
4
+ create function agent_board._session_identity_read_allowed(p_identity_agent_id uuid)
5
+ returns boolean
6
+ language sql
7
+ stable
8
+ security definer
9
+ set search_path = ''
10
+ as $$
11
+ with recursive lineage(agent_id, parent_agent_id) as (
12
+ select si.agent_id, si.parent_agent_id
13
+ from agent_board.session_identities si
14
+ where si.agent_id = p_identity_agent_id
15
+ union all
16
+ select parent.agent_id, parent.parent_agent_id
17
+ from agent_board.session_identities parent
18
+ join lineage child on child.parent_agent_id = parent.agent_id
19
+ )
20
+ select agent_board.current_agent_id() is not null
21
+ and (agent_board._session_identity_owner(p_identity_agent_id)
22
+ or exists (
23
+ select 1 from lineage where agent_id = agent_board.current_agent_id()
24
+ ));
25
+ $$;
26
+ revoke all on function agent_board._session_identity_read_allowed(uuid) from public, anon, authenticated;
27
+ grant execute on function agent_board._session_identity_read_allowed(uuid) to service_role;
28
+ comment on function agent_board._session_identity_read_allowed(uuid) is 'Internal SIDOWNER read predicate. A verified active human root owner, the selected identity, or its strict lineage ancestor may inspect its normalized identity state. It never treats a sibling or another root as an ancestor.';
29
+
30
+ create function agent_board.get_session_identity_state(p_identity_agent_id uuid)
31
+ returns table(
32
+ agent_id uuid,
33
+ parent_agent_id uuid,
34
+ root_agent_id uuid,
35
+ handle text,
36
+ depth smallint,
37
+ harness text,
38
+ lease_state text,
39
+ resolution_state text,
40
+ fingerprint_resolution text,
41
+ current_risky_digest text,
42
+ approved_risky_digest text,
43
+ recorded_fingerprint jsonb,
44
+ tombstoned_at timestamptz,
45
+ tombstone_reason text
46
+ )
47
+ language plpgsql
48
+ stable
49
+ security definer
50
+ set search_path = ''
51
+ as $$
52
+ begin
53
+ if not agent_board._session_identity_read_allowed(p_identity_agent_id) then
54
+ raise exception 'identity_read_forbidden' using errcode = '42501';
55
+ end if;
56
+
57
+ return query
58
+ select si.agent_id,
59
+ si.parent_agent_id,
60
+ si.root_agent_id,
61
+ a.handle,
62
+ si.depth,
63
+ si.harness,
64
+ case
65
+ when si.identity_kind <> 'session' then 'not_applicable'
66
+ when si.holder_secret_hash is null then 'unclaimed'
67
+ when agent_board._session_identity_lease_live(si.agent_id) then 'live'
68
+ else 'lapsed'
69
+ end,
70
+ si.resolution_state,
71
+ coalesce(si.recorded_fingerprint ->> 'resolution', 'unresolved'),
72
+ si.current_risky_digest,
73
+ si.approved_risky_digest,
74
+ si.recorded_fingerprint,
75
+ si.tombstoned_at,
76
+ si.tombstone_reason
77
+ from agent_board.session_identities si
78
+ join agent_board.agents a on a.id = si.agent_id
79
+ where si.agent_id = p_identity_agent_id;
80
+ end;
81
+ $$;
82
+ revoke all on function agent_board.get_session_identity_state(uuid) from public, anon;
83
+ grant execute on function agent_board.get_session_identity_state(uuid) to authenticated, service_role;
84
+ comment on function agent_board.get_session_identity_state(uuid) is 'SIDOWNER normalized safe identity-state read. The active human root owner, selected identity, or a strict lineage ancestor may inspect one identity, including tombstones. Missing and inaccessible identities both refuse identity_read_forbidden; no credential epochs, aliases, holder hashes, keys, or credentials are projected.';
85
+
86
+ create function agent_board.list_session_children(p_parent_agent_id uuid)
87
+ returns table(
88
+ agent_id uuid,
89
+ parent_agent_id uuid,
90
+ root_agent_id uuid,
91
+ handle text,
92
+ depth smallint,
93
+ harness text,
94
+ lease_state text,
95
+ resolution_state text,
96
+ fingerprint_resolution text,
97
+ current_risky_digest text,
98
+ approved_risky_digest text,
99
+ recorded_fingerprint jsonb,
100
+ tombstoned_at timestamptz,
101
+ tombstone_reason text
102
+ )
103
+ language plpgsql
104
+ stable
105
+ security definer
106
+ set search_path = ''
107
+ as $$
108
+ begin
109
+ if not agent_board._session_identity_read_allowed(p_parent_agent_id) then
110
+ raise exception 'identity_read_forbidden' using errcode = '42501';
111
+ end if;
112
+
113
+ return query
114
+ with recursive descendants(agent_id) as (
115
+ select child.agent_id
116
+ from agent_board.session_identities child
117
+ where child.parent_agent_id = p_parent_agent_id
118
+ union all
119
+ select child.agent_id
120
+ from agent_board.session_identities child
121
+ join descendants parent on child.parent_agent_id = parent.agent_id
122
+ )
123
+ select state.agent_id,
124
+ state.parent_agent_id,
125
+ state.root_agent_id,
126
+ state.handle,
127
+ state.depth,
128
+ state.harness,
129
+ state.lease_state,
130
+ state.resolution_state,
131
+ state.fingerprint_resolution,
132
+ state.current_risky_digest,
133
+ state.approved_risky_digest,
134
+ state.recorded_fingerprint,
135
+ state.tombstoned_at,
136
+ state.tombstone_reason
137
+ from descendants d
138
+ cross join lateral agent_board.get_session_identity_state(d.agent_id) state
139
+ order by state.depth, state.agent_id;
140
+ end;
141
+ $$;
142
+ revoke all on function agent_board.list_session_children(uuid) from public, anon;
143
+ grant execute on function agent_board.list_session_children(uuid) to authenticated, service_role;
144
+ comment on function agent_board.list_session_children(uuid) is 'SIDOWNER scoped strict-lineage descendant list. The parent is excluded; only parent_agent_id edges are traversed, never forked_from. Authorized callers receive the normalized safe projection for depth-one and depth-two descendants, including tombstones, ordered by depth then identity.';
145
+
146
+ create function agent_board._session_identity_has_human_owner(p_root_agent_id uuid)
147
+ returns boolean
148
+ language sql
149
+ stable
150
+ security definer
151
+ set search_path = ''
152
+ as $$
153
+ select exists (
154
+ select 1 from agent_board.agents root
155
+ join agent_board.agents human on human.auth_user_id = root.owner_user_id
156
+ where root.id = p_root_agent_id
157
+ and human.agent_type = 'human'
158
+ and human.status = 'active'
159
+ );
160
+ $$;
161
+ revoke all on function agent_board._session_identity_has_human_owner(uuid) from public, anon, authenticated;
162
+ grant execute on function agent_board._session_identity_has_human_owner(uuid) to service_role;
163
+ comment on function agent_board._session_identity_has_human_owner(uuid) is 'Internal N1 root-owner existence predicate, not acting-human authorization: the root owner_user_id must bind an active human agent, matching the human-existence requirement of _session_identity_owner.';
164
+
165
+ create or replace function agent_board.record_session_identity_state(p_identity_agent_id uuid, p_fingerprint jsonb)
166
+ returns void language plpgsql security definer set search_path = '' as $$
167
+ declare v_identity agent_board.session_identities%rowtype;
168
+ v_risky text := nullif(p_fingerprint ->> 'risky_digest', '');
169
+ v_recorded jsonb := coalesce(p_fingerprint -> 'recorded', '{}'::jsonb)
170
+ || jsonb_build_object('resolution', coalesce(p_fingerprint ->> 'resolution', 'unresolved'));
171
+ begin
172
+ if not agent_board._is_service_role() then
173
+ raise exception 'identity reading is service_role-only' using errcode = '42501';
174
+ end if;
175
+ if coalesce(p_fingerprint ->> 'resolution', 'unresolved') not in ('resolved', 'unresolved') then
176
+ raise exception 'invalid identity fingerprint resolution' using errcode = '22023';
177
+ end if;
178
+ perform 1 from agent_board.agents where id = (
179
+ select root_agent_id from agent_board.session_identities where agent_id = p_identity_agent_id
180
+ ) for update;
181
+ select * into v_identity from agent_board.session_identities where agent_id = p_identity_agent_id for update;
182
+ if not found then raise exception 'identity not found' using errcode = 'P0002'; end if;
183
+ if v_identity.tombstoned_at is not null then raise exception 'identity_tombstoned' using errcode = '42501'; end if;
184
+ if not exists (select 1 from agent_board.agents a where a.id = p_identity_agent_id and a.auth_user_id = auth.uid() and a.status = 'active')
185
+ or (v_identity.identity_kind = 'session' and agent_board.current_agent_id() is distinct from p_identity_agent_id) then
186
+ raise exception 'identity_lease_not_held' using errcode = '42501';
187
+ end if;
188
+ if v_identity.depth = 0 and not agent_board._session_identity_has_human_owner(v_identity.root_agent_id) then
189
+ raise exception 'identity_human_owner_required' using errcode = '42501';
190
+ end if;
191
+ if jsonb_typeof(coalesce(p_fingerprint, '{}'::jsonb)) <> 'object' or jsonb_typeof(v_recorded) <> 'object' then
192
+ raise exception 'invalid identity fingerprint reading' using errcode = '22023';
193
+ end if;
194
+ -- Return normally: throwing here would roll back the freeze. SIDSERVER must
195
+ -- commit this measured reading before invoking a protected action separately.
196
+ update agent_board.session_identities
197
+ set current_risky_digest = v_risky, recorded_fingerprint = v_recorded,
198
+ resolution_state = case
199
+ when p_fingerprint ->> 'resolution' is distinct from 'resolved' or v_risky is null
200
+ or v_identity.approved_risky_digest is null then 'frozen'
201
+ when v_risky is distinct from v_identity.approved_risky_digest or v_identity.resolution_state = 'frozen' then 'frozen'
202
+ else 'active' end,
203
+ updated_at = now()
204
+ where agent_id = p_identity_agent_id;
205
+ end;
206
+ $$;
207
+ revoke all on function agent_board.record_session_identity_state(uuid,jsonb) from public, anon, authenticated;
208
+ grant execute on function agent_board.record_session_identity_state(uuid,jsonb) to service_role;
209
+ comment on function agent_board.record_session_identity_state(uuid,jsonb) is 'Service-only fingerprint recording bound to verified subject and epoch, with root-before-identity locking. N1 refuses every depth-zero identity without a bound active human owner before any reading or freeze is stored. Other trusted session recording, durable freeze, explicit approval and lease-preservation semantics remain unchanged; no public body attestation is accepted.';
@@ -0,0 +1,218 @@
1
+ -- #674 (SECURITY): grant_board_role must check the TARGET's current role.
2
+ --
3
+ -- DEFECT. agent_board.grant_board_role authorized a grant by the NEW role only, through
4
+ -- _grant_within_ceiling(grantor, board, new_role). It never read the target's CURRENT role, so
5
+ -- any active member -- an observer included -- could demote every owner and admin of a board,
6
+ -- and an admin could then remove them (update_board_membership refuses an admin removing an
7
+ -- owner directly, but not an admin removing a member it had just demoted), leaving the board
8
+ -- with no owner. Evidence: message-board-ui contracts/evidence/membership-semantics.md, C.2-C.5,
9
+ -- C.7, E.1, E.3, E.4b, E.4c.
10
+ --
11
+ -- OWNER DECISIONS (2026-10-14, plans/plan-674-grant-target-role.md D0):
12
+ -- 1. deploy 1.63.0 and publish it to npm (with the 1.61.1/1.62.0 SID migrations);
13
+ -- 2. a rolled-back production probe is allowed;
14
+ -- 3. only owners and admins may change anyone ELSE's role.
15
+ --
16
+ -- THE RULE (D1-D9 in brief). Two checks, only when the grantor is a different AGENT ID from the
17
+ -- target ("self" is the same agent id, not the same human):
18
+ -- C-A (decision 3): grantor rank < admin -> 42501 'grant on another member requires owner or admin role'
19
+ -- C-B (#674): grantor rank <= target's CURRENT rank
20
+ -- -> 42501 'grant requires a higher board role than the target'
21
+ -- Ranks are _role_rank's (owner 4, admin 3, member 2, observer 1, null/other 0), so a missing
22
+ -- grantor row fails closed. Equal rank is refused for every rank: an owner cannot act on another
23
+ -- owner (D3; symmetric with update_board_membership), an admin cannot act on another admin (D4),
24
+ -- and a retry of a promotion to the grantor's own rank refuses. Any role may still lower ITSELF
25
+ -- (D2); the unchanged ceiling bounds a self-grant to keep-or-lower. service_role keeps its
26
+ -- existing supplied-grantor trust boundary (D8): the checks run relative to the grantor it
27
+ -- names. The signed route inherits by construction. Delegates are unchanged (no EXECUTE; a
28
+ -- delegate target is refused at step 2). boards.role_gate_enforced does not interact.
29
+ -- #675 (ownerless boards through SELF-exit) stays separate and is not made worse (D9).
30
+ --
31
+ -- ORDER (D5). Steps 1-5 unchanged (role literal, delegate target, board advisory lock, resolved
32
+ -- actor, fleet gate). Step 6 is now ONE statement: the target's active row locked
33
+ -- FOR NO KEY UPDATE OF t -- the mode the UPDATE already takes, so the lock mode and set are
34
+ -- unchanged and the lock is only taken earlier -- LEFT JOINed to the grantor's active row
35
+ -- (unlocked). Step 7 the ceiling, unchanged. Step 8 C-A then C-B. Step 9 the UPDATE, unchanged.
36
+ -- Every existing refusal keeps its code, message and precedence: P0002, then the ceiling, then
37
+ -- the new checks. Lock order stays advisory -> agents(SHARE) -> board_memberships.
38
+ --
39
+ -- SOURCE RULE (R1, AGENTS.md rule 5). The body is pg_get_functiondef of
40
+ -- agent_board.grant_board_role(uuid,uuid,text,uuid) on a fresh replay of this tree (160
41
+ -- migrations; md5(prosrc) 038bad38eae94604312e38bc74a7782c), which is byte-identical to the
42
+ -- live definition in 20261008000100_agent_board_hosted_delegates.sql:1893-1983, plus ONLY the
43
+ -- #674-marked edits. It is NOT the body at 20260909000100:480, which predates the HOSTED
44
+ -- delegate-target refusal; copying that one would silently revert it.
45
+ --
46
+ -- EC-1 (rule 1), run on that replay: pg_proc over every namespace except pg_catalog,
47
+ -- information_schema, pg_toast, pg_temp* and the test helpers, subject
48
+ -- agent_board_test.stripped_body(oid, true) (literals kept, so dynamic SQL counts), pattern
49
+ -- agent_board_test.call_re('grant_board_role'), plus every view's stripped_viewdef: exactly
50
+ -- {agent_board.signed_grant_board_role}; zero views. agent_board_grant_target_role.test.sql
51
+ -- re-asserts it.
52
+ --
53
+ -- DEFINITION-ONLY: every top-level statement is `create or replace function`, `revoke`,
54
+ -- `grant` or `comment`. No data is altered; past demotions made through the defect are not
55
+ -- undone. Rollback posture: forward_corrective_migration -- repinning to 1.62.0 does NOT revert
56
+ -- this, and restoring the old body would re-open the hole.
57
+ --
58
+ -- Same signature and return type, so CREATE OR REPLACE (not DROP). The ACL and the COMMENT are
59
+ -- restated anyway, identical in ACL to 20260909000100:591-592.
60
+
61
+ CREATE OR REPLACE FUNCTION agent_board.grant_board_role(p_board_id uuid, p_agent_id uuid, p_grantee_role text, p_grantor_agent_id uuid DEFAULT NULL::uuid)
62
+ RETURNS TABLE(board_id uuid, agent_id uuid, role text, updated_at timestamp with time zone)
63
+ LANGUAGE plpgsql
64
+ SECURITY DEFINER
65
+ SET search_path TO 'agent_board', 'public'
66
+ AS $function$
67
+ declare
68
+ v_grantor uuid;
69
+ v_updated agent_board.board_memberships%rowtype;
70
+ -- #674: the target's and the grantor's CURRENT active roles, read in one statement (step 6).
71
+ v_target_role text;
72
+ v_grantor_role text;
73
+ begin
74
+ if p_grantee_role not in ('owner', 'admin', 'member', 'observer') then
75
+ raise exception 'invalid grantee role' using errcode = '22023';
76
+ end if;
77
+
78
+ -- #276, FORCED BY AN EXISTING CLASS ASSERTION, not chosen. Adding the ledger gate makes this
79
+ -- function row-lock `agents` (transitively, through _assert_actor_not_fleet_revoked) as well
80
+ -- as board_memberships, which puts it in the class agent_board_signer_lifecycle.test.sql
81
+ -- section 9 pins from the call-graph closure: every function locking BOTH relations must take
82
+ -- the board advisory lock that serialises them against onboard_federated_fleet_agent. That
83
+ -- assertion FAILED and named this function when the gate was added without this line.
84
+ --
85
+ -- The tempting reply is that this function's order is agents -> board_memberships, the SAME
86
+ -- direction as onboarding, so it cannot close the F4 cycle. That is true today and is exactly
87
+ -- the ordering argument #269 refused: mutual exclusion survives a lock being added downstream,
88
+ -- a re-derived ordering does not. So the lock is taken rather than the assertion relaxed.
89
+ --
90
+ -- It also closes a gap 20260901000110 left open and explicitly declined to determine: this
91
+ -- function reads the ceiling for authorization (via _grant_within_ceiling below) and was one
92
+ -- of the three readers that did NOT take the lock serialising the ceiling row's ABSENCE, so a
93
+ -- concurrent first-ceiling insert could be missed and the read pass as default-owner. It is
94
+ -- now serialised, and agent_board_atomic_fleet_onboarding.test.sql's ceiling-reader registry
95
+ -- records it as such.
96
+ --
97
+ -- FIRST, before any row lock, so both routes are advisory -> agents -> board_memberships.
98
+ -- Re-taking it inside signed_grant_board_role (which already holds it) is a no-op: advisory
99
+ -- transaction locks are re-entrant within a session.
100
+ -- HOSTED (role-writer class): a hosted delegate is a member, never higher.
101
+ perform agent_board._refuse_delegate_membership_target(p_agent_id, 'grant_board_role');
102
+
103
+ perform agent_board._lock_board_authority_ceiling(p_board_id);
104
+
105
+ -- Grantor is the RESOLVED actor. service_role -> supplied arg; authenticated ->
106
+ -- current_agent_id() and raises 42501 if null (suspended/unmapped) or if the
107
+ -- supplied arg <> current_agent_id(). This closes the single-principal escalation.
108
+ v_grantor := agent_board._resolved_actor_agent_id(p_grantor_agent_id, 'p_grantor_agent_id');
109
+
110
+ -- #276. Fleet revocation removes board authority, not only send. Sited HERE — inside the
111
+ -- chokepoint, not in the signed wrapper — because signed_grant_board_role is the only other
112
+ -- referrer of this function in the whole database, so the signed route inherits this check
113
+ -- by construction and the unsigned `authenticated` route (the wider door) is closed by the
114
+ -- same line. Before the membership pre-check so the lock order is agents -> board_memberships,
115
+ -- matching onboard_federated_fleet_agent; below the write it would be the inversion #269
116
+ -- reproduced as `deadlock detected`, and this route has no advisory lock to be rescued by.
117
+ perform agent_board._assert_actor_not_fleet_revoked(v_grantor, 'grant_board_role');
118
+
119
+ -- #674 step 6. ONE statement replaces the bare EXISTS pre-check: the target's active row,
120
+ -- row-locked in NO KEY UPDATE mode, on t only (the mode the UPDATE below takes, only earlier),
121
+ -- LEFT JOINed to the grantor's active row, which is NOT locked. Its snapshot is taken after
122
+ -- the board advisory-lock wait above. After a row-lock wait, READ COMMITTED re-fetches only
123
+ -- the locked row t; the joined grantor row g keeps the statement snapshot's version. That is
124
+ -- benign (plan D5): every grant on this board and link_session_alias are excluded by the
125
+ -- advisory lock, and the one unserialized writer that can change an active grantor's role
126
+ -- (invite-link redeem's rank-max) only RAISES it, so a stale grantor rank is conservative.
127
+ -- A later change (#675's hook) must not rely on g being fresh after this statement.
128
+ -- Both reads are single-row by the partial unique index on active memberships.
129
+ select t.role, g.role
130
+ into v_target_role, v_grantor_role
131
+ from agent_board.board_memberships t
132
+ left join agent_board.board_memberships g
133
+ on g.board_id = t.board_id
134
+ and g.agent_id = v_grantor
135
+ and g.membership_state in ('joined', 'subscribed')
136
+ where t.board_id = p_board_id
137
+ and t.agent_id = p_agent_id
138
+ and t.membership_state in ('joined', 'subscribed')
139
+ for no key update of t;
140
+ if not found then
141
+ raise exception 'grantee has no membership on this board' using errcode = 'P0002';
142
+ end if;
143
+
144
+ -- Ceiling consumption: grantee-bounded against min(grantor role, board ceiling).
145
+ if not agent_board._grant_within_ceiling(v_grantor, p_board_id, p_grantee_role) then
146
+ raise exception 'grant exceeds grantor role or board authority ceiling' using errcode = '42501';
147
+ end if;
148
+
149
+ -- #674 step 8. Two checks for a grantor that is NOT the target (self = the same agent id,
150
+ -- not the same human). C-A (owner decision 3): only an owner or admin may change anyone
151
+ -- else's role. C-B (#674): the grantor must STRICTLY outrank the target's current role, so
152
+ -- equal rank is refused for every rank (owner -> owner, admin -> admin). Both read the roles
153
+ -- step 6 observed. A null role ranks 0, so a missing grantor row fails closed. C-A runs
154
+ -- before C-B; the ceiling above runs before both. Self-grants are exempt from both, and the
155
+ -- ceiling still bounds a self-grant to keep-or-lower.
156
+ if v_grantor is distinct from p_agent_id then
157
+ if agent_board._role_rank(v_grantor_role) < agent_board._role_rank('admin') then
158
+ raise exception 'grant on another member requires owner or admin role' using errcode = '42501';
159
+ end if;
160
+ if agent_board._role_rank(v_grantor_role) <= agent_board._role_rank(v_target_role) then
161
+ raise exception 'grant requires a higher board role than the target' using errcode = '42501';
162
+ end if;
163
+ end if;
164
+
165
+ update agent_board.board_memberships bm
166
+ set role = p_grantee_role,
167
+ updated_at = now()
168
+ where bm.board_id = p_board_id
169
+ and bm.agent_id = p_agent_id
170
+ and bm.membership_state in ('joined', 'subscribed')
171
+ returning * into v_updated;
172
+
173
+ -- CR finding: the state predicate above closes the unauthorized WRITE, but on its own it
174
+ -- CREATES a false-success path. If an eviction commits between the pre-check and this
175
+ -- UPDATE (the TOCTOU window the mirrored predicate exists for), the UPDATE matches zero
176
+ -- rows, v_updated stays NULL, and the RETURN QUERY below would emit ONE ALL-NULL ROW with
177
+ -- no error -- a caller that checks "did I get a row back?" reads that as a completed grant.
178
+ -- Fail closed with the same P0002 the pre-check raises, so the serial and racing cases are
179
+ -- indistinguishable to the caller.
180
+ if not found then
181
+ raise exception 'grantee has no membership on this board' using errcode = 'P0002';
182
+ end if;
183
+
184
+ return query
185
+ select v_updated.board_id, v_updated.agent_id, v_updated.role, v_updated.updated_at;
186
+ end;
187
+ $function$;
188
+
189
+ revoke all on function agent_board.grant_board_role(uuid, uuid, text, uuid) from public, anon;
190
+ grant execute on function agent_board.grant_board_role(uuid, uuid, text, uuid) to authenticated, service_role;
191
+
192
+ comment on function agent_board.grant_board_role(uuid, uuid, text, uuid) is
193
+ 'Live board-role grant chokepoint. Grantor is the resolved actor (_resolved_actor_agent_id: '
194
+ 'service_role uses the supplied arg, authenticated uses current_agent_id() and fails closed on '
195
+ 'null/mismatch). #276: the resolved grantor is then checked against the fleet revocation ledger '
196
+ 'by _assert_actor_not_fleet_revoked -- an agent carrying an agent-subject fleet_revocation_events '
197
+ 'row cannot complete a grant (42501, ''fleet_revoked:''), which is the same answer '
198
+ 'evaluate_fleet_sender_policy already gives that principal on the send path. The check sits in '
199
+ 'THIS function rather than in signed_grant_board_role because signed_grant_board_role is the only '
200
+ 'other referrer of it in the database, so the signed route inherits the gate and the wider '
201
+ 'unsigned authenticated route is closed by the same line; it is placed before the membership '
202
+ 'pre-check so the lock order is agents(FOR SHARE) -> board_memberships, matching '
203
+ 'onboard_federated_fleet_agent. A principal with no fleet binding has no such row and is '
204
+ 'unaffected. Then requires an ACTIVE grantee membership (joined|subscribed; P0002 otherwise) -- '
205
+ 'predicate on both the pre-check and the UPDATE (TOCTOU). Then enforces '
206
+ '_grant_within_ceiling(grantor, board, grantee_role) before setting board_memberships.role. '
207
+ 'ROLERESET (#79): non-active rows cannot be re-armed with an elevated role. '
208
+ '#674: the membership pre-check is ONE statement that locks the target''s active row FOR NO KEY '
209
+ 'UPDATE OF t (the mode the UPDATE takes, only earlier; lock order unchanged: advisory -> '
210
+ 'agents(SHARE) -> board_memberships) and LEFT JOINs the grantor''s active row, unlocked. After '
211
+ 'the unchanged ceiling check, a grantor that is not the target (self = the same agent id, not '
212
+ 'the same human) must be an owner or admin (C-A, 42501 ''grant on another member requires owner '
213
+ 'or admin role'') and must strictly outrank the target''s CURRENT role (C-B, 42501 ''grant '
214
+ 'requires a higher board role than the target''), so equal rank is refused for every rank. A '
215
+ 'self-grant is exempt from both and stays bounded to keep-or-lower by the ceiling. For '
216
+ 'service_role the checks run relative to the grantor the caller SUPPLIES -- the existing '
217
+ 'operator-trust boundary, which #674 does not change (passing p_grantor_agent_id = p_agent_id '
218
+ 'takes the self-exemption).';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consiliency/agent-board-schema",
3
- "version": "1.59.0",
3
+ "version": "1.63.0",
4
4
  "description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
5
5
  "repository": "https://github.com/Consiliency/message-board.git",
6
6
  "type": "module",
@@ -2,7 +2,7 @@
2
2
  "schema": "agent_board_schema_manifest.v1",
3
3
  "interface": "IF-0-MBPLANE-BUNDLE-1",
4
4
  "package": "@consiliency/agent-board-schema",
5
- "packageVersion": "1.59.0",
5
+ "packageVersion": "1.63.0",
6
6
  "schemaVersion": 8,
7
7
  "minClientVersion": "1.1.0",
8
8
  "source": {
@@ -46,8 +46,8 @@
46
46
  "usage": "upsert (retention/monitoring defaults; on conflict do nothing)"
47
47
  }
48
48
  ],
49
- "migrationCount": 157,
50
- "aggregateSha256": "481b368359a456111c6a2f901ceba486f32085ee8d48c36aefcfb592c0f1070f",
49
+ "migrationCount": 161,
50
+ "aggregateSha256": "884150d3dfbccdbe8a0be038688832baa83a185ae219ab0afcf9d585dc42a984",
51
51
  "migrations": [
52
52
  {
53
53
  "order": 0,
@@ -1461,6 +1461,42 @@
1461
1461
  "sourceRef": "message-board",
1462
1462
  "bytes": 37430,
1463
1463
  "sha256": "4c2c4041bb41126b3e1bce940bc8a52ac78b69389a0c866556ba0c0e4adc3564"
1464
+ },
1465
+ {
1466
+ "order": 157,
1467
+ "id": "20261010000100_agent_board_own_post_not_delivered",
1468
+ "filename": "20261010000100_agent_board_own_post_not_delivered.sql",
1469
+ "origin": "message-board-forward",
1470
+ "sourceRef": "message-board",
1471
+ "bytes": 25831,
1472
+ "sha256": "6cbbe0bb9ccf7ab65402a58de145ee21bc2158ddc7502671ea13a98415ca6421"
1473
+ },
1474
+ {
1475
+ "order": 158,
1476
+ "id": "20261011000100_agent_board_session_identities",
1477
+ "filename": "20261011000100_agent_board_session_identities.sql",
1478
+ "origin": "message-board-forward",
1479
+ "sourceRef": "message-board",
1480
+ "bytes": 119197,
1481
+ "sha256": "0f429d2b7c2a361741fb00019a173c82024a59362ea2d3a70cb11ab594b1d055"
1482
+ },
1483
+ {
1484
+ "order": 159,
1485
+ "id": "20261012000100_agent_board_sidowner",
1486
+ "filename": "20261012000100_agent_board_sidowner.sql",
1487
+ "origin": "message-board-forward",
1488
+ "sourceRef": "message-board",
1489
+ "bytes": 9979,
1490
+ "sha256": "e18fb5a7ce0b00e9cccf12a60cbad49d820612c2c50f65c648d4105c80fbc921"
1491
+ },
1492
+ {
1493
+ "order": 160,
1494
+ "id": "20261014000100_agent_board_grant_target_role",
1495
+ "filename": "20261014000100_agent_board_grant_target_role.sql",
1496
+ "origin": "message-board-forward",
1497
+ "sourceRef": "message-board",
1498
+ "bytes": 14704,
1499
+ "sha256": "4a6c00546a1d2837f88eece1de082cc76eec1f85cce8cf9ac7998e42bf32a5c5"
1464
1500
  }
1465
1501
  ],
1466
1502
  "excludedBridge": {
@@ -38,9 +38,9 @@ describe('manifest integrity', () => {
38
38
  expect(manifest.schemaVersion).toBe(8);
39
39
  expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
40
40
  });
41
- it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 157)', () => {
41
+ it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS + one OWNPOST + one SIDSCHEMA + one SIDOWNER + one GRANTTARGETROLE (#674) forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 161)', () => {
42
42
  expect(manifest.migrationCount).toBe(manifest.migrations.length);
43
- expect(manifest.migrationCount).toBe(157);
43
+ expect(manifest.migrationCount).toBe(161);
44
44
  // MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
45
45
  // schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
46
46
  // enrollment-only additive surface (identity_providers registry, enrollment audit,
@@ -353,6 +353,13 @@ describe('manifest integrity', () => {
353
353
  // revocation marker revoke_human_subtree derives from it; the tombstone's and the exchange's
354
354
  // lock order against the subtree revokers (AB15).
355
355
  '20261009000100_agent_board_hosted_followups.sql',
356
+ // OWNPOST (#608): an agent's own board post is a feed-only row for it, not an inbox item;
357
+ // a board-wide task fan-out skips its creator; own_inbox callbacks skip feed-only rows.
358
+ '20261010000100_agent_board_own_post_not_delivered.sql',
359
+ '20261011000100_agent_board_session_identities.sql',
360
+ '20261012000100_agent_board_sidowner.sql',
361
+ // #674: grant_board_role checks the target's current role (C-A + C-B).
362
+ '20261014000100_agent_board_grant_target_role.sql',
356
363
  ]) {
357
364
  expect(names.has(f)).toBe(true);
358
365
  }
@@ -29,7 +29,9 @@ begin
29
29
  values
30
30
  (v_actor, 'agent', 'agent', 'addressing-actor-' || v_suffix, 'ADDRESSING Actor', 'active', '{"role":"phase_worker","groupAliases":["ops"]}'::jsonb),
31
31
  (v_worker, 'agent', 'agent', 'addressing-worker-' || v_suffix, 'ADDRESSING Worker', 'active', '{"role":"phase_worker","groupAliases":["ops"]}'::jsonb),
32
- (v_reviewer, 'agent', 'agent', 'addressing-reviewer-' || v_suffix, 'ADDRESSING Reviewer', 'active', '{"roles":["reviewer"],"groupAliases":["review"]}'::jsonb),
32
+ -- `role: phase_worker` on the reviewer too: since #608 a set descriptor never reaches its own
33
+ -- sender, so the direct-send ambiguity case below needs TWO matching agents besides the actor.
34
+ (v_reviewer, 'agent', 'agent', 'addressing-reviewer-' || v_suffix, 'ADDRESSING Reviewer', 'active', '{"role":"phase_worker","roles":["reviewer"],"groupAliases":["review"]}'::jsonb),
33
35
  (v_human, 'human', 'human', 'addressing-human-' || v_suffix, 'ADDRESSING Human', 'active', '{}'::jsonb);
34
36
 
35
37
  insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
@@ -307,11 +307,11 @@ select ok(
307
307
  'agent fleet revocation takes its machine locks before the agent lock [SOLE PIN: no isolation spec discriminates this ordering -- measured against all 20]'
308
308
  );
309
309
  select ok(
310
- strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agents a where a.id = p_agent_id for update'
310
+ strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agents a where a.id = v_id for update'
311
311
  ) > 0
312
312
  and strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agent_signing_keys'
313
313
  ) > 0
314
- and strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agents a where a.id = p_agent_id for update'
314
+ and strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agents a where a.id = v_id for update'
315
315
  ) < strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agent_signing_keys'
316
316
  ),
317
317
  'subtree primitive locks the agent before signing keys [SOLE PIN: no isolation spec discriminates this ordering -- measured against all 20]'
@@ -558,7 +558,10 @@ select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below
558
558
  -- lock also closes this reader's exposure to the ceiling row's ABSENCE, one of the three
559
559
  -- 20260901000110 left explicitly undetermined.
560
560
  'agent_board.grant_board_role(uuid,uuid,text,uuid) => serialized',
561
- 'agent_board.onboard_federated_fleet_agent(text,text,text,jsonb,text,text,text,text,boolean,text,timestamp with time zone,text,text,text[],text[],text,uuid,uuid,text,text,text,text,text,text,text) => serialized'
561
+ 'agent_board.link_session_alias(uuid,text,text) => serialized',
562
+ 'agent_board.mint_delegated_child(uuid,text,jsonb,text,interval,boolean,text,text,text,jsonb,jsonb,uuid) => serialized',
563
+ 'agent_board.onboard_federated_fleet_agent(text,text,text,jsonb,text,text,text,text,boolean,text,timestamp with time zone,text,text,text[],text[],text,uuid,uuid,text,text,text,text,text,text,text) => serialized',
564
+ 'agent_board.readmit_session_identity(uuid,text,text,jsonb,jsonb,interval) => serialized'
562
565
  ]::text[],
563
566
  'every ceiling-authorization reader in pg_proc is declared with its serialization state'
564
567
  );
@@ -718,8 +721,11 @@ select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below
718
721
  -- once the ledger gate made it lock both). Still authenticated,service_role — #276 removes
719
722
  -- authority from a revoked principal, it does not change who may call the RPC.
720
723
  'agent_board.grant_board_role(uuid,uuid,text,uuid) [definer] => authenticated,service_role',
724
+ 'agent_board.link_session_alias(uuid,text,text) [definer] => service_role',
725
+ 'agent_board.mint_delegated_child(uuid,text,jsonb,text,interval,boolean,text,text,text,jsonb,jsonb,uuid) [definer] => service_role',
721
726
  'agent_board.onboard_federated_fleet_agent(text,text,text,jsonb,text,text,text,text,boolean,text,timestamp with time zone,text,text,text[],text[],text,uuid,uuid,text,text,text,text,text,text,text) [definer] => service_role',
722
727
  'agent_board.reactivate_fleet_machine(uuid,uuid,text) [definer] => service_role',
728
+ 'agent_board.readmit_session_identity(uuid,text,text,jsonb,jsonb,interval) [definer] => authenticated',
723
729
  'agent_board.revoke_fleet_subject(text,uuid,uuid,text,jsonb) [definer] => authenticated,service_role',
724
730
  'agent_board.revoke_human_subtree(uuid,uuid,text) [definer] => service_role',
725
731
  'agent_board.set_board_authority_ceiling(uuid,text,uuid) [definer] => authenticated,service_role',
@@ -727,7 +733,10 @@ select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below
727
733
  -- FIRST, because its order was board_memberships(grantee) -> agents(signer) against
728
734
  -- onboard_federated_fleet_agent's agents -> board_memberships -- a reproduced ABBA between
729
735
  -- two honest callers. It is service_role-only like every other signed_* wrapper (#118).
730
- 'agent_board.signed_grant_board_role(jsonb,jsonb) [definer] => service_role'
736
+ 'agent_board.signed_grant_board_role(jsonb,jsonb) [definer] => service_role',
737
+ -- SIDSCHEMA integrity repair: the verified Portal withdrawal now takes board mutexes
738
+ -- before the instance/family rows. Its service-only transport is unchanged.
739
+ 'agent_board.tombstone_delegate_instance(text,text,text,text) [definer] => service_role'
731
740
  ]::text[],
732
741
  'every function on this migration set''s surface is declared with its definer flag and exact EXECUTE grantees'
733
742
  );