@consiliency/agent-board-schema 1.10.0 → 1.22.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 (109) hide show
  1. package/fixtures/already-applied.json +136 -0
  2. package/migrations/20260729000100_agent_board_fedsession_autojoin_default_board.sql +364 -0
  3. package/migrations/20260730000100_agent_board_gatenull_service_role_bypass.sql +369 -0
  4. package/migrations/20260730000200_agent_board_membstate_authz.sql +195 -0
  5. package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
  6. package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
  7. package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
  8. package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
  9. package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
  10. package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
  11. package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
  12. package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
  13. package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
  14. package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
  15. package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
  16. package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
  17. package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
  18. package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
  19. package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
  20. package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
  21. package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
  22. package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
  23. package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
  24. package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
  25. package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
  26. package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
  27. package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
  28. package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
  29. package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
  30. package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
  31. package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
  32. package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
  33. package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
  34. package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
  35. package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
  36. package/package.json +1 -1
  37. package/schema-manifest.json +309 -3
  38. package/src/tooling.test.mjs +123 -2
  39. package/tests/agent_board_actorspoof.test.sql +349 -0
  40. package/tests/agent_board_agentsdir.test.sql +154 -0
  41. package/tests/agent_board_authbound_boundary.test.sql +9 -1
  42. package/tests/agent_board_claimgate_enforce.test.sql +464 -0
  43. package/tests/agent_board_claimgate_predicate.test.sql +360 -0
  44. package/tests/agent_board_claimkey.test.sql +229 -0
  45. package/tests/agent_board_consent_backfill.test.sql +48 -0
  46. package/tests/agent_board_consent_emission.test.sql +601 -0
  47. package/tests/agent_board_consent_gating.test.sql +489 -0
  48. package/tests/agent_board_consent_substrate.test.sql +720 -0
  49. package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
  50. package/tests/agent_board_delegation_enforcement.test.sql +55 -6
  51. package/tests/agent_board_delegation_policies.test.sql +107 -1
  52. package/tests/agent_board_fedsession.test.sql +6 -4
  53. package/tests/agent_board_fedsession_autojoin.test.sql +181 -0
  54. package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
  55. package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
  56. package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
  57. package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
  58. package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
  59. package/tests/agent_board_fleet_policy.test.sql +2 -2
  60. package/tests/agent_board_gatenull_membstate.test.sql +352 -0
  61. package/tests/agent_board_grants_hygiene.test.sql +408 -0
  62. package/tests/agent_board_identity_providers.test.sql +9 -0
  63. package/tests/agent_board_inbox_reads.test.sql +29 -12
  64. package/tests/agent_board_inbox_rpcs.test.sql +44 -34
  65. package/tests/agent_board_invitepolicy.test.sql +205 -0
  66. package/tests/agent_board_leaveidem.test.sql +194 -0
  67. package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
  68. package/tests/agent_board_mint_harden.test.sql +303 -0
  69. package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
  70. package/tests/agent_board_obsgate.test.sql +178 -0
  71. package/tests/agent_board_obsgate2.test.sql +153 -0
  72. package/tests/agent_board_obsgate3.test.sql +170 -0
  73. package/tests/agent_board_obsgate4.test.sql +196 -0
  74. package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
  75. package/tests/agent_board_pubexec_class.test.sql +53 -0
  76. package/tests/agent_board_rejectsem.test.sql +271 -0
  77. package/tests/agent_board_resigning_refresh.test.sql +1 -1
  78. package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
  79. package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
  80. package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
  81. package/tests/agent_board_rolereset.test.sql +351 -0
  82. package/tests/agent_board_roomdeleg.test.sql +5 -3
  83. package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
  84. package/tests/agent_board_roomlink.test.sql +25 -9
  85. package/tests/agent_board_rowid.test.sql +1274 -0
  86. package/tests/agent_board_signature_rls_reads.test.sql +3 -3
  87. package/tests/agent_board_signed_write_gateway.test.sql +3 -3
  88. package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
  89. package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
  90. package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
  91. package/tests/agent_board_signing_store.test.sql +6 -6
  92. package/tests/fixtures/isolation-substrate.sql +93 -0
  93. package/tests/fixtures/test-prereqs.sql +33 -0
  94. package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
  95. package/tests/isolation/expected/claimkey_renew_race.out +38 -0
  96. package/tests/isolation/expected/consent_approval_lock.out +43 -0
  97. package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
  98. package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
  99. package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
  100. package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
  101. package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
  102. package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
  103. package/tests/isolation/specs/consent_approval_lock.spec +98 -0
  104. package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
  105. package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
  106. package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
  107. package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
  108. package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
  109. package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
