@consiliency/agent-board-schema 1.24.0 → 1.27.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 (33) hide show
  1. package/fixtures/already-applied.json +12 -0
  2. package/migrations/20260906000100_agent_board_enroll_resumption.sql +749 -0
  3. package/migrations/20260907000100_agent_board_signed_authority_provenance.sql +243 -0
  4. package/migrations/20260908000100_agent_board_signer_lifecycle_revalidation.sql +451 -0
  5. package/package.json +1 -1
  6. package/schema-manifest.json +30 -3
  7. package/src/tooling.test.mjs +3 -3
  8. package/tests/agent_board_atomic_fleet_onboarding.test.sql +129 -112
  9. package/tests/agent_board_claimkey.test.sql +25 -3
  10. package/tests/agent_board_consent_emission.test.sql +17 -13
  11. package/tests/agent_board_consent_gating.test.sql +55 -11
  12. package/tests/agent_board_delegation_policies.test.sql +3 -1
  13. package/tests/agent_board_enroll_resumption.test.sql +412 -0
  14. package/tests/agent_board_federated_lock_order_class.test.sql +1 -3
  15. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -6
  16. package/tests/agent_board_fleet_machine_reactivation.test.sql +56 -5
  17. package/tests/agent_board_mint_harden.test.sql +39 -4
  18. package/tests/agent_board_outbox_wake_ordering.test.sql +27 -12
  19. package/tests/agent_board_rejectsem.test.sql +1 -1
  20. package/tests/agent_board_rolereset.test.sql +23 -2
  21. package/tests/agent_board_rowid.test.sql +24 -13
  22. package/tests/agent_board_scoped_send_regrant.test.sql +8 -4
  23. package/tests/agent_board_signed_authority_provenance.test.sql +477 -0
  24. package/tests/agent_board_signer_lifecycle.test.sql +709 -0
  25. package/tests/agent_board_source_strip_helper.test.sql +285 -0
  26. package/tests/agent_board_wake_recovery.test.sql +58 -40
  27. package/tests/catalog/p_metadata_parameters.json +693 -0
  28. package/tests/fixtures/test-prereqs.sql +313 -0
  29. package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +3 -3
  30. package/tests/isolation/expected/signed_grant_onboarding_deadlock.out +220 -0
  31. package/tests/isolation/expected/signer_lifecycle_revoke.out +307 -0
  32. package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +159 -0
  33. package/tests/isolation/specs/signer_lifecycle_revoke.spec +176 -0
