@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.
- package/fixtures/already-applied.json +12 -0
- package/migrations/20260906000100_agent_board_enroll_resumption.sql +749 -0
- package/migrations/20260907000100_agent_board_signed_authority_provenance.sql +243 -0
- package/migrations/20260908000100_agent_board_signer_lifecycle_revalidation.sql +451 -0
- package/package.json +1 -1
- package/schema-manifest.json +30 -3
- package/src/tooling.test.mjs +3 -3
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +129 -112
- package/tests/agent_board_claimkey.test.sql +25 -3
- package/tests/agent_board_consent_emission.test.sql +17 -13
- package/tests/agent_board_consent_gating.test.sql +55 -11
- package/tests/agent_board_delegation_policies.test.sql +3 -1
- package/tests/agent_board_enroll_resumption.test.sql +412 -0
- package/tests/agent_board_federated_lock_order_class.test.sql +1 -3
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -6
- package/tests/agent_board_fleet_machine_reactivation.test.sql +56 -5
- package/tests/agent_board_mint_harden.test.sql +39 -4
- package/tests/agent_board_outbox_wake_ordering.test.sql +27 -12
- package/tests/agent_board_rejectsem.test.sql +1 -1
- package/tests/agent_board_rolereset.test.sql +23 -2
- package/tests/agent_board_rowid.test.sql +24 -13
- package/tests/agent_board_scoped_send_regrant.test.sql +8 -4
- package/tests/agent_board_signed_authority_provenance.test.sql +477 -0
- package/tests/agent_board_signer_lifecycle.test.sql +709 -0
- package/tests/agent_board_source_strip_helper.test.sql +285 -0
- package/tests/agent_board_wake_recovery.test.sql +58 -40
- package/tests/catalog/p_metadata_parameters.json +693 -0
- package/tests/fixtures/test-prereqs.sql +313 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +3 -3
- package/tests/isolation/expected/signed_grant_onboarding_deadlock.out +220 -0
- package/tests/isolation/expected/signer_lifecycle_revoke.out +307 -0
- package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +159 -0
- 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;
|