@consiliency/agent-board-schema 1.12.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.
- package/fixtures/already-applied.json +124 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/package.json +1 -1
- package/schema-manifest.json +282 -3
- package/src/tooling.test.mjs +113 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_fedsession.test.sql +6 -4
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +29 -12
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +25 -9
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- 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;
|