@@ -0,0 +1,474 @@
1
+ -- =============================================================================
2
+ -- DELEGCAPCEILING (#142) — operator delegation caps are a DURABLE ceiling that a
3
+ -- self-service (authenticated) root cannot loosen.
4
+ -- =============================================================================
5
+ -- DEFECT (#142, surfaced by the #138 panel): upsert_delegation_policy
6
+ -- (20260820000100) UPDATES the root's latest-unrevoked policy row IN PLACE on the
7
+ -- authenticated self-service path, flipping scope_attestation -> 'self_declared' and
8
+ -- rewriting max_active_children/child_ttl_ceiling anywhere in the self-service clamp
9
+ -- range (up to 25 children / 24h). An operator_vetted row set to a DELIBERATELY tighter
10
+ -- cap (e.g. max_active_children=1) is therefore overwritten by a self-service widen to
11
+ -- 25 — the operator's intent is NOT durable. #138's mint re-clamp only honors the GLOBAL
12
+ -- self-service ceiling (25/24h), so an operator cap of 1 is lost the moment mint opens to
13
+ -- the authenticated (EF-forwarded) path.
14
+ --
15
+ -- MAINTAINER DECISION (locked): the operator's cap is a HARD CEILING. A root may still
16
+ -- self-service its own delegation policy, but the EFFECTIVE cap the mint enforces on the
17
+ -- self-service path is always the TIGHTER of (operator-set cap, root's self-set cap, the
18
+ -- global self-service ceiling 25/24h). A self-service write can never end up looser than
19
+ -- what an operator set. The operator (p_self_service=false) path keeps its full latitude.
20
+ --
21
+ -- MECHANISM (option 2(b): explicit operator-ceiling columns the mint clamps to). Chosen
22
+ -- over the two-row model (2(a)) because it preserves the existing single-"latest-unrevoked
23
+ -- -row" selection that BOTH upsert_delegation_policy and mint_delegated_child already use —
24
+ -- no reader has to learn to join a second row, and no ambiguity about which row is effective.
25
+ -- * Two NULLABLE columns hold the operator ceiling: operator_max_active_children,
26
+ -- operator_child_ttl_ceiling. NULL = "no operator ceiling" (the pre-#142 behavior).
27
+ -- * They are written ONLY on the operator (service_role) branch of upsert_delegation_policy
28
+ -- (= the operator's own caps). The self-service branch PRESERVES them (carries the
29
+ -- existing values forward on UPDATE; NULL on a first INSERT), so a self-service overwrite
30
+ -- can no longer destroy the operator ceiling. authenticated has NO direct table UPDATE
31
+ -- grant (20260717000400: only SELECT), so the definer function is the sole writer path and
32
+ -- the operator branch is the sole ceiling writer — "service_role-write-only" by construction.
33
+ -- * mint_delegated_child's self-service branch clamps to the TIGHTEST of the three. The mint
34
+ -- stays the LOAD-BEARING durable guard (#138 doctrine: treat the stored policy row as
35
+ -- advisory, re-clamp at mint); the ceiling columns make the operator's cap survive the
36
+ -- self-overwrite so the mint has something tighter than the global 25/24h to clamp to.
37
+ --
38
+ -- CATALOG ENUMERATION OF CAP CONSUMERS (CLAUDE.md rule #1) — "the mint clamp closes #142"
39
+ -- rests on the mint being the ONLY authenticated cap consumer. Every reader of
40
+ -- delegation_policies.max_active_children / .child_ttl_ceiling was enumerated:
41
+ -- * mint_delegated_child — has the p_self_service (EF-forwarded authenticated) path. CLAMPED here.
42
+ -- * renew_delegated_grant — reads child_ttl_ceiling but is service_role-only with NO
43
+ -- self-service parameter and NO EF caller (only mint-child EF exists).
44
+ -- Operator-only => operator latitude (invariant 5); no clamp needed.
45
+ -- * upsert_delegation_policy — the writer; already clamps its OWN self-service writes.
46
+ -- No authenticated-reachable path sets delegation_policies.revoked_at (only service_role holds
47
+ -- UPDATE), so a root cannot revoke its operator policy to escape into the ceiling-less INSERT
48
+ -- branch — the operator row stays the latest-unrevoked and the self-service path UPDATEs it.
49
+ --
50
+ -- require_child_signing_key (invariant 4) and max_depth are NOT modeled as ceiling columns:
51
+ -- the self-service branch already PINS require=true and max_depth=1 unconditionally (#59), so an
52
+ -- operator's require=true can never be relaxed by self-service — an operator-require column would
53
+ -- be unreachable dead surface (CLAUDE.md rule #2). The self-pin IS the durability mechanism, and
54
+ -- the suite pins it by mutating the pin.
55
+ --
56
+ -- Both functions recreated with CREATE OR REPLACE (signatures UNCHANGED, so the ACL + COMMENT
57
+ -- are preserved — no DROP, which would re-open EXECUTE-to-PUBLIC on a SECURITY DEFINER function).
58
+ -- Grants/revokes/comments restated anyway (#118 discipline). The mint's OPERATOR branch is left
59
+ -- byte-for-byte unchanged (invariant 5). Additive, forward-only; schema_version stays 8.
60
+ -- =============================================================================
61
+
62
+ -- ---------------------------------------------------------------------------
63
+ -- 1. Operator-ceiling columns. Nullable; NULL = no operator ceiling (pre-#142).
64
+ -- ---------------------------------------------------------------------------
65
+ alter table agent_board.delegation_policies
66
+ add column if not exists operator_max_active_children int,
67
+ add column if not exists operator_child_ttl_ceiling interval;
68
+
69
+ comment on column agent_board.delegation_policies.operator_max_active_children is
70
+ 'Operator (service_role) hard CEILING on max_active_children for the self-service mint path '
71
+ '(#142). Written ONLY by the operator branch of upsert_delegation_policy (= the operator''s own '
72
+ 'cap); the self-service branch PRESERVES it. NULL = no operator ceiling (bounded only by the '
73
+ 'global 25 self-service ceiling). mint_delegated_child clamps the self-service effective cap to '
74
+ 'least(self, this, 25). The operator (p_self_service=false) path ignores it (operator latitude).';
75
+
76
+ comment on column agent_board.delegation_policies.operator_child_ttl_ceiling is
77
+ 'Operator (service_role) hard CEILING on child_ttl_ceiling for the self-service mint path (#142). '
78
+ 'Written ONLY by the operator branch of upsert_delegation_policy; the self-service branch '
79
+ 'PRESERVES it. NULL = no operator ceiling (bounded only by the global 24h self-service ceiling). '
80
+ 'mint_delegated_child clamps the self-service TTL to least(self, this, 24h). Operator path ignores it.';
81
+
82
+ -- ---------------------------------------------------------------------------
83
+ -- 2. Backfill: every existing operator_vetted row was service_role-written, so its
84
+ -- current caps ARE the operator ceiling. self_declared rows carry no operator intent
85
+ -- (leave their ceiling NULL). Guarded on the NOT-NULL child_ttl_ceiling target so a
86
+ -- re-run is a no-op.
87
+ -- ---------------------------------------------------------------------------
88
+ update agent_board.delegation_policies
89
+ set operator_max_active_children = max_active_children,
90
+ operator_child_ttl_ceiling = child_ttl_ceiling
91
+ where scope_attestation = 'operator_vetted'
92
+ and operator_child_ttl_ceiling is null;
93
+
94
+ -- ---------------------------------------------------------------------------
95
+ -- 3. upsert_delegation_policy — recreated VERBATIM from 20260820000100 except: the
96
+ -- operator branch now ESTABLISHES the operator ceiling (= its own caps) and the
97
+ -- self-service branch PRESERVES the existing ceiling (never overwrites it). Signature
98
+ -- unchanged, so CREATE OR REPLACE keeps the ACL + comment (both restated below, #118).
99
+ -- ---------------------------------------------------------------------------
100
+ create or replace function agent_board.upsert_delegation_policy(
101
+ p_root_agent_id uuid,
102
+ p_mode text default 'root_minted',
103
+ p_allowed_scopes jsonb default null,
104
+ p_max_active_children int default null,
105
+ p_child_ttl_ceiling interval default null,
106
+ p_require_child_signing_key boolean default null
107
+ )
108
+ returns agent_board.delegation_policies
109
+ language plpgsql
110
+ security definer
111
+ set search_path to 'agent_board', 'public'
112
+ as $$
113
+ declare
114
+ v_actor uuid;
115
+ v_is_service boolean := agent_board._is_service_role();
116
+ v_attestation text;
117
+ v_mode text;
118
+ v_max_children int;
119
+ v_ttl interval;
120
+ v_require_key boolean;
121
+ -- #142: the operator ceiling to WRITE (operator path establishes it; self-service
122
+ -- preserves the existing values so a self-overwrite cannot destroy the operator's cap).
123
+ v_operator_max int;
124
+ v_operator_ttl interval;
125
+ v_existing agent_board.delegation_policies%rowtype;
126
+ v_policy agent_board.delegation_policies%rowtype;
127
+ begin
128
+ -- Actor bound to the SESSION: authenticated -> current_agent_id() and p_root must
129
+ -- match it; service_role -> the supplied p_root (create-on-behalf-of). The binding is
130
+ -- derived from the session, never trusted from the parameter (Lane A's coalesce lesson).
131
+ v_actor := agent_board._resolved_actor_agent_id(p_root_agent_id, 'p_root_agent_id');
132
+
133
+ if not exists (select 1 from agent_board.agents where id = v_actor) then
134
+ raise exception 'root agent not found' using errcode = 'P0002';
135
+ end if;
136
+
137
+ -- Serialize concurrent self-service writes for the SAME root. The find-then-branch
138
+ -- below takes `for update`, but a `for update` over ZERO rows locks nothing, so two
139
+ -- concurrent FIRST upserts could each insert an active policy row (same-actor, not a
140
+ -- cross-root escalation, but it makes latest-policy selection ambiguous). A per-root
141
+ -- transaction advisory lock is the mutex — dedicated namespace, no coupling to the
142
+ -- agents row other flows may lock.
143
+ perform pg_advisory_xact_lock(hashtext('agent_board.delegation_policy:' || v_actor::text));
144
+
145
+ if p_mode is not null and p_mode not in ('independent_auth', 'root_minted') then
146
+ raise exception 'invalid delegation policy mode' using errcode = '22023';
147
+ end if;
148
+ v_mode := coalesce(p_mode, 'root_minted');
149
+
150
+ if v_is_service then
151
+ -- OPERATOR path: trusted. Scopes are operator-vetted; caps as supplied (table
152
+ -- defaults apply for nulls).
153
+ v_attestation := 'operator_vetted';
154
+ v_max_children := p_max_active_children;
155
+ v_ttl := coalesce(p_child_ttl_ceiling, interval '8 hours');
156
+ v_require_key := coalesce(p_require_child_signing_key, true);
157
+ else
158
+ -- SELF-SERVICE path: dangerous fields are CLAMPED, not trusted, and the scopes are
159
+ -- accepted but STAMPED self_declared.
160
+ v_attestation := 'self_declared';
161
+ -- require_child_signing_key: PINNED true regardless of input.
162
+ v_require_key := true;
163
+ -- max_active_children: NON-NULL, bounded 1..25.
164
+ v_max_children := coalesce(p_max_active_children, 5);
165
+ if v_max_children < 1 or v_max_children > 25 then
166
+ raise exception 'max_active_children must be between 1 and 25 for self-service delegation policies' using errcode = '22023';
167
+ end if;
168
+ -- child_ttl_ceiling: positive, capped at 24h (default 8h).
169
+ v_ttl := coalesce(p_child_ttl_ceiling, interval '8 hours');
170
+ if v_ttl <= interval '0' then
171
+ raise exception 'child_ttl_ceiling must be positive' using errcode = '22023';
172
+ end if;
173
+ if v_ttl > interval '24 hours' then
174
+ raise exception 'child_ttl_ceiling may not exceed 24 hours for self-service delegation policies' using errcode = '22023';
175
+ end if;
176
+ end if;
177
+
178
+ -- Upsert the root's CURRENT active policy. There is no unique on root_agent_id and the
179
+ -- mint reads the latest unrevoked row, so find-then-branch under a row lock rather than
180
+ -- ON CONFLICT: a concurrent self-service write serializes here. max_depth is PINNED 1 on
181
+ -- both branches — the mint enforces it structurally, but pinning the stored field stops a
182
+ -- future depth-aware mint inheriting an inflated value.
183
+ select * into v_existing
184
+ from agent_board.delegation_policies dp
185
+ where dp.root_agent_id = v_actor
186
+ and dp.revoked_at is null
187
+ order by dp.created_at desc
188
+ limit 1
189
+ for update;
190
+
191
+ -- #142: resolve the operator ceiling to WRITE. Operator establishes it as its own caps;
192
+ -- self-service PRESERVES whatever the operator set (v_existing carries it; when `not found`
193
+ -- the rowtype fields are NULL => no operator ceiling on a first self-service INSERT).
194
+ if v_is_service then
195
+ v_operator_max := v_max_children;
196
+ v_operator_ttl := v_ttl;
197
+ else
198
+ v_operator_max := v_existing.operator_max_active_children;
199
+ v_operator_ttl := v_existing.operator_child_ttl_ceiling;
200
+ end if;
201
+
202
+ if found then
203
+ update agent_board.delegation_policies dp
204
+ set mode = v_mode,
205
+ max_depth = 1,
206
+ max_active_children = v_max_children,
207
+ allowed_scopes = coalesce(p_allowed_scopes, '[]'::jsonb),
208
+ child_ttl_ceiling = v_ttl,
209
+ require_child_signing_key = v_require_key,
210
+ scope_attestation = v_attestation,
211
+ operator_max_active_children = v_operator_max,
212
+ operator_child_ttl_ceiling = v_operator_ttl
213
+ where dp.id = v_existing.id
214
+ returning * into v_policy;
215
+ else
216
+ insert into agent_board.delegation_policies(
217
+ root_agent_id, mode, max_depth, max_active_children, allowed_scopes,
218
+ child_ttl_ceiling, require_child_signing_key, scope_attestation,
219
+ operator_max_active_children, operator_child_ttl_ceiling
220
+ )
221
+ values (
222
+ v_actor, v_mode, 1, v_max_children, coalesce(p_allowed_scopes, '[]'::jsonb),
223
+ v_ttl, v_require_key, v_attestation,
224
+ v_operator_max, v_operator_ttl
225
+ )
226
+ returning * into v_policy;
227
+ end if;
228
+
229
+ return v_policy;
230
+ end;
231
+ $$;
232
+
233
+ comment on function agent_board.upsert_delegation_policy(uuid, text, jsonb, int, interval, boolean) is
234
+ 'Self-service writer for a root''s delegation policy (ROOMDELEG-2 / #59; #142 durable operator '
235
+ 'ceiling). authenticated: actor bound root == current_agent_id(); scopes accepted but stamped '
236
+ 'scope_attestation=''self_declared'' (ADVISORY); require_child_signing_key pinned true, '
237
+ 'max_active_children clamped 1..25, child_ttl_ceiling clamped <=24h, max_depth pinned 1, and the '
238
+ 'operator_* ceiling columns are PRESERVED (never overwritten). service_role: create-on-behalf-of, '
239
+ 'operator_vetted, caps as supplied, and ESTABLISHES operator_max_active_children/'
240
+ 'operator_child_ttl_ceiling as the durable hard ceiling the self-service mint clamps to (#142). '
241
+ 'Upserts the root''s latest unrevoked policy row.';
242
+
243
+ revoke all on function agent_board.upsert_delegation_policy(uuid, text, jsonb, int, interval, boolean) from public, anon, authenticated;
244
+ grant execute on function agent_board.upsert_delegation_policy(uuid, text, jsonb, int, interval, boolean) to authenticated, service_role;
245
+
246
+ -- ---------------------------------------------------------------------------
247
+ -- 4. mint_delegated_child — recreated VERBATIM from 20260822000100 except the
248
+ -- SELF-SERVICE branch now also honors the operator ceiling (#142): the effective
249
+ -- max_active_children and TTL ceiling are the TIGHTEST of (self, operator ceiling,
250
+ -- global 25/24h). The OPERATOR branch is byte-for-byte unchanged (invariant 5).
251
+ -- Signature unchanged => CREATE OR REPLACE preserves ACL + comment (restated, #118).
252
+ -- ---------------------------------------------------------------------------
253
+ create or replace function agent_board.mint_delegated_child(
254
+ p_root_agent_id uuid,
255
+ p_child_slug text,
256
+ p_requested_scopes jsonb,
257
+ p_child_public_key text default null,
258
+ p_ttl interval default null,
259
+ p_self_service boolean default false
260
+ )
261
+ returns table(
262
+ child_agent_id uuid,
263
+ grant_expires_at timestamptz,
264
+ signing_key_id uuid
265
+ )
266
+ language plpgsql
267
+ security definer
268
+ set search_path = agent_board, public
269
+ as $$
270
+ declare
271
+ v_policy agent_board.delegation_policies%rowtype;
272
+ v_root agent_board.agents%rowtype;
273
+ v_slug text;
274
+ v_requested jsonb := coalesce(p_requested_scopes, '[]'::jsonb);
275
+ v_allowed jsonb;
276
+ v_active_children int;
277
+ v_child_auth_user_id uuid;
278
+ v_child_id uuid;
279
+ v_key_id uuid := null;
280
+ v_grant_expires_at timestamptz;
281
+ v_effective_max int;
282
+ v_ceiling interval;
283
+ -- Per-root provisioning rate cap for the authenticated (self-service) path.
284
+ v_rate_limit constant int := 25;
285
+ v_rate_window constant interval := interval '1 hour';
286
+ begin
287
+ if not agent_board._is_service_role() then
288
+ raise exception 'mint_delegated_child is service_role-only' using errcode = '42501';
289
+ end if;
290
+
291
+ -- Non-positive TTL floor: an immediately-expired grant escapes the
292
+ -- max_active_children count while still provisioning an auth user/agent/key.
293
+ if p_ttl is not null and p_ttl <= interval '0' then
294
+ raise exception 'non_positive_ttl: requested grant TTL must be positive' using errcode = '22023';
295
+ end if;
296
+
297
+ v_slug := agent_board._normalize_provision_slug(p_child_slug);
298
+
299
+ -- Root must exist.
300
+ select * into v_root from agent_board.agents where id = p_root_agent_id;
301
+ if not found then
302
+ raise exception 'root agent not found' using errcode = 'P0002';
303
+ end if;
304
+
305
+ -- Root policy must exist, be unrevoked, and be root_minted. #11: take a row lock
306
+ -- on the policy up front so a concurrent mint against the same root serializes
307
+ -- here — the max_active_children count->insert below cannot then be raced past
308
+ -- the ceiling (TOCTOU close).
309
+ select * into v_policy
310
+ from agent_board.delegation_policies dp
311
+ where dp.root_agent_id = p_root_agent_id
312
+ and dp.revoked_at is null
313
+ order by dp.created_at desc
314
+ limit 1
315
+ for update;
316
+ if not found then
317
+ raise exception 'no active delegation policy for root agent' using errcode = '42501';
318
+ end if;
319
+ if v_policy.mode <> 'root_minted' then
320
+ raise exception 'delegation policy mode is not root_minted (independent_auth children self-provision)' using errcode = '42501';
321
+ end if;
322
+
323
+ -- Root principal must be active (self + lineage).
324
+ if not agent_board._agent_principal_active(p_root_agent_id) then
325
+ raise exception 'principal_inactive: root agent principal is not fully active' using errcode = '42501';
326
+ end if;
327
+
328
+ -- max_depth = 1 / no transitive minting: p_root_agent_id must be a TRUE lineage
329
+ -- root (unparented) and must not itself be a delegated child.
330
+ if v_root.parent_agent_id is not null then
331
+ raise exception 'max_depth: only a true lineage root may mint (no transitive delegation)' using errcode = '42501';
332
+ end if;
333
+ if exists (select 1 from agent_board.delegation_grants g where g.child_agent_id = p_root_agent_id) then
334
+ raise exception 'max_depth: a delegated child may not mint further children' using errcode = '42501';
335
+ end if;
336
+
337
+ -- Scope subset: requested ⊆ allowed (JSON array containment). A NULL allowed_scopes
338
+ -- means the root carries no delegable scope, so only an empty request is a subset.
339
+ v_allowed := coalesce(v_policy.allowed_scopes, '[]'::jsonb);
340
+ if not (v_allowed @> v_requested) then
341
+ raise exception 'scope_exceeded: requested scopes are not a subset of the root allowed_scopes' using errcode = '42501';
342
+ end if;
343
+
344
+ -- max_active_children: the root's live (non-revoked, unexpired) grant count must
345
+ -- remain within the ceiling after this mint.
346
+ if p_self_service then
347
+ -- F1 (#138) + #142: never trust the stored caps on the authenticated (EF-forwarded)
348
+ -- path. A root can self-widen its own policy row, and its operator_vetted ceiling must
349
+ -- survive that overwrite, so the effective cap is the TIGHTEST of the root's self cap,
350
+ -- the operator ceiling (operator_max_active_children; NULL => no operator ceiling), and
351
+ -- the GLOBAL 25-child self-service ceiling. NULLs coalesce to 25 so they add no slack.
352
+ v_effective_max := least(
353
+ coalesce(v_policy.max_active_children, 25),
354
+ coalesce(v_policy.operator_max_active_children, 25),
355
+ 25);
356
+ select count(*) into v_active_children
357
+ from agent_board.delegation_grants g
358
+ where g.root_agent_id = p_root_agent_id
359
+ and g.revoked_at is null
360
+ and g.grant_expires_at > now();
361
+ if v_active_children >= v_effective_max then
362
+ raise exception 'max_active_children: root has reached its active delegated-child ceiling' using errcode = '42501';
363
+ end if;
364
+ elsif v_policy.max_active_children is not null then
365
+ -- Operator path: trusted caps as supplied (NULL = unbounded, operator's choice).
366
+ select count(*) into v_active_children
367
+ from agent_board.delegation_grants g
368
+ where g.root_agent_id = p_root_agent_id
369
+ and g.revoked_at is null
370
+ and g.grant_expires_at > now();
371
+ if v_active_children >= v_policy.max_active_children then
372
+ raise exception 'max_active_children: root has reached its active delegated-child ceiling' using errcode = '42501';
373
+ end if;
374
+ end if;
375
+
376
+ -- Per-root provisioning RATE cap (authenticated path only): bounds cumulative
377
+ -- mint->expire->mint fan-out. Enforced after the gates so only would-be-successful
378
+ -- mints consume budget, before any provisioning.
379
+ if p_self_service then
380
+ if not agent_board._check_and_bump_child_mint_rate(p_root_agent_id, v_rate_limit, v_rate_window) then
381
+ raise exception 'mint_rate_exceeded: root has reached its provisioning rate cap for the current window' using errcode = '42501';
382
+ end if;
383
+ end if;
384
+
385
+ -- Provision the child: parent = root = p_root_agent_id, same owner_user_id, own
386
+ -- auth binding (distinct auth_user_id for the active-unique index).
387
+ v_child_auth_user_id := agent_board._ensure_agent_auth_user(v_slug);
388
+
389
+ insert into agent_board.agents(
390
+ agent_type, kind, handle, display_name, status,
391
+ auth_user_id, owner_user_id, parent_agent_id, root_agent_id, metadata
392
+ )
393
+ values (
394
+ 'agent', 'agent'::agent_board.agent_kind, v_slug,
395
+ agent_board._default_provision_display_name(v_slug), 'active',
396
+ v_child_auth_user_id, v_root.owner_user_id, p_root_agent_id, p_root_agent_id,
397
+ jsonb_build_object('delegatedByRootAgentId', p_root_agent_id, 'grantedScopes', v_requested)
398
+ )
399
+ returning id into v_child_id;
400
+
401
+ -- Scoped REVOCABLE signing key when the policy requires it. #5: the child's
402
+ -- public key MUST be supplied by the caller (generated client-side; the private
403
+ -- key never touches the DB). We NEVER seed a placeholder/zero key and NEVER
404
+ -- generate a key in-DB. status='active' + proof_state='proven' so the child can
405
+ -- immediately re-sign with the private key it already holds. The fingerprint is
406
+ -- derived over the RAW 32 bytes via _raw_key_fingerprint (single-sourced with enroll;
407
+ -- rejects a non-canonical alias) so the same key hashes identically across writers.
408
+ if v_policy.require_child_signing_key then
409
+ if p_child_public_key is null then
410
+ raise exception 'a child public key is required when the delegation policy requires a signing key' using errcode = '22023';
411
+ end if;
412
+ -- Raw Ed25519 public keys are 32 bytes => 43 base64url chars (no padding).
413
+ if p_child_public_key !~ '^[A-Za-z0-9_-]{43}$' then
414
+ raise exception 'malformed child public key: expected a 43-char base64url raw Ed25519 key' using errcode = '22023';
415
+ end if;
416
+ -- Reject the all-''A'' key: 43 base64url ''A''s decode to 32 zero bytes (a
417
+ -- small-order point), which is exactly the retired placeholder.
418
+ if p_child_public_key = repeat('A', 43) then
419
+ raise exception 'child public key is the all-zero small-order placeholder and is rejected' using errcode = '22023';
420
+ end if;
421
+
422
+ insert into agent_board.agent_signing_keys(
423
+ id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint,
424
+ status, proof_state, metadata
425
+ )
426
+ values (
427
+ gen_random_uuid(), v_child_id, 'delegated-ephemeral', 1, p_child_public_key,
428
+ 'raw_ed25519_base64url',
429
+ agent_board._raw_key_fingerprint(p_child_public_key),
430
+ 'active', 'proven',
431
+ jsonb_build_object('ephemeral', true, 'scopes', v_requested)
432
+ )
433
+ returning id into v_key_id;
434
+ end if;
435
+
436
+ -- Grant row: ceiling-bounded hours-scale working clock. On the authenticated path the
437
+ -- TTL ceiling is re-clamped to the TIGHTEST of the stored self ceiling, the operator
438
+ -- ceiling (operator_child_ttl_ceiling; NULL => none), and the global 24h (#138 F1 + #142) —
439
+ -- the stored ceiling is not trusted. The operator path honors the stored ceiling verbatim.
440
+ if p_self_service then
441
+ v_ceiling := least(
442
+ coalesce(v_policy.child_ttl_ceiling, interval '24 hours'),
443
+ coalesce(v_policy.operator_child_ttl_ceiling, interval '24 hours'),
444
+ interval '24 hours');
445
+ else
446
+ v_ceiling := v_policy.child_ttl_ceiling;
447
+ end if;
448
+ v_grant_expires_at := now() + least(coalesce(p_ttl, v_ceiling), v_ceiling);
449
+
450
+ insert into agent_board.delegation_grants(
451
+ child_agent_id, root_agent_id, parent_agent_id, delegation_policy_id,
452
+ granted_scopes, grant_expires_at
453
+ )
454
+ values (
455
+ v_child_id, p_root_agent_id, p_root_agent_id, v_policy.id,
456
+ v_requested, v_grant_expires_at
457
+ );
458
+
459
+ return query select v_child_id, v_grant_expires_at, v_key_id;
460
+ end;
461
+ $$;
462
+
463
+ comment on function agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean) is
464
+ 'Mints a bounded root_minted delegated child (scope ⊆ root allowed_scopes, max_depth=1, '
465
+ 'policy-row-locked max_active_children, positive ceiling-bounded grant TTL) enrolling the '
466
+ 'CALLER-SUPPLIED Ed25519 public key with a RAW-byte fingerprint (_raw_key_fingerprint; '
467
+ 'private key stays client-side). service_role-only. p_self_service marks the EF-forwarded '
468
+ 'authenticated path: it re-clamps max_active_children and the TTL ceiling to the TIGHTEST of '
469
+ '(root self cap, operator ceiling operator_*/#142, global 25/24h) — stored caps are NOT trusted '
470
+ 'and an operator ceiling survives a self-widen — and applies a per-root provisioning rate cap. '
471
+ 'The operator (p_self_service=false) path keeps its latitude. Direct authenticated EXECUTE stays revoked.';
472
+
473
+ revoke all on function agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean) from public, anon, authenticated;
474
+ grant execute on function agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean) to service_role;