@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,192 @@
|
|
|
1
|
+
-- MBSENDAUTHZ SL-2 (Lane B): fleet-policy non-fleet default-deny (mode-gated).
|
|
2
|
+
--
|
|
3
|
+
-- Forward migration. Re-declares evaluate_signed_write_fleet_policy with its full
|
|
4
|
+
-- body forward-copied verbatim from 20260711000100 (exact signature preserved),
|
|
5
|
+
-- replacing ONLY the non-fleet fail-open (v_machine_id is null => allow) with a
|
|
6
|
+
-- per-sender-fleet-scope mode-gated branch:
|
|
7
|
+
-- * fail_open_preserved (default / absent flag row) => allow silently (unchanged);
|
|
8
|
+
-- * observe => allow AND record one
|
|
9
|
+
-- would_be_deny telemetry row;
|
|
10
|
+
-- * deny => deny (denial_class
|
|
11
|
+
-- 'nonfleet_sender_denied')
|
|
12
|
+
-- unless the sender is in the
|
|
13
|
+
-- tight nonfleet_sender_allowlist.
|
|
14
|
+
-- Fleet-resolvable senders keep the existing per-recipient evaluation path unchanged.
|
|
15
|
+
-- Dormant: every scope defaults to fail_open_preserved until an operator flips it.
|
|
16
|
+
|
|
17
|
+
create or replace function agent_board.evaluate_signed_write_fleet_policy(
|
|
18
|
+
p_operation text,
|
|
19
|
+
p_payload jsonb,
|
|
20
|
+
p_gateway_decision jsonb
|
|
21
|
+
)
|
|
22
|
+
returns table(
|
|
23
|
+
allowed boolean,
|
|
24
|
+
actionable boolean,
|
|
25
|
+
denial_class text,
|
|
26
|
+
denied_recipient_agent_id uuid,
|
|
27
|
+
evaluated_at timestamptz
|
|
28
|
+
)
|
|
29
|
+
language plpgsql
|
|
30
|
+
security definer
|
|
31
|
+
set search_path = agent_board, public
|
|
32
|
+
as $$
|
|
33
|
+
declare
|
|
34
|
+
v_actor uuid := nullif(p_gateway_decision ->> 'agent_id', '')::uuid;
|
|
35
|
+
v_machine_id uuid;
|
|
36
|
+
v_board_id uuid;
|
|
37
|
+
v_recipient_ids uuid[] := '{}'::uuid[];
|
|
38
|
+
v_recipient_id uuid;
|
|
39
|
+
v_evidence_id uuid;
|
|
40
|
+
v_decision record;
|
|
41
|
+
v_status text := coalesce(nullif(p_gateway_decision ->> 'verification_status', ''), 'unsigned');
|
|
42
|
+
v_scope text;
|
|
43
|
+
v_mode text;
|
|
44
|
+
v_origin text;
|
|
45
|
+
begin
|
|
46
|
+
if not agent_board._is_service_role() then
|
|
47
|
+
raise exception 'signed fleet policy evaluation requires service role' using errcode = '42501';
|
|
48
|
+
end if;
|
|
49
|
+
if v_actor is null then
|
|
50
|
+
raise exception 'actor_mismatch: signed write actor is required' using errcode = '42501';
|
|
51
|
+
end if;
|
|
52
|
+
|
|
53
|
+
select fma.machine_id
|
|
54
|
+
into v_machine_id
|
|
55
|
+
from agent_board.fleet_machine_agents fma
|
|
56
|
+
join agent_board.fleet_machines fm on fm.id = fma.machine_id
|
|
57
|
+
where fma.agent_id = v_actor
|
|
58
|
+
and fma.lifecycle_state = 'active'
|
|
59
|
+
and fm.lifecycle_state = 'active'
|
|
60
|
+
order by fma.updated_at desc
|
|
61
|
+
limit 1;
|
|
62
|
+
|
|
63
|
+
-- Non-fleet sender (v_machine_id is null): the historical fail-open, now
|
|
64
|
+
-- mode-gated per sender-fleet-scope. Absent flag row => fail_open_preserved
|
|
65
|
+
-- (unchanged behavior). This branch fires before any board is resolved, so
|
|
66
|
+
-- the scope is the catch-all 'nonfleet' bucket. Retained until an operator
|
|
67
|
+
-- flips a scope to deny (deferred to operations).
|
|
68
|
+
if v_machine_id is null then
|
|
69
|
+
v_scope := agent_board._resolve_send_fleet_scope(v_actor);
|
|
70
|
+
v_mode := agent_board._send_fleet_scope_mode(v_scope);
|
|
71
|
+
v_origin := coalesce(nullif(p_gateway_decision -> 'signed_write' ->> 'origin', ''), 'unknown');
|
|
72
|
+
if v_mode = 'deny' then
|
|
73
|
+
if exists (select 1 from agent_board.nonfleet_sender_allowlist a where a.agent_id = v_actor) then
|
|
74
|
+
return query select true, true, null::text, null::uuid, now();
|
|
75
|
+
return;
|
|
76
|
+
end if;
|
|
77
|
+
perform agent_board._record_send_authz_telemetry(
|
|
78
|
+
'would_be_deny', v_origin, v_actor, null, p_operation, v_scope,
|
|
79
|
+
'nonfleet_sender_denied', jsonb_build_object('mode', 'deny', 'redacted', true));
|
|
80
|
+
return query select false, false, 'nonfleet_sender_denied'::text, null::uuid, now();
|
|
81
|
+
return;
|
|
82
|
+
elsif v_mode = 'observe' then
|
|
83
|
+
perform agent_board._record_send_authz_telemetry(
|
|
84
|
+
'would_be_deny', v_origin, v_actor, null, p_operation, v_scope,
|
|
85
|
+
'nonfleet_sender_denied', jsonb_build_object('mode', 'observe', 'redacted', true));
|
|
86
|
+
return query select true, true, null::text, null::uuid, now();
|
|
87
|
+
return;
|
|
88
|
+
else
|
|
89
|
+
-- fail_open_preserved (default): allow silently, current behavior.
|
|
90
|
+
return query select true, true, null::text, null::uuid, now();
|
|
91
|
+
return;
|
|
92
|
+
end if;
|
|
93
|
+
end if;
|
|
94
|
+
|
|
95
|
+
insert into agent_board.signature_evidence(
|
|
96
|
+
agent_id, signing_key_id, key_id, key_version, algorithm, source_kind,
|
|
97
|
+
operation, payload_digest, canonicalization_version, signature_digest,
|
|
98
|
+
verification_status, failure_class, signed_at, verified_at, nonce,
|
|
99
|
+
idempotency_key, external_evidence_ref, metadata
|
|
100
|
+
) values (
|
|
101
|
+
v_actor,
|
|
102
|
+
nullif(p_gateway_decision ->> 'signing_key_id', '')::uuid,
|
|
103
|
+
nullif(p_gateway_decision ->> 'key_id', ''),
|
|
104
|
+
nullif(p_gateway_decision ->> 'key_version', '')::integer,
|
|
105
|
+
coalesce(nullif(p_gateway_decision ->> 'algorithm', ''), 'Ed25519'),
|
|
106
|
+
'external',
|
|
107
|
+
p_operation,
|
|
108
|
+
coalesce(nullif(p_gateway_decision ->> 'payload_digest', ''), 'sha256:unsigned'),
|
|
109
|
+
coalesce(nullif(p_gateway_decision ->> 'canonicalization_version', ''), 'agent-provenance-jcs.v1'),
|
|
110
|
+
nullif(p_gateway_decision ->> 'signature_digest', ''),
|
|
111
|
+
case when v_status in ('unsigned', 'observed', 'warn', 'verified', 'failed') then v_status else 'failed' end,
|
|
112
|
+
nullif(p_gateway_decision ->> 'failure_class', ''),
|
|
113
|
+
nullif(p_gateway_decision ->> 'signed_at', '')::timestamptz,
|
|
114
|
+
nullif(p_gateway_decision ->> 'verified_at', '')::timestamptz,
|
|
115
|
+
nullif(p_gateway_decision ->> 'nonce', ''),
|
|
116
|
+
nullif(p_gateway_decision ->> 'idempotency_key', ''),
|
|
117
|
+
'signed-write-preflight:' || coalesce(nullif(p_gateway_decision ->> 'idempotency_key', ''), nullif(p_gateway_decision ->> 'nonce', ''), gen_random_uuid()::text),
|
|
118
|
+
jsonb_build_object('gateway', 'signed-write-preflight', 'redacted', true, 'metadata-safe', true)
|
|
119
|
+
) returning id into v_evidence_id;
|
|
120
|
+
|
|
121
|
+
if p_operation = 'send_direct_message' then
|
|
122
|
+
if nullif(p_payload ->> 'p_recipient_agent_id', '') is not null then
|
|
123
|
+
v_recipient_ids := array[(p_payload ->> 'p_recipient_agent_id')::uuid];
|
|
124
|
+
else
|
|
125
|
+
v_recipient_ids := agent_board._addressing_agent_ids_or_raise(v_actor, jsonb_build_array(p_payload -> 'p_recipient_descriptor'));
|
|
126
|
+
end if;
|
|
127
|
+
elsif p_operation = 'send_multicast_message' then
|
|
128
|
+
if p_payload ? 'p_recipient_agent_ids' then
|
|
129
|
+
select coalesce(array_agg(value::uuid), '{}'::uuid[]) into v_recipient_ids
|
|
130
|
+
from jsonb_array_elements_text(p_payload -> 'p_recipient_agent_ids');
|
|
131
|
+
else
|
|
132
|
+
v_recipient_ids := agent_board._addressing_agent_ids_or_raise(v_actor, p_payload -> 'p_recipient_descriptors');
|
|
133
|
+
end if;
|
|
134
|
+
elsif p_operation in ('post_board_message', 'create_tasking_delivery') then
|
|
135
|
+
if nullif(p_payload ->> 'p_board_id', '') is not null then
|
|
136
|
+
v_board_id := (p_payload ->> 'p_board_id')::uuid;
|
|
137
|
+
elsif nullif(p_payload ->> 'p_board_slug', '') is not null then
|
|
138
|
+
select id into v_board_id from agent_board.boards where slug = p_payload ->> 'p_board_slug' and status = 'active';
|
|
139
|
+
end if;
|
|
140
|
+
if p_operation = 'create_tasking_delivery' then
|
|
141
|
+
if p_payload ? 'p_recipient_agent_ids' then
|
|
142
|
+
select coalesce(array_agg(value::uuid), '{}'::uuid[]) into v_recipient_ids
|
|
143
|
+
from jsonb_array_elements_text(p_payload -> 'p_recipient_agent_ids');
|
|
144
|
+
elsif p_payload ? 'p_recipient_descriptors' and p_payload -> 'p_recipient_descriptors' <> 'null'::jsonb then
|
|
145
|
+
v_recipient_ids := agent_board._addressing_agent_ids_or_raise(v_actor, p_payload -> 'p_recipient_descriptors');
|
|
146
|
+
end if;
|
|
147
|
+
end if;
|
|
148
|
+
if v_board_id is not null then
|
|
149
|
+
select array(
|
|
150
|
+
select distinct recipient_id from (
|
|
151
|
+
select unnest(v_recipient_ids) as recipient_id
|
|
152
|
+
union
|
|
153
|
+
select bm.agent_id from agent_board.board_memberships bm
|
|
154
|
+
where bm.board_id = v_board_id and bm.membership_state in ('joined', 'subscribed')
|
|
155
|
+
) recipients
|
|
156
|
+
) into v_recipient_ids;
|
|
157
|
+
end if;
|
|
158
|
+
else
|
|
159
|
+
return query select true, true, null::text, null::uuid, now();
|
|
160
|
+
return;
|
|
161
|
+
end if;
|
|
162
|
+
|
|
163
|
+
if coalesce(cardinality(v_recipient_ids), 0) = 0 then
|
|
164
|
+
return query select false, false, 'recipient_policy_denied'::text, null::uuid, now();
|
|
165
|
+
return;
|
|
166
|
+
end if;
|
|
167
|
+
|
|
168
|
+
foreach v_recipient_id in array v_recipient_ids loop
|
|
169
|
+
select * into v_decision
|
|
170
|
+
from agent_board.evaluate_fleet_sender_policy(
|
|
171
|
+
v_actor,
|
|
172
|
+
v_recipient_id,
|
|
173
|
+
v_machine_id,
|
|
174
|
+
v_board_id,
|
|
175
|
+
p_operation,
|
|
176
|
+
v_evidence_id,
|
|
177
|
+
false
|
|
178
|
+
);
|
|
179
|
+
if not coalesce(v_decision.allowed, false) or not coalesce(v_decision.actionable, false) then
|
|
180
|
+
return query select false, false, coalesce(v_decision.denial_class, 'recipient_policy_denied'), v_recipient_id, now();
|
|
181
|
+
return;
|
|
182
|
+
end if;
|
|
183
|
+
end loop;
|
|
184
|
+
|
|
185
|
+
return query select true, true, null::text, null::uuid, now();
|
|
186
|
+
end;
|
|
187
|
+
$$;
|
|
188
|
+
|
|
189
|
+
-- Re-assert grants (mirrors 20260711000100:217/220) so a fresh replay is
|
|
190
|
+
-- order-independent.
|
|
191
|
+
revoke all on function agent_board.evaluate_signed_write_fleet_policy(text, jsonb, jsonb) from public, anon, authenticated;
|
|
192
|
+
grant execute on function agent_board.evaluate_signed_write_fleet_policy(text, jsonb, jsonb) to service_role;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- MBSENDAUTHZ SL-3 (Lane C): dormant board-role grant chokepoint that consumes
|
|
2
|
+
-- the MBAUTHZ ceiling helper.
|
|
3
|
+
--
|
|
4
|
+
-- Additive. There is no authenticated role-granting RPC today (update_board_membership
|
|
5
|
+
-- mutates only membership_state). This adds the minimal dormant chokepoint
|
|
6
|
+
-- grant_board_role — service_role-only (revoked from authenticated) — that consults
|
|
7
|
+
-- the MBAUTHZ _grant_within_ceiling helper before writing board_memberships.role.
|
|
8
|
+
-- It adds NO ceiling logic of its own: the grantee-bounded semantics
|
|
9
|
+
-- (grantee <= min(grantor role, board ceiling); no ceiling row => owner) live in
|
|
10
|
+
-- MBAUTHZ's _grant_within_ceiling and are consumed as-is.
|
|
11
|
+
|
|
12
|
+
create or replace function agent_board.grant_board_role(
|
|
13
|
+
p_board_id uuid,
|
|
14
|
+
p_agent_id uuid,
|
|
15
|
+
p_grantee_role text,
|
|
16
|
+
p_grantor_agent_id uuid
|
|
17
|
+
)
|
|
18
|
+
returns table(board_id uuid, agent_id uuid, role text, updated_at timestamptz)
|
|
19
|
+
language plpgsql
|
|
20
|
+
security definer
|
|
21
|
+
set search_path = agent_board, public
|
|
22
|
+
as $$
|
|
23
|
+
declare
|
|
24
|
+
v_updated agent_board.board_memberships%rowtype;
|
|
25
|
+
begin
|
|
26
|
+
if p_grantee_role not in ('owner', 'admin', 'member', 'observer') then
|
|
27
|
+
raise exception 'invalid grantee role' using errcode = '22023';
|
|
28
|
+
end if;
|
|
29
|
+
if not exists (
|
|
30
|
+
select 1 from agent_board.board_memberships bm
|
|
31
|
+
where bm.board_id = p_board_id and bm.agent_id = p_agent_id
|
|
32
|
+
) then
|
|
33
|
+
raise exception 'grantee has no membership on this board' using errcode = 'P0002';
|
|
34
|
+
end if;
|
|
35
|
+
|
|
36
|
+
-- Ceiling consumption: grantee-bounded against min(grantor role, board ceiling).
|
|
37
|
+
if not agent_board._grant_within_ceiling(p_grantor_agent_id, p_board_id, p_grantee_role) then
|
|
38
|
+
raise exception 'grant exceeds grantor role or board authority ceiling' using errcode = '42501';
|
|
39
|
+
end if;
|
|
40
|
+
|
|
41
|
+
update agent_board.board_memberships bm
|
|
42
|
+
set role = p_grantee_role,
|
|
43
|
+
updated_at = now()
|
|
44
|
+
where bm.board_id = p_board_id
|
|
45
|
+
and bm.agent_id = p_agent_id
|
|
46
|
+
returning * into v_updated;
|
|
47
|
+
|
|
48
|
+
return query
|
|
49
|
+
select v_updated.board_id, v_updated.agent_id, v_updated.role, v_updated.updated_at;
|
|
50
|
+
end;
|
|
51
|
+
$$;
|
|
52
|
+
|
|
53
|
+
comment on function agent_board.grant_board_role(uuid, uuid, text, uuid) is
|
|
54
|
+
'Dormant service_role-only board-role grant chokepoint. Raises unless _grant_within_ceiling(grantor, board, grantee_role) holds (grantee-bounded), then sets board_memberships.role. Consumes the MBAUTHZ ceiling helper; adds no ceiling logic.';
|
|
55
|
+
|
|
56
|
+
-- Dormant: revoked from authenticated, granted to service_role only.
|
|
57
|
+
revoke all on function agent_board.grant_board_role(uuid, uuid, text, uuid) from public, anon, authenticated;
|
|
58
|
+
grant execute on function agent_board.grant_board_role(uuid, uuid, text, uuid) to service_role;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- MBSESSION SL-1 (IF-0-MBSESSION-1): board-native fleet_agent_session identity class + TTL pin.
|
|
2
|
+
--
|
|
3
|
+
-- Additive, forward-only. Does NOT edit the historical 20260710000400 bytes: the
|
|
4
|
+
-- inline CHECK Postgres named `nonfleet_sessions_identity_class_check` is replaced
|
|
5
|
+
-- here (drop-if-exists + re-add) with the 5th literal `fleet_agent_session` (the old
|
|
6
|
+
-- four classes still validate), and a new `nonfleet_sessions_fleet_ttl_pin` prevents a
|
|
7
|
+
-- fleet_agent_session row from inheriting the 3600 default (caps it at 900 seconds).
|
|
8
|
+
|
|
9
|
+
alter table agent_board.nonfleet_sessions
|
|
10
|
+
drop constraint if exists nonfleet_sessions_identity_class_check;
|
|
11
|
+
|
|
12
|
+
alter table agent_board.nonfleet_sessions
|
|
13
|
+
add constraint nonfleet_sessions_identity_class_check
|
|
14
|
+
check (identity_class in ('ephemeral_child', 'hosted_workload', 'remote_sandbox', 'invited_external', 'fleet_agent_session'));
|
|
15
|
+
|
|
16
|
+
alter table agent_board.nonfleet_sessions
|
|
17
|
+
drop constraint if exists nonfleet_sessions_fleet_ttl_pin;
|
|
18
|
+
|
|
19
|
+
alter table agent_board.nonfleet_sessions
|
|
20
|
+
add constraint nonfleet_sessions_fleet_ttl_pin
|
|
21
|
+
check (identity_class <> 'fleet_agent_session' or max_ttl_seconds <= 900);
|
|
22
|
+
|
|
23
|
+
comment on constraint nonfleet_sessions_fleet_ttl_pin on agent_board.nonfleet_sessions is
|
|
24
|
+
'Board-native fleet_agent_session sessions are pinned to a <= 900s TTL and cannot inherit the 3600 default.';
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
-- MBSESSION SL-1 (IF-0-MBSESSION-2 + IF-0-MBSESSION-3): whole-principal PoP session mint.
|
|
2
|
+
--
|
|
3
|
+
-- Additive, forward-only. Adds:
|
|
4
|
+
-- * agent_board._agent_principal_active(uuid) — stable recursive lineage walk that
|
|
5
|
+
-- returns true iff every agent from p_agent_id up to its lineage root is active.
|
|
6
|
+
-- * agent_board.mint_fleet_agent_session(...) — security definer, service_role-only;
|
|
7
|
+
-- re-validates the WHOLE principal (challenge + signing key + lineage) before it
|
|
8
|
+
-- consumes the challenge and inserts a fleet_agent_session nonfleet_sessions row.
|
|
9
|
+
-- Both live in this migration because the mint consumes the new identity_class from
|
|
10
|
+
-- 20260718000500 and calls _agent_principal_active.
|
|
11
|
+
|
|
12
|
+
-- IF-3: whole-principal liveness. Walk parent_agent_id from the seed up to the
|
|
13
|
+
-- lineage root (parent NULL or id = root_agent_id). True iff EVERY agent in the
|
|
14
|
+
-- chain is status = 'active'; a non-existent agent reduces to false; an unparented
|
|
15
|
+
-- agent reduces to its own status.
|
|
16
|
+
create or replace function agent_board._agent_principal_active(p_agent_id uuid)
|
|
17
|
+
returns boolean
|
|
18
|
+
language sql
|
|
19
|
+
stable
|
|
20
|
+
security definer
|
|
21
|
+
set search_path = ''
|
|
22
|
+
as $$
|
|
23
|
+
with recursive lineage(id, parent_agent_id, root_agent_id, status, depth) as (
|
|
24
|
+
select a.id, a.parent_agent_id, a.root_agent_id, a.status, 0
|
|
25
|
+
from agent_board.agents a
|
|
26
|
+
where a.id = p_agent_id
|
|
27
|
+
union all
|
|
28
|
+
select p.id, p.parent_agent_id, p.root_agent_id, p.status, l.depth + 1
|
|
29
|
+
from lineage l
|
|
30
|
+
join agent_board.agents p on p.id = l.parent_agent_id
|
|
31
|
+
where l.parent_agent_id is not null
|
|
32
|
+
and l.id is distinct from l.root_agent_id
|
|
33
|
+
and l.depth < 64
|
|
34
|
+
)
|
|
35
|
+
select coalesce(bool_and(status = 'active'), false)
|
|
36
|
+
from lineage;
|
|
37
|
+
$$;
|
|
38
|
+
|
|
39
|
+
comment on function agent_board._agent_principal_active(uuid) is
|
|
40
|
+
'True iff every agent in the lineage from the given agent up to its root is status = active. Refresh-checked revocation signal for fleet_agent_session minting.';
|
|
41
|
+
|
|
42
|
+
revoke all on function agent_board._agent_principal_active(uuid) from public, anon, authenticated;
|
|
43
|
+
grant execute on function agent_board._agent_principal_active(uuid) to service_role;
|
|
44
|
+
|
|
45
|
+
-- IF-2: whole-principal PoP session mint. service_role-only. Any failing clause
|
|
46
|
+
-- raises and mints no row.
|
|
47
|
+
create or replace function agent_board.mint_fleet_agent_session(
|
|
48
|
+
p_challenge_id uuid,
|
|
49
|
+
p_nonce text,
|
|
50
|
+
p_signing_key_id uuid,
|
|
51
|
+
p_agent_id uuid,
|
|
52
|
+
p_max_ttl_seconds integer default 900
|
|
53
|
+
)
|
|
54
|
+
returns table(session_id text, expires_at timestamptz)
|
|
55
|
+
language plpgsql
|
|
56
|
+
security definer
|
|
57
|
+
set search_path = agent_board, public
|
|
58
|
+
as $$
|
|
59
|
+
declare
|
|
60
|
+
v_challenge agent_board.agent_signing_key_challenges%rowtype;
|
|
61
|
+
v_key agent_board.agent_signing_keys%rowtype;
|
|
62
|
+
v_auth_user_id uuid;
|
|
63
|
+
v_parent_agent_id uuid;
|
|
64
|
+
v_ttl integer := least(coalesce(p_max_ttl_seconds, 900), 900);
|
|
65
|
+
v_session_id text := 'fas_' || replace(gen_random_uuid()::text, '-', '');
|
|
66
|
+
v_expires_at timestamptz;
|
|
67
|
+
begin
|
|
68
|
+
if v_ttl <= 0 then
|
|
69
|
+
raise exception 'max_ttl_seconds must be positive' using errcode = '22023';
|
|
70
|
+
end if;
|
|
71
|
+
|
|
72
|
+
-- (i) challenge: exists, bound to this agent + signing key, unconsumed, unexpired.
|
|
73
|
+
select * into v_challenge
|
|
74
|
+
from agent_board.agent_signing_key_challenges c
|
|
75
|
+
where c.id = p_challenge_id
|
|
76
|
+
for update;
|
|
77
|
+
if not found then
|
|
78
|
+
raise exception 'signing key challenge not found' using errcode = 'P0002';
|
|
79
|
+
end if;
|
|
80
|
+
if v_challenge.agent_id <> p_agent_id or v_challenge.signing_key_id <> p_signing_key_id then
|
|
81
|
+
raise exception 'actor_mismatch: challenge is not bound to this agent and signing key' using errcode = '42501';
|
|
82
|
+
end if;
|
|
83
|
+
if v_challenge.proof_state <> 'pending' or v_challenge.consumed_at is not null then
|
|
84
|
+
raise exception 'nonce_replay: signing key challenge already consumed' using errcode = '23505';
|
|
85
|
+
end if;
|
|
86
|
+
if v_challenge.expires_at <= now() then
|
|
87
|
+
raise exception 'timestamp_out_of_window: signing key challenge expired' using errcode = '22023';
|
|
88
|
+
end if;
|
|
89
|
+
-- #10 nonce binding (belt-and-suspenders on top of the challenge_id binding): the
|
|
90
|
+
-- caller must present the exact stored challenge_nonce for this challenge row.
|
|
91
|
+
if p_nonce is null or p_nonce <> v_challenge.challenge_nonce then
|
|
92
|
+
raise exception 'nonce_mismatch: presented nonce does not match the bound challenge' using errcode = '42501';
|
|
93
|
+
end if;
|
|
94
|
+
|
|
95
|
+
-- (ii) signing key: bound to this agent, active, unrevoked.
|
|
96
|
+
select * into v_key
|
|
97
|
+
from agent_board.agent_signing_keys ask
|
|
98
|
+
where ask.id = p_signing_key_id
|
|
99
|
+
and ask.agent_id = p_agent_id
|
|
100
|
+
for update;
|
|
101
|
+
if not found then
|
|
102
|
+
raise exception 'signing key not found for agent' using errcode = 'P0002';
|
|
103
|
+
end if;
|
|
104
|
+
if v_key.status <> 'active' or v_key.revoked_at is not null then
|
|
105
|
+
raise exception 'key_revoked: signing key is not active' using errcode = '42501';
|
|
106
|
+
end if;
|
|
107
|
+
-- #12 key expiry: today only the challenge RPC checks agent_signing_keys.expires_at;
|
|
108
|
+
-- the mint must reject an expired key as well.
|
|
109
|
+
if v_key.expires_at is not null and v_key.expires_at <= now() then
|
|
110
|
+
raise exception 'key_expired: signing key is past its expires_at' using errcode = '42501';
|
|
111
|
+
end if;
|
|
112
|
+
|
|
113
|
+
-- (iii) whole principal active (self + every ancestor up to lineage root).
|
|
114
|
+
if not agent_board._agent_principal_active(p_agent_id) then
|
|
115
|
+
raise exception 'principal_inactive: agent principal is not fully active' using errcode = '42501';
|
|
116
|
+
end if;
|
|
117
|
+
|
|
118
|
+
-- (iv) #4 governing delegation-grant liveness for root_minted children. A child
|
|
119
|
+
-- that holds a delegation_grants row may mint ONLY while that grant is unrevoked
|
|
120
|
+
-- and unexpired (grant_expires_at > now()). independent_auth principals hold no
|
|
121
|
+
-- grant row and skip this gate. This binds the hours-scale grant clock to the mint
|
|
122
|
+
-- so an expired/revoked grant cannot keep re-minting <=900s tokens indefinitely.
|
|
123
|
+
if exists (select 1 from agent_board.delegation_grants g where g.child_agent_id = p_agent_id) then
|
|
124
|
+
if not exists (
|
|
125
|
+
select 1
|
|
126
|
+
from agent_board.delegation_grants g
|
|
127
|
+
where g.child_agent_id = p_agent_id
|
|
128
|
+
and g.revoked_at is null
|
|
129
|
+
and g.grant_expires_at > now()
|
|
130
|
+
) then
|
|
131
|
+
raise exception 'grant_expired: the governing delegation grant is revoked or expired' using errcode = '42501';
|
|
132
|
+
end if;
|
|
133
|
+
end if;
|
|
134
|
+
|
|
135
|
+
-- Scope to the agent's authenticated principal. A fleet_agent_session must bind to
|
|
136
|
+
-- an auth-bound agent (no auth_user_id column on nonfleet_sessions; this is a mint
|
|
137
|
+
-- invariant flowed to the EF token sub, not a stored value).
|
|
138
|
+
select a.auth_user_id, a.parent_agent_id
|
|
139
|
+
into v_auth_user_id, v_parent_agent_id
|
|
140
|
+
from agent_board.agents a
|
|
141
|
+
where a.id = p_agent_id;
|
|
142
|
+
if v_auth_user_id is null then
|
|
143
|
+
raise exception 'agent has no auth_user_id principal to scope the session' using errcode = '42501';
|
|
144
|
+
end if;
|
|
145
|
+
|
|
146
|
+
-- Consume the challenge (one-time).
|
|
147
|
+
update agent_board.agent_signing_key_challenges
|
|
148
|
+
set proof_state = 'consumed',
|
|
149
|
+
consumed_at = now(),
|
|
150
|
+
updated_at = now()
|
|
151
|
+
where id = p_challenge_id;
|
|
152
|
+
|
|
153
|
+
v_expires_at := now() + make_interval(secs => v_ttl);
|
|
154
|
+
|
|
155
|
+
insert into agent_board.nonfleet_sessions(
|
|
156
|
+
id, session_id, identity_class, agent_id, parent_agent_id,
|
|
157
|
+
max_ttl_seconds, issued_at, expires_at
|
|
158
|
+
)
|
|
159
|
+
values (
|
|
160
|
+
v_session_id, v_session_id, 'fleet_agent_session', p_agent_id, v_parent_agent_id,
|
|
161
|
+
v_ttl, now(), v_expires_at
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return query select v_session_id, v_expires_at;
|
|
165
|
+
end;
|
|
166
|
+
$$;
|
|
167
|
+
|
|
168
|
+
comment on function agent_board.mint_fleet_agent_session(uuid, text, uuid, uuid, integer) is
|
|
169
|
+
'Whole-principal PoP session mint: re-validates challenge (incl. bound nonce) + active/unrevoked/unexpired signing key + full-lineage liveness + governing delegation-grant liveness, consumes the challenge, and inserts a <=900s fleet_agent_session. service_role-only.';
|
|
170
|
+
|
|
171
|
+
revoke all on function agent_board.mint_fleet_agent_session(uuid, text, uuid, uuid, integer) from public, anon, authenticated;
|
|
172
|
+
grant execute on function agent_board.mint_fleet_agent_session(uuid, text, uuid, uuid, integer) to service_role;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- MBSESSION SL-1 (IF-0-MBSESSION-6): expired nonfleet_sessions reaper.
|
|
2
|
+
--
|
|
3
|
+
-- Additive, forward-only. security definer, service_role-only. Deletes non-revoked
|
|
4
|
+
-- nonfleet_sessions rows whose expires_at is older than p_older_than (default 7 days)
|
|
5
|
+
-- and returns the affected count. Uses the partial idx_nonfleet_sessions_expires_at
|
|
6
|
+
-- (predicate: where revoked_at is null) — the WHERE clause matches that predicate so
|
|
7
|
+
-- the index is usable. Scheduling (pg_cron / external cadence) is a deferred owner;
|
|
8
|
+
-- the callable function is in-scope.
|
|
9
|
+
create or replace function agent_board.reap_expired_nonfleet_sessions(
|
|
10
|
+
p_older_than interval default interval '7 days'
|
|
11
|
+
)
|
|
12
|
+
returns bigint
|
|
13
|
+
language plpgsql
|
|
14
|
+
security definer
|
|
15
|
+
set search_path = agent_board, public
|
|
16
|
+
as $$
|
|
17
|
+
declare
|
|
18
|
+
v_count bigint;
|
|
19
|
+
begin
|
|
20
|
+
with deleted as (
|
|
21
|
+
delete from agent_board.nonfleet_sessions ns
|
|
22
|
+
where ns.revoked_at is null
|
|
23
|
+
and ns.expires_at < now() - coalesce(p_older_than, interval '7 days')
|
|
24
|
+
returning 1
|
|
25
|
+
)
|
|
26
|
+
select count(*) into v_count from deleted;
|
|
27
|
+
return v_count;
|
|
28
|
+
end;
|
|
29
|
+
$$;
|
|
30
|
+
|
|
31
|
+
comment on function agent_board.reap_expired_nonfleet_sessions(interval) is
|
|
32
|
+
'Deletes non-revoked nonfleet_sessions rows expired older than p_older_than (default 7 days); returns the affected count. service_role-only.';
|
|
33
|
+
|
|
34
|
+
revoke all on function agent_board.reap_expired_nonfleet_sessions(interval) from public, anon, authenticated;
|
|
35
|
+
grant execute on function agent_board.reap_expired_nonfleet_sessions(interval) to service_role;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- MBSESSION SL-1 (IF-0-MBSESSION-4 + IF-0-MBSESSION-8): scoped-send re-grant + schema_version bump.
|
|
2
|
+
--
|
|
3
|
+
-- Additive, forward-only. Reverses the 8 core send-RPC `revoke execute ... from
|
|
4
|
+
-- authenticated` staged at 20260711000100:224-231 by re-granting execute to
|
|
5
|
+
-- `authenticated` on the exact same 8 overload signatures (mirrored byte-for-byte).
|
|
6
|
+
-- Actor derivation is UNCHANGED (_resolved_actor_agent_id, current_agent_id()); the
|
|
7
|
+
-- MBSENDAUTHZ role-gate/telemetry bodies are preserved by Postgres across
|
|
8
|
+
-- create-or-replace, so this is a fresh grant only. The signed_* gateway RPCs revoked
|
|
9
|
+
-- at :217-222 stay service_role-only and are NOT touched here.
|
|
10
|
+
--
|
|
11
|
+
-- MBSESSION is the enforcement-switches-on point (the re-grant makes the authenticated
|
|
12
|
+
-- live path real), so schema_version bumps to 8.
|
|
13
|
+
|
|
14
|
+
grant execute on function agent_board.send_direct_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) to authenticated;
|
|
15
|
+
grant execute on function agent_board.send_direct_message(uuid, uuid, jsonb, text, text, text, text, text, text, jsonb) to authenticated;
|
|
16
|
+
grant execute on function agent_board.send_multicast_message(uuid, uuid, uuid[], text, text, text, text, text, jsonb) to authenticated;
|
|
17
|
+
grant execute on function agent_board.send_multicast_message(uuid, uuid, jsonb, text, text, text, text, text, text, jsonb) to authenticated;
|
|
18
|
+
grant execute on function agent_board.post_board_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) to authenticated;
|
|
19
|
+
grant execute on function agent_board.post_board_message(uuid, uuid, text, text, text, text, text, text, text, jsonb) to authenticated;
|
|
20
|
+
grant execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) to authenticated;
|
|
21
|
+
grant execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, jsonb, text, text, text, text, text, jsonb) to authenticated;
|
|
22
|
+
|
|
23
|
+
insert into agent_board.schema_version(version) values (8)
|
|
24
|
+
on conflict ((true)) do update set version = greatest(agent_board.schema_version.version, excluded.version);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- MBDELEG SL-1 (IF-0-MBDELEG-1): extend agent_board.agents.status with 'revoked'.
|
|
2
|
+
--
|
|
3
|
+
-- Additive, forward-only. Does NOT edit the historical 20260502000000 bytes: the
|
|
4
|
+
-- inline CHECK Postgres auto-named `agents_status_check` is replaced here
|
|
5
|
+
-- (drop-if-exists + re-add) with a 4th literal `revoked` (the existing three
|
|
6
|
+
-- classes still validate). Mirrors the `nonfleet_sessions_identity_class_check`
|
|
7
|
+
-- CHECK-extension precedent at 20260718000500.
|
|
8
|
+
--
|
|
9
|
+
-- Rationale: a cascade-killed principal (revoke_agent_subtree / revoke_human_subtree,
|
|
10
|
+
-- 20260718001100) must be forensically distinguishable from an operator-`'suspended'`
|
|
11
|
+
-- one and safely un-revokable, so a dedicated `'revoked'` literal is chosen over
|
|
12
|
+
-- reusing `'suspended'`. Every existing consumer treats any non-`active` value as
|
|
13
|
+
-- non-live (current_agent_id() filters status='active'; _agent_principal_active
|
|
14
|
+
-- requires status='active' for the whole lineage), so a 4th literal is safe.
|
|
15
|
+
|
|
16
|
+
alter table agent_board.agents
|
|
17
|
+
drop constraint if exists agents_status_check;
|
|
18
|
+
|
|
19
|
+
alter table agent_board.agents
|
|
20
|
+
add constraint agents_status_check
|
|
21
|
+
check (status in ('active', 'inactive', 'suspended', 'revoked'));
|
|
22
|
+
|
|
23
|
+
comment on constraint agents_status_check on agent_board.agents is
|
|
24
|
+
'Agent lifecycle status. ''revoked'' (MBDELEG) marks a cascade-killed principal, forensically distinct from operator-''suspended''; both are non-active and fail every liveness check.';
|