@consiliency/agent-board-schema 1.2.0 → 1.4.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/bin/agent-board-schema.js +0 -0
- package/fixtures/already-applied.json +108 -0
- package/migrations/20260714000200_agent_board_workload_providers_rls.sql +35 -0
- package/migrations/20260717000100_agent_board_board_role.sql +180 -0
- package/migrations/20260717000200_agent_board_thread_acl.sql +149 -0
- package/migrations/20260717000300_agent_board_agent_lineage.sql +54 -0
- package/migrations/20260717000400_agent_board_delegation_policies.sql +175 -0
- package/migrations/20260718000100_agent_board_send_authz_config.sql +172 -0
- package/migrations/20260718000200_agent_board_send_role_gate.sql +804 -0
- package/migrations/20260718000300_agent_board_fleet_policy_default_deny.sql +192 -0
- package/migrations/20260718000400_agent_board_grant_ceiling_wiring.sql +58 -0
- package/migrations/20260718000500_agent_board_fleet_agent_session_class.sql +24 -0
- package/migrations/20260718000600_agent_board_mint_fleet_agent_session.sql +172 -0
- package/migrations/20260718000700_agent_board_nonfleet_session_reaper.sql +35 -0
- package/migrations/20260718000800_agent_board_regrant_scoped_send.sql +24 -0
- package/migrations/20260718000900_agent_board_agents_status_revoked.sql +24 -0
- package/migrations/20260718001000_agent_board_delegation_enforcement.sql +296 -0
- package/migrations/20260718001100_agent_board_revoke_agent_subtree.sql +238 -0
- package/migrations/20260718001200_agent_board_resigning_refresh.sql +129 -0
- package/migrations/20260718001300_agent_board_identity_providers.sql +70 -0
- package/migrations/20260718001400_agent_board_enrollment_federation_audit.sql +50 -0
- package/migrations/20260718001500_agent_board_enroll_federated_identity.sql +220 -0
- package/migrations/20260719000100_agent_board_signing_challenge_pending_cap.sql +124 -0
- package/migrations/20260719000200_agent_board_signing_challenge_reaper.sql +34 -0
- package/migrations/20260719000300_agent_board_enroll_rate_cap.sql +72 -0
- package/migrations/20260719001000_agent_board_identity_providers_portal_contract.sql +38 -0
- package/migrations/20260719001100_agent_board_enroll_federated_identity_pop.sql +267 -0
- package/migrations/20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql +296 -0
- package/migrations/20260719001300_agent_board_enroll_rate_events_reaper_rls.sql +62 -0
- package/package.json +9 -9
- package/schema-manifest.json +247 -4
- package/src/tooling.test.mjs +42 -1
- package/tests/agent_board_agent_lineage.test.sql +73 -0
- package/tests/agent_board_agents_status_revoked.test.sql +60 -0
- package/tests/agent_board_auth_rpcs.test.sql +17 -16
- package/tests/agent_board_board_role.test.sql +129 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
- package/tests/agent_board_delegation_enforcement.test.sql +175 -0
- package/tests/agent_board_delegation_policies.test.sql +119 -0
- package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
- package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
- package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
- package/tests/agent_board_enrollment_audit_content.test.sql +134 -0
- package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
- package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
- package/tests/agent_board_fleet_registry.test.sql +2 -2
- package/tests/agent_board_identity_providers.test.sql +104 -0
- package/tests/agent_board_inbox_rpcs.test.sql +6 -6
- package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
- package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +130 -0
- package/tests/agent_board_resigning_refresh.test.sql +73 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
- package/tests/agent_board_schema_version.test.sql +2 -2
- package/tests/agent_board_schema_version_rpc.test.sql +1 -1
- package/tests/agent_board_scoped_send_regrant.test.sql +138 -0
- package/tests/agent_board_send_authz_config.test.sql +84 -0
- package/tests/agent_board_send_authz_matrix.test.sql +159 -0
- package/tests/agent_board_send_role_gate.test.sql +177 -0
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
- package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
- package/tests/agent_board_thread_acl.test.sql +129 -0
- package/tests/agent_board_workload_providers_rls.test.sql +40 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
-- MBDELEG SL-1 (IF-0-MBDELEG-2): per-child delegation grant table + bounded
|
|
2
|
+
-- root-minted spinoff RPCs.
|
|
3
|
+
--
|
|
4
|
+
-- Additive, forward-only. First consumer of delegation_policies (20260717000400).
|
|
5
|
+
--
|
|
6
|
+
-- Scope model (FROZEN): a root's scope IS delegation_policies.allowed_scopes (the
|
|
7
|
+
-- only machine-readable scope a root carries). allowed_scopes / granted_scopes are
|
|
8
|
+
-- JSON ARRAYS; "child scope ⊆ root scope" is exactly the jsonb array-containment
|
|
9
|
+
-- check `allowed_scopes @> requested_scopes`.
|
|
10
|
+
--
|
|
11
|
+
-- Two clocks: delegation_grants.grant_expires_at is the HOURS-SCALE working/grant
|
|
12
|
+
-- clock (bounded by delegation_policies.child_ttl_ceiling), distinct from the
|
|
13
|
+
-- <=900s fleet_agent_session token clock minted by mint_fleet_agent_session.
|
|
14
|
+
--
|
|
15
|
+
-- independent_auth is the default mode and opts out of ALL of the below: an
|
|
16
|
+
-- independent_auth subagent is provisioned lineage-independent (parent_agent_id and
|
|
17
|
+
-- root_agent_id NULL, sharing only owner_user_id), holds no delegation_grants row,
|
|
18
|
+
-- and enrolls its own key + PoP. Because it carries no parent, _agent_principal_active
|
|
19
|
+
-- reduces to its own status='active' on the up-walk, so it survives any other
|
|
20
|
+
-- agent's/root's revocation. Only mint_delegated_child (root_minted) rows are
|
|
21
|
+
-- created here.
|
|
22
|
+
|
|
23
|
+
-- 1. delegation_grants (per-child) -------------------------------------------
|
|
24
|
+
create table if not exists agent_board.delegation_grants (
|
|
25
|
+
id uuid primary key default gen_random_uuid(),
|
|
26
|
+
child_agent_id uuid not null unique references agent_board.agents(id) on delete cascade,
|
|
27
|
+
root_agent_id uuid not null references agent_board.agents(id) on delete cascade,
|
|
28
|
+
parent_agent_id uuid not null references agent_board.agents(id),
|
|
29
|
+
delegation_policy_id uuid not null references agent_board.delegation_policies(id),
|
|
30
|
+
granted_scopes jsonb not null,
|
|
31
|
+
-- Hours-scale working/grant clock, bounded by delegation_policies.child_ttl_ceiling.
|
|
32
|
+
grant_expires_at timestamptz not null,
|
|
33
|
+
last_renewed_at timestamptz,
|
|
34
|
+
revoked_at timestamptz,
|
|
35
|
+
created_at timestamptz not null default now()
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
comment on table agent_board.delegation_grants is
|
|
39
|
+
'Per-child root-minted delegation grant. grant_expires_at is the hours-scale working clock (<= root policy child_ttl_ceiling), distinct from the <=900s session-token clock. independent_auth children hold NO row here.';
|
|
40
|
+
|
|
41
|
+
create index if not exists idx_agent_board_delegation_grants_root
|
|
42
|
+
on agent_board.delegation_grants(root_agent_id)
|
|
43
|
+
where revoked_at is null;
|
|
44
|
+
|
|
45
|
+
alter table agent_board.delegation_grants enable row level security;
|
|
46
|
+
|
|
47
|
+
drop policy if exists agent_board_authenticated_select_delegation_grants on agent_board.delegation_grants;
|
|
48
|
+
create policy agent_board_authenticated_select_delegation_grants
|
|
49
|
+
on agent_board.delegation_grants
|
|
50
|
+
as permissive
|
|
51
|
+
for select
|
|
52
|
+
to authenticated
|
|
53
|
+
using (root_agent_id = agent_board.current_agent_id());
|
|
54
|
+
|
|
55
|
+
grant select on agent_board.delegation_grants to authenticated;
|
|
56
|
+
grant select, insert, update on agent_board.delegation_grants to service_role;
|
|
57
|
+
|
|
58
|
+
-- 2. mint_delegated_child ----------------------------------------------------
|
|
59
|
+
-- service_role-only. Mints a bounded root_minted child ONLY when: the root's
|
|
60
|
+
-- delegation_policies row exists, is unrevoked, and mode='root_minted'; the root
|
|
61
|
+
-- principal is active; requested scopes ⊆ allowed_scopes; the root is a TRUE
|
|
62
|
+
-- lineage root (parent_agent_id NULL and holds no delegation_grants row of its own
|
|
63
|
+
-- => max_depth=1, no transitive minting); and max_active_children is not exceeded.
|
|
64
|
+
-- Provisions the child (parent = root = p_root_agent_id, same owner_user_id, own
|
|
65
|
+
-- auth binding), enrolls a scoped ephemeral REVOCABLE signing key when the policy
|
|
66
|
+
-- requires it, and inserts a delegation_grants row with a ceiling-bounded
|
|
67
|
+
-- grant_expires_at.
|
|
68
|
+
create or replace function agent_board.mint_delegated_child(
|
|
69
|
+
p_root_agent_id uuid,
|
|
70
|
+
p_child_slug text,
|
|
71
|
+
p_requested_scopes jsonb,
|
|
72
|
+
p_child_public_key text default null,
|
|
73
|
+
p_ttl interval default null
|
|
74
|
+
)
|
|
75
|
+
returns table(
|
|
76
|
+
child_agent_id uuid,
|
|
77
|
+
grant_expires_at timestamptz,
|
|
78
|
+
signing_key_id uuid
|
|
79
|
+
)
|
|
80
|
+
language plpgsql
|
|
81
|
+
security definer
|
|
82
|
+
set search_path = agent_board, public
|
|
83
|
+
as $$
|
|
84
|
+
declare
|
|
85
|
+
v_policy agent_board.delegation_policies%rowtype;
|
|
86
|
+
v_root agent_board.agents%rowtype;
|
|
87
|
+
v_slug text;
|
|
88
|
+
v_requested jsonb := coalesce(p_requested_scopes, '[]'::jsonb);
|
|
89
|
+
v_allowed jsonb;
|
|
90
|
+
v_active_children int;
|
|
91
|
+
v_child_auth_user_id uuid;
|
|
92
|
+
v_child_id uuid;
|
|
93
|
+
v_key_id uuid := null;
|
|
94
|
+
v_grant_expires_at timestamptz;
|
|
95
|
+
begin
|
|
96
|
+
if not agent_board._is_service_role() then
|
|
97
|
+
raise exception 'mint_delegated_child is service_role-only' using errcode = '42501';
|
|
98
|
+
end if;
|
|
99
|
+
|
|
100
|
+
v_slug := agent_board._normalize_provision_slug(p_child_slug);
|
|
101
|
+
|
|
102
|
+
-- Root must exist.
|
|
103
|
+
select * into v_root from agent_board.agents where id = p_root_agent_id;
|
|
104
|
+
if not found then
|
|
105
|
+
raise exception 'root agent not found' using errcode = 'P0002';
|
|
106
|
+
end if;
|
|
107
|
+
|
|
108
|
+
-- Root policy must exist, be unrevoked, and be root_minted. #11: take a row lock
|
|
109
|
+
-- on the policy up front so a concurrent mint against the same root serializes
|
|
110
|
+
-- here — the max_active_children count->insert below cannot then be raced past
|
|
111
|
+
-- the ceiling (TOCTOU close).
|
|
112
|
+
select * into v_policy
|
|
113
|
+
from agent_board.delegation_policies dp
|
|
114
|
+
where dp.root_agent_id = p_root_agent_id
|
|
115
|
+
and dp.revoked_at is null
|
|
116
|
+
order by dp.created_at desc
|
|
117
|
+
limit 1
|
|
118
|
+
for update;
|
|
119
|
+
if not found then
|
|
120
|
+
raise exception 'no active delegation policy for root agent' using errcode = '42501';
|
|
121
|
+
end if;
|
|
122
|
+
if v_policy.mode <> 'root_minted' then
|
|
123
|
+
raise exception 'delegation policy mode is not root_minted (independent_auth children self-provision)' using errcode = '42501';
|
|
124
|
+
end if;
|
|
125
|
+
|
|
126
|
+
-- Root principal must be active (self + lineage).
|
|
127
|
+
if not agent_board._agent_principal_active(p_root_agent_id) then
|
|
128
|
+
raise exception 'principal_inactive: root agent principal is not fully active' using errcode = '42501';
|
|
129
|
+
end if;
|
|
130
|
+
|
|
131
|
+
-- max_depth = 1 / no transitive minting: p_root_agent_id must be a TRUE lineage
|
|
132
|
+
-- root (unparented) and must not itself be a delegated child.
|
|
133
|
+
if v_root.parent_agent_id is not null then
|
|
134
|
+
raise exception 'max_depth: only a true lineage root may mint (no transitive delegation)' using errcode = '42501';
|
|
135
|
+
end if;
|
|
136
|
+
if exists (select 1 from agent_board.delegation_grants g where g.child_agent_id = p_root_agent_id) then
|
|
137
|
+
raise exception 'max_depth: a delegated child may not mint further children' using errcode = '42501';
|
|
138
|
+
end if;
|
|
139
|
+
|
|
140
|
+
-- Scope subset: requested ⊆ allowed (JSON array containment). A NULL allowed_scopes
|
|
141
|
+
-- means the root carries no delegable scope, so only an empty request is a subset.
|
|
142
|
+
v_allowed := coalesce(v_policy.allowed_scopes, '[]'::jsonb);
|
|
143
|
+
if not (v_allowed @> v_requested) then
|
|
144
|
+
raise exception 'scope_exceeded: requested scopes are not a subset of the root allowed_scopes' using errcode = '42501';
|
|
145
|
+
end if;
|
|
146
|
+
|
|
147
|
+
-- max_active_children: when set, the root's live (non-revoked, unexpired) grant
|
|
148
|
+
-- count must remain <= max_active_children after this mint.
|
|
149
|
+
if v_policy.max_active_children is not null then
|
|
150
|
+
select count(*) into v_active_children
|
|
151
|
+
from agent_board.delegation_grants g
|
|
152
|
+
where g.root_agent_id = p_root_agent_id
|
|
153
|
+
and g.revoked_at is null
|
|
154
|
+
and g.grant_expires_at > now();
|
|
155
|
+
if v_active_children >= v_policy.max_active_children then
|
|
156
|
+
raise exception 'max_active_children: root has reached its active delegated-child ceiling' using errcode = '42501';
|
|
157
|
+
end if;
|
|
158
|
+
end if;
|
|
159
|
+
|
|
160
|
+
-- Provision the child: parent = root = p_root_agent_id, same owner_user_id, own
|
|
161
|
+
-- auth binding (distinct auth_user_id for the active-unique index).
|
|
162
|
+
v_child_auth_user_id := agent_board._ensure_agent_auth_user(v_slug);
|
|
163
|
+
|
|
164
|
+
insert into agent_board.agents(
|
|
165
|
+
agent_type, kind, handle, display_name, status,
|
|
166
|
+
auth_user_id, owner_user_id, parent_agent_id, root_agent_id, metadata
|
|
167
|
+
)
|
|
168
|
+
values (
|
|
169
|
+
'agent', 'agent'::agent_board.agent_kind, v_slug,
|
|
170
|
+
agent_board._default_provision_display_name(v_slug), 'active',
|
|
171
|
+
v_child_auth_user_id, v_root.owner_user_id, p_root_agent_id, p_root_agent_id,
|
|
172
|
+
jsonb_build_object('delegatedByRootAgentId', p_root_agent_id, 'grantedScopes', v_requested)
|
|
173
|
+
)
|
|
174
|
+
returning id into v_child_id;
|
|
175
|
+
|
|
176
|
+
-- Scoped REVOCABLE signing key when the policy requires it. #5: the child's
|
|
177
|
+
-- public key MUST be supplied by the caller (generated client-side; the private
|
|
178
|
+
-- key never touches the DB). We NEVER seed a placeholder/zero key and NEVER
|
|
179
|
+
-- generate a key in-DB. status='active' + proof_state='proven' so the child can
|
|
180
|
+
-- immediately re-sign with the private key it already holds. The fingerprint is
|
|
181
|
+
-- derived deterministically from the supplied public key.
|
|
182
|
+
if v_policy.require_child_signing_key then
|
|
183
|
+
if p_child_public_key is null then
|
|
184
|
+
raise exception 'a child public key is required when the delegation policy requires a signing key' using errcode = '22023';
|
|
185
|
+
end if;
|
|
186
|
+
-- Raw Ed25519 public keys are 32 bytes => 43 base64url chars (no padding).
|
|
187
|
+
if p_child_public_key !~ '^[A-Za-z0-9_-]{43}$' then
|
|
188
|
+
raise exception 'malformed child public key: expected a 43-char base64url raw Ed25519 key' using errcode = '22023';
|
|
189
|
+
end if;
|
|
190
|
+
-- Reject the all-''A'' key: 43 base64url ''A''s decode to 32 zero bytes (a
|
|
191
|
+
-- small-order point), which is exactly the retired placeholder.
|
|
192
|
+
if p_child_public_key = repeat('A', 43) then
|
|
193
|
+
raise exception 'child public key is the all-zero small-order placeholder and is rejected' using errcode = '22023';
|
|
194
|
+
end if;
|
|
195
|
+
|
|
196
|
+
insert into agent_board.agent_signing_keys(
|
|
197
|
+
id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint,
|
|
198
|
+
status, proof_state, metadata
|
|
199
|
+
)
|
|
200
|
+
values (
|
|
201
|
+
gen_random_uuid(), v_child_id, 'delegated-ephemeral', 1, p_child_public_key,
|
|
202
|
+
'raw_ed25519_base64url',
|
|
203
|
+
'sha256:' || rtrim(translate(encode(extensions.digest(convert_to(p_child_public_key, 'UTF8'), 'sha256'), 'base64'), '+/', '-_'), '='),
|
|
204
|
+
'active', 'proven',
|
|
205
|
+
jsonb_build_object('ephemeral', true, 'scopes', v_requested)
|
|
206
|
+
)
|
|
207
|
+
returning id into v_key_id;
|
|
208
|
+
end if;
|
|
209
|
+
|
|
210
|
+
-- Grant row: ceiling-bounded hours-scale working clock.
|
|
211
|
+
v_grant_expires_at := now() + least(coalesce(p_ttl, v_policy.child_ttl_ceiling), v_policy.child_ttl_ceiling);
|
|
212
|
+
|
|
213
|
+
insert into agent_board.delegation_grants(
|
|
214
|
+
child_agent_id, root_agent_id, parent_agent_id, delegation_policy_id,
|
|
215
|
+
granted_scopes, grant_expires_at
|
|
216
|
+
)
|
|
217
|
+
values (
|
|
218
|
+
v_child_id, p_root_agent_id, p_root_agent_id, v_policy.id,
|
|
219
|
+
v_requested, v_grant_expires_at
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return query select v_child_id, v_grant_expires_at, v_key_id;
|
|
223
|
+
end;
|
|
224
|
+
$$;
|
|
225
|
+
|
|
226
|
+
comment on function agent_board.mint_delegated_child(uuid, text, jsonb, text, interval) is
|
|
227
|
+
'Mints a bounded root_minted delegated child (scope ⊆ root allowed_scopes, max_depth=1, policy-row-locked max_active_children, ceiling-bounded grant TTL) enrolling the CALLER-SUPPLIED Ed25519 public key (never a placeholder; private key stays client-side) as a revocable signing key. service_role-only.';
|
|
228
|
+
|
|
229
|
+
revoke all on function agent_board.mint_delegated_child(uuid, text, jsonb, text, interval) from public, anon, authenticated;
|
|
230
|
+
grant execute on function agent_board.mint_delegated_child(uuid, text, jsonb, text, interval) to service_role;
|
|
231
|
+
|
|
232
|
+
-- 3. renew_delegated_grant ---------------------------------------------------
|
|
233
|
+
-- service_role-only. Extends grant_expires_at to now() + least(coalesce(p_extend,
|
|
234
|
+
-- child_ttl_ceiling), child_ttl_ceiling) ONLY while the root principal is active,
|
|
235
|
+
-- the root policy is unrevoked, and the grant is unrevoked. A revoked/inactive root
|
|
236
|
+
-- => renewal raises, so the grant can never outlive the root's authority.
|
|
237
|
+
create or replace function agent_board.renew_delegated_grant(
|
|
238
|
+
p_child_agent_id uuid,
|
|
239
|
+
p_extend interval default null
|
|
240
|
+
)
|
|
241
|
+
returns table(
|
|
242
|
+
grant_expires_at timestamptz
|
|
243
|
+
)
|
|
244
|
+
language plpgsql
|
|
245
|
+
security definer
|
|
246
|
+
set search_path = agent_board, public
|
|
247
|
+
as $$
|
|
248
|
+
declare
|
|
249
|
+
v_grant agent_board.delegation_grants%rowtype;
|
|
250
|
+
v_policy agent_board.delegation_policies%rowtype;
|
|
251
|
+
v_new_expires_at timestamptz;
|
|
252
|
+
begin
|
|
253
|
+
if not agent_board._is_service_role() then
|
|
254
|
+
raise exception 'renew_delegated_grant is service_role-only' using errcode = '42501';
|
|
255
|
+
end if;
|
|
256
|
+
|
|
257
|
+
select * into v_grant
|
|
258
|
+
from agent_board.delegation_grants g
|
|
259
|
+
where g.child_agent_id = p_child_agent_id
|
|
260
|
+
for update;
|
|
261
|
+
if not found then
|
|
262
|
+
raise exception 'delegation grant not found for child' using errcode = 'P0002';
|
|
263
|
+
end if;
|
|
264
|
+
if v_grant.revoked_at is not null then
|
|
265
|
+
raise exception 'grant_revoked: cannot renew a revoked delegation grant' using errcode = '42501';
|
|
266
|
+
end if;
|
|
267
|
+
|
|
268
|
+
select * into v_policy
|
|
269
|
+
from agent_board.delegation_policies dp
|
|
270
|
+
where dp.id = v_grant.delegation_policy_id;
|
|
271
|
+
if not found or v_policy.revoked_at is not null then
|
|
272
|
+
raise exception 'policy_revoked: the root delegation policy is revoked' using errcode = '42501';
|
|
273
|
+
end if;
|
|
274
|
+
|
|
275
|
+
-- The operational meaning of "<= root's remaining authority": a revoked/inactive
|
|
276
|
+
-- root makes _agent_principal_active(root) false, so renewal raises.
|
|
277
|
+
if not agent_board._agent_principal_active(v_grant.root_agent_id) then
|
|
278
|
+
raise exception 'principal_inactive: root agent principal is not active; grant cannot outlive its root' using errcode = '42501';
|
|
279
|
+
end if;
|
|
280
|
+
|
|
281
|
+
v_new_expires_at := now() + least(coalesce(p_extend, v_policy.child_ttl_ceiling), v_policy.child_ttl_ceiling);
|
|
282
|
+
|
|
283
|
+
update agent_board.delegation_grants
|
|
284
|
+
set grant_expires_at = v_new_expires_at,
|
|
285
|
+
last_renewed_at = now()
|
|
286
|
+
where child_agent_id = p_child_agent_id;
|
|
287
|
+
|
|
288
|
+
return query select v_new_expires_at;
|
|
289
|
+
end;
|
|
290
|
+
$$;
|
|
291
|
+
|
|
292
|
+
comment on function agent_board.renew_delegated_grant(uuid, interval) is
|
|
293
|
+
'Extends a child grant''s hours-scale grant_expires_at only while the root principal is active and unrevoked; raises otherwise so a grant cannot outlive the root''s authority. service_role-only.';
|
|
294
|
+
|
|
295
|
+
revoke all on function agent_board.renew_delegated_grant(uuid, interval) from public, anon, authenticated;
|
|
296
|
+
grant execute on function agent_board.renew_delegated_grant(uuid, interval) to service_role;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
-- MBDELEG SL-1 (IF-0-MBDELEG-3): refresh-checked subtree revocation cascade.
|
|
2
|
+
--
|
|
3
|
+
-- Additive, forward-only. Net-new. Requires the 'revoked' agents.status literal
|
|
4
|
+
-- from 20260718000900 (this file writes agents.status='revoked').
|
|
5
|
+
--
|
|
6
|
+
-- Two cascades share one per-principal primitive, _revoke_agent_principal:
|
|
7
|
+
-- * revoke_agent_subtree(root) walks DOWN the grant-derived lineage (the root
|
|
8
|
+
-- itself + every descendant that holds a delegation_grants row). This is the
|
|
9
|
+
-- OPPOSITE direction to _agent_principal_active (20260718000500), which walks UP
|
|
10
|
+
-- to ancestors — do not invert either. Because independent_auth principals carry
|
|
11
|
+
-- NULL parent_agent_id and hold no grant row, they are structurally NOT
|
|
12
|
+
-- descendants of any root and are never in this set: they survive a root/parent
|
|
13
|
+
-- revocation and are killed only by a self-revoke or by revoke_human_subtree.
|
|
14
|
+
-- * revoke_human_subtree(owner) is TOTAL for the human: it resolves EVERY agent
|
|
15
|
+
-- owned by owner_user_id (all lineage roots AND every lineage-independent
|
|
16
|
+
-- independent_auth principal) and revokes each. It is deliberately broader than
|
|
17
|
+
-- revoke_agent_subtree because current_agent_id() joins on the agent's own
|
|
18
|
+
-- auth_user_id, so disabling the human's auth.users row alone flips no agent.
|
|
19
|
+
--
|
|
20
|
+
-- Per revoked principal, in one transaction: flip every non-revoked signing key to
|
|
21
|
+
-- status='revoked'/proof_state='revoked' (revoked_at=now()), flip agents.status to
|
|
22
|
+
-- 'revoked', revoke any live delegation_grants row, and append fleet_revocation_events
|
|
23
|
+
-- rows (subject_kind='agent' per principal, 'signing_key' per key) FOR AUDIT ONLY —
|
|
24
|
+
-- no refresh/send path reads that ledger, so a ledger row alone is inert. The kill is
|
|
25
|
+
-- delivered by the key + status flips: the next <=15-min mint_fleet_agent_session
|
|
26
|
+
-- re-mint FAILS (revoked key => 42501; non-active status => _agent_principal_active
|
|
27
|
+
-- false), so the subtree dies within one token TTL even though its grant lifetime was
|
|
28
|
+
-- hours.
|
|
29
|
+
|
|
30
|
+
-- Per-principal revocation primitive. Returns the number of signing keys it flipped.
|
|
31
|
+
create or replace function agent_board._revoke_agent_principal(
|
|
32
|
+
p_agent_id uuid,
|
|
33
|
+
p_revoked_by_agent_id uuid,
|
|
34
|
+
p_reason text
|
|
35
|
+
)
|
|
36
|
+
returns integer
|
|
37
|
+
language plpgsql
|
|
38
|
+
security definer
|
|
39
|
+
set search_path = agent_board, public
|
|
40
|
+
as $$
|
|
41
|
+
declare
|
|
42
|
+
v_key record;
|
|
43
|
+
v_key_count integer := 0;
|
|
44
|
+
v_agent_flipped boolean := false;
|
|
45
|
+
begin
|
|
46
|
+
-- Flip every live signing key + emit a signing_key audit row per key.
|
|
47
|
+
for v_key in
|
|
48
|
+
select id
|
|
49
|
+
from agent_board.agent_signing_keys
|
|
50
|
+
where agent_id = p_agent_id
|
|
51
|
+
and status not in ('revoked', 'compromised')
|
|
52
|
+
for update
|
|
53
|
+
loop
|
|
54
|
+
update agent_board.agent_signing_keys
|
|
55
|
+
set status = 'revoked',
|
|
56
|
+
proof_state = 'revoked',
|
|
57
|
+
revoked_at = now(),
|
|
58
|
+
revoked_by_agent_id = p_revoked_by_agent_id,
|
|
59
|
+
revocation_reason = p_reason,
|
|
60
|
+
updated_at = now()
|
|
61
|
+
where id = v_key.id;
|
|
62
|
+
|
|
63
|
+
insert into agent_board.fleet_revocation_events(
|
|
64
|
+
subject_kind, subject_id, signing_key_id, agent_id,
|
|
65
|
+
revoked_by_agent_id, revocation_reason, metadata
|
|
66
|
+
)
|
|
67
|
+
values (
|
|
68
|
+
'signing_key', v_key.id, v_key.id, p_agent_id,
|
|
69
|
+
p_revoked_by_agent_id, p_reason,
|
|
70
|
+
jsonb_build_object('source', 'revoke_agent_subtree')
|
|
71
|
+
)
|
|
72
|
+
on conflict (subject_kind, subject_id) do update
|
|
73
|
+
set revoked_by_agent_id = excluded.revoked_by_agent_id,
|
|
74
|
+
revocation_reason = excluded.revocation_reason,
|
|
75
|
+
signing_key_id = excluded.signing_key_id,
|
|
76
|
+
agent_id = excluded.agent_id,
|
|
77
|
+
effective_at = now(),
|
|
78
|
+
metadata = excluded.metadata;
|
|
79
|
+
|
|
80
|
+
v_key_count := v_key_count + 1;
|
|
81
|
+
end loop;
|
|
82
|
+
|
|
83
|
+
-- Flip the agent status (idempotent: only count a genuine active->revoked flip).
|
|
84
|
+
update agent_board.agents
|
|
85
|
+
set status = 'revoked',
|
|
86
|
+
updated_at = now()
|
|
87
|
+
where id = p_agent_id
|
|
88
|
+
and status <> 'revoked';
|
|
89
|
+
if found then
|
|
90
|
+
v_agent_flipped := true;
|
|
91
|
+
end if;
|
|
92
|
+
|
|
93
|
+
-- Revoke any live grant this principal holds as a child.
|
|
94
|
+
update agent_board.delegation_grants
|
|
95
|
+
set revoked_at = now()
|
|
96
|
+
where child_agent_id = p_agent_id
|
|
97
|
+
and revoked_at is null;
|
|
98
|
+
|
|
99
|
+
-- Also revoke the principal's legacy agent_credentials (mirrors revoke_agent in
|
|
100
|
+
-- 20260512000000). Without this, a later status flip-back would re-open the
|
|
101
|
+
-- refresh-token credential path that the key/status flips did not touch.
|
|
102
|
+
update agent_board.agent_credentials ac
|
|
103
|
+
set revoked_at = coalesce(ac.revoked_at, now()),
|
|
104
|
+
updated_at = now(),
|
|
105
|
+
metadata = coalesce(ac.metadata, '{}'::jsonb)
|
|
106
|
+
|| jsonb_build_object(
|
|
107
|
+
'revokedByAgentId', p_revoked_by_agent_id,
|
|
108
|
+
'revocationReason', p_reason,
|
|
109
|
+
'source', 'revoke_agent_subtree'
|
|
110
|
+
)
|
|
111
|
+
where ac.agent_id = p_agent_id
|
|
112
|
+
and ac.revoked_at is null;
|
|
113
|
+
|
|
114
|
+
-- Audit row for the principal itself.
|
|
115
|
+
insert into agent_board.fleet_revocation_events(
|
|
116
|
+
subject_kind, subject_id, agent_id,
|
|
117
|
+
revoked_by_agent_id, revocation_reason, metadata
|
|
118
|
+
)
|
|
119
|
+
values (
|
|
120
|
+
'agent', p_agent_id, p_agent_id,
|
|
121
|
+
p_revoked_by_agent_id, p_reason,
|
|
122
|
+
jsonb_build_object('source', 'revoke_agent_subtree', 'statusFlipped', v_agent_flipped)
|
|
123
|
+
)
|
|
124
|
+
on conflict (subject_kind, subject_id) do update
|
|
125
|
+
set revoked_by_agent_id = excluded.revoked_by_agent_id,
|
|
126
|
+
revocation_reason = excluded.revocation_reason,
|
|
127
|
+
agent_id = excluded.agent_id,
|
|
128
|
+
effective_at = now(),
|
|
129
|
+
metadata = excluded.metadata;
|
|
130
|
+
|
|
131
|
+
return v_key_count;
|
|
132
|
+
end;
|
|
133
|
+
$$;
|
|
134
|
+
|
|
135
|
+
comment on function agent_board._revoke_agent_principal(uuid, uuid, text) is
|
|
136
|
+
'Per-principal revocation primitive: flips all live signing keys + agents.status to revoked, revokes any child grant, appends audit-only fleet_revocation_events. Shared by revoke_agent_subtree and revoke_human_subtree.';
|
|
137
|
+
|
|
138
|
+
revoke all on function agent_board._revoke_agent_principal(uuid, uuid, text) from public, anon, authenticated;
|
|
139
|
+
grant execute on function agent_board._revoke_agent_principal(uuid, uuid, text) to service_role;
|
|
140
|
+
|
|
141
|
+
-- revoke_agent_subtree: DOWN-walking grant-derived cascade from a root.
|
|
142
|
+
create or replace function agent_board.revoke_agent_subtree(
|
|
143
|
+
p_root_agent_id uuid,
|
|
144
|
+
p_revoked_by_agent_id uuid,
|
|
145
|
+
p_reason text default 'root revocation'
|
|
146
|
+
)
|
|
147
|
+
returns table(
|
|
148
|
+
revoked_agent_count integer,
|
|
149
|
+
revoked_key_count integer
|
|
150
|
+
)
|
|
151
|
+
language plpgsql
|
|
152
|
+
security definer
|
|
153
|
+
set search_path = agent_board, public
|
|
154
|
+
as $$
|
|
155
|
+
declare
|
|
156
|
+
v_agent uuid;
|
|
157
|
+
v_agent_count integer := 0;
|
|
158
|
+
v_key_count integer := 0;
|
|
159
|
+
begin
|
|
160
|
+
if not agent_board._is_service_role() then
|
|
161
|
+
raise exception 'revoke_agent_subtree is service_role-only' using errcode = '42501';
|
|
162
|
+
end if;
|
|
163
|
+
|
|
164
|
+
if not exists (select 1 from agent_board.agents where id = p_root_agent_id) then
|
|
165
|
+
raise exception 'root agent not found' using errcode = 'P0002';
|
|
166
|
+
end if;
|
|
167
|
+
|
|
168
|
+
-- DOWN walk: the root itself (the revocation target) plus every descendant whose
|
|
169
|
+
-- authority derives from the root (it holds a delegation_grants row parented at a
|
|
170
|
+
-- member of the set). Recurses child.parent_agent_id -> parent.id via the grant
|
|
171
|
+
-- table, so lineage-independent independent_auth principals (no grant row) are
|
|
172
|
+
-- never reached.
|
|
173
|
+
for v_agent in
|
|
174
|
+
with recursive subtree(agent_id) as (
|
|
175
|
+
select p_root_agent_id
|
|
176
|
+
union
|
|
177
|
+
select g.child_agent_id
|
|
178
|
+
from agent_board.delegation_grants g
|
|
179
|
+
join subtree s on g.parent_agent_id = s.agent_id
|
|
180
|
+
)
|
|
181
|
+
select agent_id from subtree
|
|
182
|
+
loop
|
|
183
|
+
v_key_count := v_key_count + agent_board._revoke_agent_principal(v_agent, p_revoked_by_agent_id, p_reason);
|
|
184
|
+
v_agent_count := v_agent_count + 1;
|
|
185
|
+
end loop;
|
|
186
|
+
|
|
187
|
+
return query select v_agent_count, v_key_count;
|
|
188
|
+
end;
|
|
189
|
+
$$;
|
|
190
|
+
|
|
191
|
+
comment on function agent_board.revoke_agent_subtree(uuid, uuid, text) is
|
|
192
|
+
'DOWN-walking refresh-checked cascade: revokes a root and every grant-derived descendant (keys + agents.status + grants + audit rows). independent_auth principals are structurally excluded (NULL lineage / no grant). service_role-only.';
|
|
193
|
+
|
|
194
|
+
revoke all on function agent_board.revoke_agent_subtree(uuid, uuid, text) from public, anon, authenticated;
|
|
195
|
+
grant execute on function agent_board.revoke_agent_subtree(uuid, uuid, text) to service_role;
|
|
196
|
+
|
|
197
|
+
-- revoke_human_subtree: TOTAL for the human — every principal owned by owner_user_id.
|
|
198
|
+
create or replace function agent_board.revoke_human_subtree(
|
|
199
|
+
p_owner_user_id uuid,
|
|
200
|
+
p_revoked_by_agent_id uuid,
|
|
201
|
+
p_reason text default 'human revocation'
|
|
202
|
+
)
|
|
203
|
+
returns table(
|
|
204
|
+
revoked_agent_count integer,
|
|
205
|
+
revoked_key_count integer
|
|
206
|
+
)
|
|
207
|
+
language plpgsql
|
|
208
|
+
security definer
|
|
209
|
+
set search_path = agent_board, public
|
|
210
|
+
as $$
|
|
211
|
+
declare
|
|
212
|
+
v_agent uuid;
|
|
213
|
+
v_agent_count integer := 0;
|
|
214
|
+
v_key_count integer := 0;
|
|
215
|
+
begin
|
|
216
|
+
if not agent_board._is_service_role() then
|
|
217
|
+
raise exception 'revoke_human_subtree is service_role-only' using errcode = '42501';
|
|
218
|
+
end if;
|
|
219
|
+
|
|
220
|
+
-- Total human revocation: every agent owned by this human — lineage roots,
|
|
221
|
+
-- their delegated descendants, AND every lineage-independent independent_auth
|
|
222
|
+
-- principal — because current_agent_id() joins on the agent's own auth_user_id.
|
|
223
|
+
for v_agent in
|
|
224
|
+
select id from agent_board.agents where owner_user_id = p_owner_user_id
|
|
225
|
+
loop
|
|
226
|
+
v_key_count := v_key_count + agent_board._revoke_agent_principal(v_agent, p_revoked_by_agent_id, p_reason);
|
|
227
|
+
v_agent_count := v_agent_count + 1;
|
|
228
|
+
end loop;
|
|
229
|
+
|
|
230
|
+
return query select v_agent_count, v_key_count;
|
|
231
|
+
end;
|
|
232
|
+
$$;
|
|
233
|
+
|
|
234
|
+
comment on function agent_board.revoke_human_subtree(uuid, uuid, text) is
|
|
235
|
+
'TOTAL human revocation: revokes every agent owned by owner_user_id (lineage roots AND lineage-independent independent_auth principals). Broader than revoke_agent_subtree. service_role-only.';
|
|
236
|
+
|
|
237
|
+
revoke all on function agent_board.revoke_human_subtree(uuid, uuid, text) from public, anon, authenticated;
|
|
238
|
+
grant execute on function agent_board.revoke_human_subtree(uuid, uuid, text) to service_role;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
-- MBDELEG SL-2 (IF-0-MBDELEG-4): re-signing refresh; demote the 30-day
|
|
2
|
+
-- agent_credentials refresh token to an enrollment/lineage record.
|
|
3
|
+
--
|
|
4
|
+
-- Additive, forward-only. Redefines two existing functions via CREATE OR REPLACE in
|
|
5
|
+
-- this NEW file — no released historical bytes are edited:
|
|
6
|
+
--
|
|
7
|
+
-- * rotate_agent_token(uuid) — the 30-day session-exchange RPC — now RAISES a
|
|
8
|
+
-- deprecation error (0A000). Session refresh is SOLELY re-signing through the
|
|
9
|
+
-- already-built PoP -> mint_fleet_agent_session loop (MBSESSION, 20260718000600).
|
|
10
|
+
-- The RETURNS-TABLE signature is preserved (CREATE OR REPLACE cannot change it),
|
|
11
|
+
-- but the body raises before returning any row.
|
|
12
|
+
--
|
|
13
|
+
-- * _issue_agent_credential(...) — the single choke point every issuer funnels
|
|
14
|
+
-- through (provision_agent still routes here) — now tags every minted credential
|
|
15
|
+
-- with metadata.credential_role = 'enrollment', demoting it to an
|
|
16
|
+
-- enrollment/lineage record: it is NO LONGER a session-exchangeable refresh token
|
|
17
|
+
-- (the only exchange RPC, rotate_agent_token, now raises). The body is carried
|
|
18
|
+
-- forward from the latest definition (20260711000000 system-principal fail-closed
|
|
19
|
+
-- guard) so that guard is preserved.
|
|
20
|
+
--
|
|
21
|
+
-- Maintainer decision (recommended, surfaced in the closeout): demotion mechanism is
|
|
22
|
+
-- raise-on-exchange (rotate_agent_token) + enrollment-only reissue
|
|
23
|
+
-- (_issue_agent_credential), rather than dropping the column/table.
|
|
24
|
+
|
|
25
|
+
-- 1. _issue_agent_credential: preserve system-principal fail-closed guard, tag
|
|
26
|
+
-- every credential as an enrollment/lineage record.
|
|
27
|
+
create or replace function agent_board._issue_agent_credential(
|
|
28
|
+
p_agent_id uuid,
|
|
29
|
+
p_credential_label text,
|
|
30
|
+
p_replaces_credential_id uuid default null,
|
|
31
|
+
p_metadata jsonb default '{}'::jsonb
|
|
32
|
+
)
|
|
33
|
+
returns table(
|
|
34
|
+
credential_id uuid,
|
|
35
|
+
refresh_token text,
|
|
36
|
+
expires_at timestamptz,
|
|
37
|
+
created_at timestamptz,
|
|
38
|
+
replaces_credential_id uuid
|
|
39
|
+
)
|
|
40
|
+
language plpgsql
|
|
41
|
+
security definer
|
|
42
|
+
set search_path = ''
|
|
43
|
+
as $$
|
|
44
|
+
declare
|
|
45
|
+
v_refresh_token text;
|
|
46
|
+
v_expires_at timestamptz := now() + interval '30 days';
|
|
47
|
+
v_created_at timestamptz := now();
|
|
48
|
+
v_credential_id uuid;
|
|
49
|
+
begin
|
|
50
|
+
-- Fail closed for system principals (carried forward from 20260711000000): they
|
|
51
|
+
-- have no auth binding, so any credential minted here is un-exchangeable.
|
|
52
|
+
if exists (
|
|
53
|
+
select 1
|
|
54
|
+
from agent_board.agents a
|
|
55
|
+
where a.id = p_agent_id
|
|
56
|
+
and a.kind = 'system'::agent_board.agent_kind
|
|
57
|
+
) then
|
|
58
|
+
raise exception 'cannot issue an exchangeable credential for a system principal (no auth binding)'
|
|
59
|
+
using errcode = '22023';
|
|
60
|
+
end if;
|
|
61
|
+
|
|
62
|
+
v_refresh_token := agent_board._mint_agent_refresh_token();
|
|
63
|
+
|
|
64
|
+
insert into agent_board.agent_credentials(
|
|
65
|
+
agent_id,
|
|
66
|
+
refresh_token_hash,
|
|
67
|
+
hash_version,
|
|
68
|
+
credential_label,
|
|
69
|
+
expires_at,
|
|
70
|
+
replaces_credential_id,
|
|
71
|
+
metadata,
|
|
72
|
+
created_at,
|
|
73
|
+
updated_at
|
|
74
|
+
)
|
|
75
|
+
values (
|
|
76
|
+
p_agent_id,
|
|
77
|
+
extensions.digest(v_refresh_token, 'sha256'),
|
|
78
|
+
1,
|
|
79
|
+
p_credential_label,
|
|
80
|
+
v_expires_at,
|
|
81
|
+
p_replaces_credential_id,
|
|
82
|
+
-- MBDELEG: demote to an enrollment/lineage record. No session is exchangeable
|
|
83
|
+
-- from it (rotate_agent_token now raises).
|
|
84
|
+
coalesce(p_metadata, '{}'::jsonb) || jsonb_build_object('credential_role', 'enrollment'),
|
|
85
|
+
v_created_at,
|
|
86
|
+
v_created_at
|
|
87
|
+
)
|
|
88
|
+
returning id into v_credential_id;
|
|
89
|
+
|
|
90
|
+
if p_replaces_credential_id is not null then
|
|
91
|
+
update agent_board.agent_credentials
|
|
92
|
+
set revoked_at = coalesce(revoked_at, v_created_at),
|
|
93
|
+
rotated_at = coalesce(rotated_at, v_created_at),
|
|
94
|
+
replaced_by_credential_id = v_credential_id,
|
|
95
|
+
updated_at = v_created_at
|
|
96
|
+
where id = p_replaces_credential_id;
|
|
97
|
+
end if;
|
|
98
|
+
|
|
99
|
+
return query
|
|
100
|
+
select v_credential_id, v_refresh_token, v_expires_at, v_created_at, p_replaces_credential_id;
|
|
101
|
+
end;
|
|
102
|
+
$$;
|
|
103
|
+
|
|
104
|
+
-- 2. rotate_agent_token: retire the 30-day session-exchange path. Raise on call.
|
|
105
|
+
create or replace function agent_board.rotate_agent_token(
|
|
106
|
+
p_agent_id uuid
|
|
107
|
+
)
|
|
108
|
+
returns table(
|
|
109
|
+
agent_id uuid,
|
|
110
|
+
credential_id uuid,
|
|
111
|
+
refresh_token text,
|
|
112
|
+
expires_at timestamptz,
|
|
113
|
+
rotated_at timestamptz,
|
|
114
|
+
replaces_credential_id uuid
|
|
115
|
+
)
|
|
116
|
+
language plpgsql
|
|
117
|
+
security definer
|
|
118
|
+
set search_path = agent_board, public
|
|
119
|
+
as $$
|
|
120
|
+
begin
|
|
121
|
+
-- MBDELEG (no-long-lived-tokens): the 30-day refresh-token exchange is retired.
|
|
122
|
+
-- Session refresh is solely re-signing: PoP -> agent_board.mint_fleet_agent_session.
|
|
123
|
+
raise exception 'rotate_agent_token is deprecated: session refresh is re-signing (PoP -> mint_fleet_agent_session), not a 30-day refresh-token exchange'
|
|
124
|
+
using errcode = '0A000';
|
|
125
|
+
end;
|
|
126
|
+
$$;
|
|
127
|
+
|
|
128
|
+
comment on function agent_board.rotate_agent_token(uuid) is
|
|
129
|
+
'DEPRECATED (MBDELEG): raises 0A000. The 30-day refresh-token exchange is retired; session refresh is re-signing via PoP -> mint_fleet_agent_session.';
|