@@ -0,0 +1,243 @@
1
+ -- #247 — signed provenance for the authority-granting writes.
2
+ --
3
+ -- WHAT THIS CLOSES. `grant_board_role` and `set_board_authority_ceiling` are the two
4
+ -- authority-granting writes in the bundle, and neither could carry signing provenance in
5
+ -- any form. The client's `attachSigningMetadata` merges its evidence into `p_metadata`,
6
+ -- and neither function declares that parameter (verified against their NEWEST definitions,
7
+ -- 20260802000100 and 20260901000110 — not their first). Adding them to the client's signed
8
+ -- set would have attached an undeclared argument: a PostgREST signature mismatch, not a
9
+ -- silent no-op. So closing this is a schema change, not a client-list change.
10
+ --
11
+ -- MECHANISM (ratified 2026-08-12). These two get dedicated `signed_*` wrappers that call
12
+ -- `_record_signed_write_evidence` and write a real, queryable `signature_evidence` row —
13
+ -- the same mechanism as the five existing message/task signed writes, whose shape they
14
+ -- follow rather than inventing a new one. The alternative (`p_metadata.signed_write` on
15
+ -- the written row) records only that a client CLAIMED to sign; for "who granted this role,
16
+ -- and can we prove it", only an evidence row answers the question.
17
+ --
18
+ -- PROVENANCE IS DERIVED, NEVER ASSERTED. `_signed_write_actor_id` takes the actor from the
19
+ -- gateway's verified decision and raises `actor_mismatch` (42501) when a payload names a
20
+ -- different principal. That resolved actor — not `p_payload ->> 'p_grantor_agent_id'` — is
21
+ -- what the wrapper passes to the inner function AND what the evidence row names, because
22
+ -- the newest `_record_signed_write_evidence` (20260710000000) reads its actor solely from
23
+ -- `p_gateway_decision ->> 'agent_id'`. An evidence row recording the caller-supplied
24
+ -- grantor would be worse than no evidence row, because it looks like proof.
25
+ --
26
+ -- WHY set_board_authority_ceiling GAINS A PARAMETER. Its owner gate reads
27
+ -- `current_agent_id()` and nothing else, so it can only ever succeed for an authenticated
28
+ -- session. The `signed_*` layer is service_role-only BY DESIGN: 20260812000100 (#118)
29
+ -- revoked `_record_signed_write_evidence` from `authenticated` precisely because it mints
30
+ -- attestation rows from caller-supplied fields, so granting a `signed_*` wrapper to
31
+ -- `authenticated` would reopen that forge hole. A service_role-only wrapper therefore
32
+ -- cannot satisfy a `current_agent_id()`-only gate, and the bundle's existing delegation
33
+ -- mechanism is `_resolved_actor_agent_id` — exactly the shape `grant_board_role` already
34
+ -- has. Hence the new `p_actor_agent_id`.
35
+ --
36
+ -- D4 — THIS CHANGES NO UNSIGNED CALL. `p_actor_agent_id` defaults to null, and when it is
37
+ -- null the setter takes the pre-#247 path verbatim (`v_actor := current_agent_id()`), for
38
+ -- EVERY role. The resolver engages only when a caller supplies the argument. Under
39
+ -- `authenticated` the resolver additionally requires the supplied id to equal
40
+ -- `current_agent_id()` (42501 otherwise), so the new parameter cannot be used to act as
41
+ -- someone else. `create or replace` cannot add a parameter — it would leave a 2-arg and a
42
+ -- 3-arg overload live simultaneously and make a named-argument PostgREST call ambiguous —
43
+ -- so the setter is DROPped and recreated, with its `revoke`, its `grant` and its `comment`
44
+ -- restated in this same migration (a DROP discards the ACL and the COMMENT, and a
45
+ -- recreated SECURITY DEFINER function grants EXECUTE to PUBLIC by default).
46
+ --
47
+ -- EVIDENCE SHAPE. An authority write has no canonical source row to point at: the
48
+ -- `signature_evidence` source columns are message / task_event / artifact /
49
+ -- delivery_recipient. `source_kind = 'external'` is the constraint's existing shape for
50
+ -- "no internal source row, identified by a ref", and 20260718000300 already records
51
+ -- signed-write preflight evidence that way. The ref is synthesized SERVER-side from the
52
+ -- resolved actor and the board and FORCED over any caller-supplied value — a caller ref is
53
+ -- an assertion, a derived one is evidence. No table DDL is required.
54
+
55
+ -- 1. set_board_authority_ceiling — delegable actor, so a service_role gateway can carry a
56
+ -- signed grant on an owner's behalf. Everything else is verbatim from 20260901000110.
57
+ drop function if exists agent_board.set_board_authority_ceiling(uuid, text);
58
+
59
+ create or replace function agent_board.set_board_authority_ceiling(
60
+ p_board_id uuid,
61
+ p_max_grantable_role text,
62
+ p_actor_agent_id uuid default null
63
+ )
64
+ returns table(
65
+ ceiling_board_id uuid,
66
+ ceiling_max_grantable_role text
67
+ )
68
+ language plpgsql
69
+ security definer
70
+ set search_path = agent_board, public
71
+ as $$
72
+ declare
73
+ v_actor uuid;
74
+ begin
75
+ if p_max_grantable_role not in ('owner', 'admin', 'member', 'observer') then
76
+ raise exception 'max_grantable_role must be one of owner/admin/member/observer' using errcode = '22023';
77
+ end if;
78
+
79
+ -- Same board-keyed advisory lock every ceiling READER for authorization takes.
80
+ -- Held to commit, it makes the insert of a first ceiling row serialize against a
81
+ -- concurrent authorization that would otherwise observe the pre-insert ABSENCE and
82
+ -- pass as default-owner. Taken before the owner gate so the gate and the write are
83
+ -- one critical section.
84
+ perform agent_board._lock_board_authority_ceiling(p_board_id);
85
+
86
+ -- #247. Actor resolution, NOT a widening of the gate. With p_actor_agent_id omitted this
87
+ -- is the pre-#247 expression for every role, so no existing caller changes behaviour
88
+ -- (D4). When it IS supplied, _resolved_actor_agent_id binds it: service_role takes the
89
+ -- argument (this is the signed_* gateway path, where the argument is the gateway-VERIFIED
90
+ -- signer, never a raw caller claim), while authenticated still resolves to
91
+ -- current_agent_id() and raises 42501 if the supplied id differs. The owner gate below is
92
+ -- untouched and remains the only thing that authorizes the write.
93
+ if p_actor_agent_id is null then
94
+ v_actor := agent_board.current_agent_id();
95
+ else
96
+ v_actor := agent_board._resolved_actor_agent_id(p_actor_agent_id, 'p_actor_agent_id');
97
+ end if;
98
+
99
+ if v_actor is null
100
+ or not exists (
101
+ select 1
102
+ from agent_board.board_memberships bm
103
+ where bm.board_id = p_board_id
104
+ and bm.agent_id = v_actor
105
+ and bm.role = 'owner'
106
+ and bm.membership_state in ('joined', 'subscribed')
107
+ ) then
108
+ raise exception 'only a board owner may set the board authority ceiling' using errcode = '42501';
109
+ end if;
110
+
111
+ insert into agent_board.board_authority_ceilings(board_id, max_grantable_role)
112
+ values (p_board_id, p_max_grantable_role)
113
+ on conflict (board_id) do update
114
+ set max_grantable_role = excluded.max_grantable_role,
115
+ updated_at = now();
116
+
117
+ return query
118
+ select p_board_id, p_max_grantable_role;
119
+ end;
120
+ $$;
121
+
122
+ comment on function agent_board.set_board_authority_ceiling(uuid, text, uuid) is
123
+ 'Owner-gated setter for a board''s authority ceiling: takes the board-keyed _lock_board_authority_ceiling advisory lock, then upserts board_authority_ceilings only when the resolved actor is an owner of the board (else raises 42501). The actor is current_agent_id() when p_actor_agent_id is omitted — the pre-#247 behaviour for every role — and _resolved_actor_agent_id(p_actor_agent_id) when it is supplied, which lets the service_role-only signed_set_board_authority_ceiling gateway act for the gateway-verified signer while still forcing an authenticated caller to match current_agent_id(). Leaves _grant_within_ceiling semantics unchanged.';
124
+
125
+ revoke all on function agent_board.set_board_authority_ceiling(uuid, text, uuid) from public, anon;
126
+ grant execute on function agent_board.set_board_authority_ceiling(uuid, text, uuid) to authenticated, service_role;
127
+
128
+ -- 2. signed_grant_board_role — evidence-recording wrapper over the live grant chokepoint.
129
+ create or replace function agent_board.signed_grant_board_role(
130
+ p_payload jsonb,
131
+ p_gateway_decision jsonb
132
+ )
133
+ returns table(
134
+ board_id uuid,
135
+ agent_id uuid,
136
+ role text,
137
+ signature_evidence_id uuid,
138
+ verification_status text,
139
+ updated_at timestamptz
140
+ )
141
+ language plpgsql
142
+ security definer
143
+ set search_path = agent_board, public
144
+ as $$
145
+ declare
146
+ v_row record;
147
+ v_actor uuid;
148
+ v_evidence_id uuid;
149
+ v_board uuid := (p_payload ->> 'p_board_id')::uuid;
150
+ v_grantee uuid := (p_payload ->> 'p_agent_id')::uuid;
151
+ begin
152
+ -- The grantor is the gateway-verified signer. _signed_write_actor_id raises 42501 when
153
+ -- the gateway names no actor, and raises actor_mismatch when the payload names a
154
+ -- DIFFERENT one -- so a payload p_grantor_agent_id can never become the grantor. This
155
+ -- value is what grant_board_role binds (through its own _resolved_actor_agent_id) and
156
+ -- what the evidence row names; they cannot diverge.
157
+ v_actor := agent_board._signed_write_actor_id(p_payload, p_gateway_decision, 'p_grantor_agent_id');
158
+
159
+ select *
160
+ into v_row
161
+ from agent_board.grant_board_role(
162
+ v_board,
163
+ v_grantee,
164
+ p_payload ->> 'p_grantee_role',
165
+ v_actor
166
+ );
167
+
168
+ v_evidence_id := agent_board._record_signed_write_evidence(
169
+ 'external',
170
+ null,
171
+ 'grant_board_role',
172
+ p_payload,
173
+ p_gateway_decision || jsonb_build_object(
174
+ 'external_evidence_ref',
175
+ 'board-authority:grant_board_role:' || v_board::text || ':' || v_grantee::text
176
+ )
177
+ );
178
+
179
+ return query
180
+ select v_row.board_id, v_row.agent_id, v_row.role, v_evidence_id,
181
+ coalesce(p_gateway_decision ->> 'verification_status', 'unsigned'), v_row.updated_at;
182
+ end;
183
+ $$;
184
+
185
+ comment on function agent_board.signed_grant_board_role(jsonb, jsonb) is
186
+ '#247 signed-write gateway wrapper for grant_board_role. Resolves the grantor with _signed_write_actor_id (gateway actor required; a payload p_grantor_agent_id that differs raises actor_mismatch 42501), passes THAT principal to grant_board_role so the bound grantor and the recorded one cannot diverge, and records a signature_evidence row via _record_signed_write_evidence with source_kind=external and a server-derived external_evidence_ref naming the board and grantee. Adds no authority: grant_board_role''s membership, ceiling and TOCTOU gates are untouched. service_role only, like every other signed_* wrapper (#118).';
187
+
188
+ -- 3. signed_set_board_authority_ceiling — evidence-recording wrapper over the ceiling setter.
189
+ create or replace function agent_board.signed_set_board_authority_ceiling(
190
+ p_payload jsonb,
191
+ p_gateway_decision jsonb
192
+ )
193
+ returns table(
194
+ ceiling_board_id uuid,
195
+ ceiling_max_grantable_role text,
196
+ signature_evidence_id uuid,
197
+ verification_status text
198
+ )
199
+ language plpgsql
200
+ security definer
201
+ set search_path = agent_board, public
202
+ as $$
203
+ declare
204
+ v_row record;
205
+ v_actor uuid;
206
+ v_evidence_id uuid;
207
+ v_board uuid := (p_payload ->> 'p_board_id')::uuid;
208
+ begin
209
+ v_actor := agent_board._signed_write_actor_id(p_payload, p_gateway_decision, 'p_actor_agent_id');
210
+
211
+ select *
212
+ into v_row
213
+ from agent_board.set_board_authority_ceiling(
214
+ v_board,
215
+ p_payload ->> 'p_max_grantable_role',
216
+ v_actor
217
+ );
218
+
219
+ v_evidence_id := agent_board._record_signed_write_evidence(
220
+ 'external',
221
+ null,
222
+ 'set_board_authority_ceiling',
223
+ p_payload,
224
+ p_gateway_decision || jsonb_build_object(
225
+ 'external_evidence_ref',
226
+ 'board-authority:set_board_authority_ceiling:' || v_board::text || ':' || v_actor::text
227
+ )
228
+ );
229
+
230
+ return query
231
+ select v_row.ceiling_board_id, v_row.ceiling_max_grantable_role, v_evidence_id,
232
+ coalesce(p_gateway_decision ->> 'verification_status', 'unsigned');
233
+ end;
234
+ $$;
235
+
236
+ comment on function agent_board.signed_set_board_authority_ceiling(jsonb, jsonb) is
237
+ '#247 signed-write gateway wrapper for set_board_authority_ceiling. Resolves the actor with _signed_write_actor_id (gateway actor required; a differing payload p_actor_agent_id raises actor_mismatch 42501) and passes it as p_actor_agent_id, so the owner gate authorizes the SAME principal the evidence row names. Records a signature_evidence row via _record_signed_write_evidence with source_kind=external and a server-derived external_evidence_ref. Adds no authority: the owner gate still rejects any actor that is not an active board owner. service_role only, like every other signed_* wrapper (#118).';
238
+
239
+ revoke all on function agent_board.signed_grant_board_role(jsonb, jsonb) from public, anon, authenticated;
240
+ revoke all on function agent_board.signed_set_board_authority_ceiling(jsonb, jsonb) from public, anon, authenticated;
241
+
242
+ grant execute on function agent_board.signed_grant_board_role(jsonb, jsonb) to service_role;
243
+ grant execute on function agent_board.signed_set_board_authority_ceiling(jsonb, jsonb) to service_role;