@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.
Files changed (64) hide show
  1. package/bin/agent-board-schema.js +0 -0
  2. package/fixtures/already-applied.json +108 -0
  3. package/migrations/20260714000200_agent_board_workload_providers_rls.sql +35 -0
  4. package/migrations/20260717000100_agent_board_board_role.sql +180 -0
  5. package/migrations/20260717000200_agent_board_thread_acl.sql +149 -0
  6. package/migrations/20260717000300_agent_board_agent_lineage.sql +54 -0
  7. package/migrations/20260717000400_agent_board_delegation_policies.sql +175 -0
  8. package/migrations/20260718000100_agent_board_send_authz_config.sql +172 -0
  9. package/migrations/20260718000200_agent_board_send_role_gate.sql +804 -0
  10. package/migrations/20260718000300_agent_board_fleet_policy_default_deny.sql +192 -0
  11. package/migrations/20260718000400_agent_board_grant_ceiling_wiring.sql +58 -0
  12. package/migrations/20260718000500_agent_board_fleet_agent_session_class.sql +24 -0
  13. package/migrations/20260718000600_agent_board_mint_fleet_agent_session.sql +172 -0
  14. package/migrations/20260718000700_agent_board_nonfleet_session_reaper.sql +35 -0
  15. package/migrations/20260718000800_agent_board_regrant_scoped_send.sql +24 -0
  16. package/migrations/20260718000900_agent_board_agents_status_revoked.sql +24 -0
  17. package/migrations/20260718001000_agent_board_delegation_enforcement.sql +296 -0
  18. package/migrations/20260718001100_agent_board_revoke_agent_subtree.sql +238 -0
  19. package/migrations/20260718001200_agent_board_resigning_refresh.sql +129 -0
  20. package/migrations/20260718001300_agent_board_identity_providers.sql +70 -0
  21. package/migrations/20260718001400_agent_board_enrollment_federation_audit.sql +50 -0
  22. package/migrations/20260718001500_agent_board_enroll_federated_identity.sql +220 -0
  23. package/migrations/20260719000100_agent_board_signing_challenge_pending_cap.sql +124 -0
  24. package/migrations/20260719000200_agent_board_signing_challenge_reaper.sql +34 -0
  25. package/migrations/20260719000300_agent_board_enroll_rate_cap.sql +72 -0
  26. package/migrations/20260719001000_agent_board_identity_providers_portal_contract.sql +38 -0
  27. package/migrations/20260719001100_agent_board_enroll_federated_identity_pop.sql +267 -0
  28. package/migrations/20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql +296 -0
  29. package/migrations/20260719001300_agent_board_enroll_rate_events_reaper_rls.sql +62 -0
  30. package/package.json +9 -9
  31. package/schema-manifest.json +247 -4
  32. package/src/tooling.test.mjs +42 -1
  33. package/tests/agent_board_agent_lineage.test.sql +73 -0
  34. package/tests/agent_board_agents_status_revoked.test.sql +60 -0
  35. package/tests/agent_board_auth_rpcs.test.sql +17 -16
  36. package/tests/agent_board_board_role.test.sql +129 -0
  37. package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
  38. package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
  39. package/tests/agent_board_delegation_enforcement.test.sql +175 -0
  40. package/tests/agent_board_delegation_policies.test.sql +119 -0
  41. package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
  42. package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
  43. package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
  44. package/tests/agent_board_enrollment_audit_content.test.sql +134 -0
  45. package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
  46. package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
  47. package/tests/agent_board_fleet_registry.test.sql +2 -2
  48. package/tests/agent_board_identity_providers.test.sql +104 -0
  49. package/tests/agent_board_inbox_rpcs.test.sql +6 -6
  50. package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
  51. package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
  52. package/tests/agent_board_portal_offline_invariant.test.sql +130 -0
  53. package/tests/agent_board_resigning_refresh.test.sql +73 -0
  54. package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
  55. package/tests/agent_board_schema_version.test.sql +2 -2
  56. package/tests/agent_board_schema_version_rpc.test.sql +1 -1
  57. package/tests/agent_board_scoped_send_regrant.test.sql +138 -0
  58. package/tests/agent_board_send_authz_config.test.sql +84 -0
  59. package/tests/agent_board_send_authz_matrix.test.sql +159 -0
  60. package/tests/agent_board_send_role_gate.test.sql +177 -0
  61. package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
  62. package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
  63. package/tests/agent_board_thread_acl.test.sql +129 -0
  64. 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.';