@consiliency/agent-board-schema 1.12.0 → 1.23.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 +160 -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/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
- package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
- package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
- package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
- package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
- package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
- package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
- package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
- package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
- package/package.json +1 -1
- package/schema-manifest.json +363 -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_atomic_fleet_onboarding.test.sql +623 -0
- package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -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_enroll_federated_identity.test.sql +3 -3
- package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
- package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
- package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
- package/tests/agent_board_fedsession.test.sql +51 -5
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
- 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 +45 -31
- 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_outbox_wake_ordering.test.sql +452 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +14 -3
- 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 +27 -11
- 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/agent_board_wake_recovery.test.sql +756 -0
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
- package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
- package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
- package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
- package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
- package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
- package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
- package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
- package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
- package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -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/enroll_identity_board_owner_lock_order.out +55 -0
- package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
- package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
- package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
- package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
- package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
- package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -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/enroll_identity_board_owner_lock_order.spec +117 -0
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
- package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -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
- package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- #132 — fleet_machines authenticated SELECT policy: fix the dead binding arm.
|
|
2
|
+
--
|
|
3
|
+
-- The policy `agent_board_authenticated_select_fleet_machines` gated its EXISTS subquery on
|
|
4
|
+
-- `fma.machine_id = fma.id` — a fleet_machine_agents SELF-comparison (machine_id FKs
|
|
5
|
+
-- fleet_machines.id; fma.id is the junction PK), so it is NEVER true and a bound agent could
|
|
6
|
+
-- never SELECT its own machine via the binding arm (only the owner arm worked).
|
|
7
|
+
--
|
|
8
|
+
-- The naive fix (`fma.machine_id = fleet_machines.id`) INFINITELY RECURSES: fleet_machine_agents'
|
|
9
|
+
-- own SELECT policy references fleet_machines back, so a correlated subquery over the binding
|
|
10
|
+
-- table re-enters this policy. (The original never recursed only because its dead comparison
|
|
11
|
+
-- meant the subquery never evaluated against real rows.) Break the cycle with a SECURITY DEFINER
|
|
12
|
+
-- helper that reads the binding table with RLS bypassed — the same technique the other
|
|
13
|
+
-- policy-helper predicates use.
|
|
14
|
+
--
|
|
15
|
+
-- Additive, forward-only; policy + internal helper. schema_version stays 8.
|
|
16
|
+
|
|
17
|
+
create or replace function agent_board._current_agent_bound_to_machine(p_machine_id uuid)
|
|
18
|
+
returns boolean
|
|
19
|
+
language sql
|
|
20
|
+
stable
|
|
21
|
+
security definer
|
|
22
|
+
set search_path = ''
|
|
23
|
+
as $$
|
|
24
|
+
select exists (
|
|
25
|
+
select 1
|
|
26
|
+
from agent_board.fleet_machine_agents fma
|
|
27
|
+
where fma.machine_id = p_machine_id
|
|
28
|
+
and fma.agent_id = agent_board.current_agent_id()
|
|
29
|
+
and fma.lifecycle_state = 'active'
|
|
30
|
+
)
|
|
31
|
+
$$;
|
|
32
|
+
comment on function agent_board._current_agent_bound_to_machine(uuid) is
|
|
33
|
+
'#132: does the current agent hold an ACTIVE fleet_machine_agents binding on this machine? SECURITY DEFINER so the fleet_machines SELECT policy can consult the binding table without recursing into its RLS (which references fleet_machines back). Session-derived; PUBLIC EXECUTE revoked.';
|
|
34
|
+
revoke all on function agent_board._current_agent_bound_to_machine(uuid) from public, anon;
|
|
35
|
+
grant execute on function agent_board._current_agent_bound_to_machine(uuid) to authenticated, service_role;
|
|
36
|
+
|
|
37
|
+
drop policy if exists agent_board_authenticated_select_fleet_machines on agent_board.fleet_machines;
|
|
38
|
+
create policy agent_board_authenticated_select_fleet_machines on agent_board.fleet_machines
|
|
39
|
+
as permissive for select to authenticated
|
|
40
|
+
using (
|
|
41
|
+
owner_agent_id = agent_board.current_agent_id()
|
|
42
|
+
or agent_board._current_agent_bound_to_machine(id)
|
|
43
|
+
);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
-- MBCONSENT resolve/request no-op polish (#116, Fable #114 Part B).
|
|
2
|
+
--
|
|
3
|
+
-- B1 — approve-no-op left the pending row dangling. When an owner "approves" a subject who is
|
|
4
|
+
-- ALREADY active, resolve_board_request returns the active row untouched (the Option-1 no-op) but
|
|
5
|
+
-- did not retire the redundant requested/invited row, so pending listings kept showing a request
|
|
6
|
+
-- the owner believes resolved (only a reject cleared it). Fix: retire the redundant pending row to
|
|
7
|
+
-- 'left' (recoverable) in the no-op path, routed through the emitting sink so the write hits the
|
|
8
|
+
-- ledger (the emission-completeness invariant). The emitted event is board-authored (actor !=
|
|
9
|
+
-- subject) and therefore NEUTRAL for consent — it records the pending request's retirement without
|
|
10
|
+
-- touching the subject's consent-present status, which the untouched active row carries. B4
|
|
11
|
+
-- (target state / p_metadata ignored on the no-op path)
|
|
12
|
+
-- and B2 (with two pending rows only the newest resolves) are documented in-place — behaviour
|
|
13
|
+
-- unchanged, deliberately.
|
|
14
|
+
--
|
|
15
|
+
-- CREATE OR REPLACE with the same body plus the B1 retire; grants re-asserted. schema_version
|
|
16
|
+
-- stays 8. Additive, forward-only.
|
|
17
|
+
|
|
18
|
+
create or replace function agent_board.resolve_board_request(
|
|
19
|
+
p_board_id uuid,
|
|
20
|
+
p_agent_id uuid,
|
|
21
|
+
p_membership_state text,
|
|
22
|
+
p_created_by_agent_id uuid,
|
|
23
|
+
p_metadata jsonb default '{}'::jsonb
|
|
24
|
+
)
|
|
25
|
+
returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, left_at timestamp with time zone, updated_at timestamp with time zone)
|
|
26
|
+
language plpgsql
|
|
27
|
+
security definer
|
|
28
|
+
set search_path to 'agent_board', 'public'
|
|
29
|
+
as $function$
|
|
30
|
+
declare
|
|
31
|
+
v_effective_state text;
|
|
32
|
+
v_board agent_board.boards%rowtype;
|
|
33
|
+
v_membership_id uuid;
|
|
34
|
+
v_active_id uuid;
|
|
35
|
+
v_membership agent_board.board_memberships%rowtype;
|
|
36
|
+
v_actor uuid := agent_board.current_agent_id();
|
|
37
|
+
begin
|
|
38
|
+
if v_actor is null and not agent_board._is_service_role() then
|
|
39
|
+
raise exception 'current agent principal not resolved' using errcode = '42501';
|
|
40
|
+
end if;
|
|
41
|
+
|
|
42
|
+
if p_membership_state not in ('joined', 'subscribed', 'removed') then
|
|
43
|
+
raise exception 'resolve_board_request only accepts joined, subscribed, or removed' using errcode = '22023';
|
|
44
|
+
end if;
|
|
45
|
+
select * into v_board from agent_board.boards where id = p_board_id and status = 'active';
|
|
46
|
+
if v_board.id is null then
|
|
47
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
48
|
+
end if;
|
|
49
|
+
if v_actor is not null then
|
|
50
|
+
if v_board.role_gate_enforced then
|
|
51
|
+
if not exists (
|
|
52
|
+
select 1
|
|
53
|
+
from agent_board.board_memberships bm
|
|
54
|
+
where bm.board_id = p_board_id
|
|
55
|
+
and bm.agent_id = v_actor
|
|
56
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
57
|
+
and bm.role in ('owner', 'admin')
|
|
58
|
+
) then
|
|
59
|
+
raise exception 'board request resolution requires owner or admin role' using errcode = '42501';
|
|
60
|
+
end if;
|
|
61
|
+
elsif v_actor <> v_board.created_by_agent_id then
|
|
62
|
+
if not exists (
|
|
63
|
+
select 1
|
|
64
|
+
from agent_board.board_memberships bm
|
|
65
|
+
where bm.board_id = p_board_id
|
|
66
|
+
and bm.agent_id = v_actor
|
|
67
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
68
|
+
) then
|
|
69
|
+
raise exception 'board request resolution requires active membership' using errcode = '42501';
|
|
70
|
+
end if;
|
|
71
|
+
end if;
|
|
72
|
+
end if;
|
|
73
|
+
|
|
74
|
+
-- Lock the WHOLE (board, agent) set first so a concurrent self-withdrawal on ANY physical row
|
|
75
|
+
-- serialises behind this resolution (#109's race is per-(board,agent), not per-row).
|
|
76
|
+
perform 1
|
|
77
|
+
from agent_board.board_memberships bm
|
|
78
|
+
where bm.board_id = p_board_id
|
|
79
|
+
and bm.agent_id = p_agent_id
|
|
80
|
+
order by bm.id
|
|
81
|
+
for update;
|
|
82
|
+
|
|
83
|
+
-- Select the PENDING row explicitly (not the total-order winner). B2 (#116): with TWO pending
|
|
84
|
+
-- rows (a requested AND an invited) for one subject this resolves the NEWEST; the older dangles.
|
|
85
|
+
-- Inherent to the multi-row model — resolution is one act on one row.
|
|
86
|
+
select bm.id
|
|
87
|
+
into v_membership_id
|
|
88
|
+
from agent_board.board_memberships bm
|
|
89
|
+
where bm.board_id = p_board_id
|
|
90
|
+
and bm.agent_id = p_agent_id
|
|
91
|
+
and bm.membership_state in ('requested', 'invited')
|
|
92
|
+
order by bm.created_at desc, bm.id desc
|
|
93
|
+
limit 1;
|
|
94
|
+
|
|
95
|
+
select bm.id
|
|
96
|
+
into v_active_id
|
|
97
|
+
from agent_board.board_memberships bm
|
|
98
|
+
where bm.board_id = p_board_id
|
|
99
|
+
and bm.agent_id = p_agent_id
|
|
100
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
101
|
+
order by bm.created_at desc, bm.id desc
|
|
102
|
+
limit 1;
|
|
103
|
+
|
|
104
|
+
if v_membership_id is null then
|
|
105
|
+
raise exception 'pending board membership not found' using errcode = 'P0002';
|
|
106
|
+
end if;
|
|
107
|
+
|
|
108
|
+
-- OPTION 1 FALLBACK (#109 A3): approving a subject who is ALREADY active is a no-op success,
|
|
109
|
+
-- not a promotion of the pending row into collision with the active one. Return the active row
|
|
110
|
+
-- untouched. Rejection still falls through to act on the pending row.
|
|
111
|
+
--
|
|
112
|
+
-- B4 (#116): the no-op path intentionally IGNORES the requested target state and p_metadata —
|
|
113
|
+
-- approving with target 'subscribed' a 'joined' active row returns 'joined' (a self-toggle
|
|
114
|
+
-- belongs to update_board_membership, which is where the old path coupled the metadata write to
|
|
115
|
+
-- a demotion). Nothing legitimate is lost.
|
|
116
|
+
if p_membership_state in ('joined', 'subscribed') and v_active_id is not null then
|
|
117
|
+
-- B1 (#116): retire the redundant pending row so it stops showing in pending listings. The
|
|
118
|
+
-- subject is already active via v_active_id (untouched); route the retire through the sink so
|
|
119
|
+
-- the write EMITS (the emission-completeness invariant requires every membership write to hit
|
|
120
|
+
-- the ledger) — a board-authored 'left' event, which is NEUTRAL for consent (not the subject's
|
|
121
|
+
-- own act) and does not affect the subject's consent-present status carried by the active row.
|
|
122
|
+
-- 'left' is recoverable. Previously only a reject cleared this, leaving an owner-visible
|
|
123
|
+
-- dangling request after a no-op approve.
|
|
124
|
+
perform agent_board._upsert_board_membership_state(
|
|
125
|
+
p_board_id, p_agent_id, 'left', p_created_by_agent_id, p_metadata, v_membership_id, 'noop_superseded'
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return query
|
|
129
|
+
select bm.id, bm.board_id, bm.agent_id, bm.membership_state, bm.left_at, bm.updated_at
|
|
130
|
+
from agent_board.board_memberships bm
|
|
131
|
+
where bm.id = v_active_id;
|
|
132
|
+
return;
|
|
133
|
+
end if;
|
|
134
|
+
|
|
135
|
+
-- REJECTSEM (#89): a rejection is NOT an eviction — resolve only acts on a PENDING row, so a
|
|
136
|
+
-- refusal lands on 'left' (recoverable), never 'removed' (which bars self-re-entry). Applied to
|
|
137
|
+
-- both requested and invited origins.
|
|
138
|
+
v_effective_state := case when p_membership_state = 'removed' then 'left'
|
|
139
|
+
else p_membership_state end;
|
|
140
|
+
|
|
141
|
+
-- #82: promotion into an active state requires the subject's own consent act (the sole
|
|
142
|
+
-- consent-predicate read site). Authenticated only; service_role keeps the operator carve-out.
|
|
143
|
+
if v_actor is not null
|
|
144
|
+
and v_effective_state in ('joined', 'subscribed')
|
|
145
|
+
and not agent_board._membership_consent_present(p_board_id, p_agent_id) then
|
|
146
|
+
raise exception 'admitting % requires an act by that agent', p_agent_id
|
|
147
|
+
using errcode = '42501';
|
|
148
|
+
end if;
|
|
149
|
+
|
|
150
|
+
v_membership := agent_board._upsert_board_membership_state(
|
|
151
|
+
p_board_id,
|
|
152
|
+
p_agent_id,
|
|
153
|
+
v_effective_state,
|
|
154
|
+
p_created_by_agent_id,
|
|
155
|
+
p_metadata,
|
|
156
|
+
v_membership_id,
|
|
157
|
+
case when p_membership_state = 'removed' then 'reject_withdrawn' else null end
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return query
|
|
161
|
+
select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.left_at, v_membership.updated_at;
|
|
162
|
+
end;
|
|
163
|
+
$function$;
|
|
164
|
+
|
|
165
|
+
revoke all on function agent_board.resolve_board_request(uuid, uuid, text, uuid, jsonb) from public, anon;
|
|
166
|
+
grant execute on function agent_board.resolve_board_request(uuid, uuid, text, uuid, jsonb) to authenticated, service_role;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- ROOMDELEG-2 / Lane B (#59) — self-service delegation policy writer for an
|
|
3
|
+
-- authenticated human-root (partial: policy writer only; minter deferred).
|
|
4
|
+
-- =============================================================================
|
|
5
|
+
-- Lane A (#57) shipped owner-gated set_board_role_gate. Lane B opens self-service
|
|
6
|
+
-- delegation to an authenticated human-agent root. This migration ships the SAFE half —
|
|
7
|
+
-- the policy writer — and DEFERS opening the minter (section 3 explains why):
|
|
8
|
+
--
|
|
9
|
+
-- upsert_delegation_policy (NEW) — write the root's own policy. SHIPS HERE.
|
|
10
|
+
-- mint_delegated_child (gate widened) — DEFERRED to #138 (needs child-key PoP +
|
|
11
|
+
-- raw-byte fingerprint first; see section 3).
|
|
12
|
+
--
|
|
13
|
+
-- BLOCKING DECISION (4-seat panel, ratified): there is NO `_grant_within_ceiling`-analog
|
|
14
|
+
-- for delegation SCOPES, and building one would be worse than not having it. A catalog
|
|
15
|
+
-- enumeration of every reader of delegation scope found that NO resource gate consumes
|
|
16
|
+
-- `allowed_scopes`: mint reads only grant liveness; the obsgate authoring/task/claims
|
|
17
|
+
-- gates and the send-authz layer gate on board membership/role and fleet origin, never
|
|
18
|
+
-- on granted scope. So `allowed_scopes` is ADVISORY — a scope-ceiling would cap a value
|
|
19
|
+
-- with zero enforcement effect, making mint's `requested ⊆ allowed_scopes` guard LOOK
|
|
20
|
+
-- non-vacuous while pinning nothing (the "succeeds where it should not" failure class).
|
|
21
|
+
-- Real capability is gated downstream: a child with self-declared scopes `["*"]` can do
|
|
22
|
+
-- nothing until an authority ADMITS it to a board (bounded by _grant_within_ceiling and
|
|
23
|
+
-- the board's ceiling), its session stays live, and its signing key is unrevoked.
|
|
24
|
+
--
|
|
25
|
+
-- Keeping "advisory" HONEST rather than a latent landmine: the frozen scope model
|
|
26
|
+
-- (20260718001000) declares a root's scope IS `allowed_scopes`, so a future phase may
|
|
27
|
+
-- start consuming it — at which point rows self-declared under this regime would silently
|
|
28
|
+
-- become load-bearing. Defuse it STRUCTURALLY with a provenance column: the authenticated
|
|
29
|
+
-- self-service path always stamps `scope_attestation='self_declared'` (session-derived,
|
|
30
|
+
-- never caller-supplied — the same doctrine as event provenance). Any future consumer
|
|
31
|
+
-- MUST require `operator_vetted` or apply a real ceiling at consumption time.
|
|
32
|
+
--
|
|
33
|
+
-- The fields that DO have teeth are hard-clamped on the self-service path (a root could
|
|
34
|
+
-- otherwise self-set keyless children / unbounded fan-out / an inflated TTL):
|
|
35
|
+
-- require_child_signing_key : pinned true (keyless children erode signed-write evidence)
|
|
36
|
+
-- max_active_children : non-null, 1..25 (each mint provisions an auth.users row)
|
|
37
|
+
-- child_ttl_ceiling : positive, <= 24h
|
|
38
|
+
-- max_depth : pinned 1 (no transitive minting; mint also enforces it)
|
|
39
|
+
-- Actor binding: root == current_agent_id() via _resolved_actor_agent_id (strict for v1;
|
|
40
|
+
-- owner-parity widening is compatible later, a contraction would not be). service_role
|
|
41
|
+
-- keeps the operator carve-out (trusted; scopes operator_vetted; caps as supplied).
|
|
42
|
+
--
|
|
43
|
+
-- schema_version stays 8: additive (one column + one new function + one gate widening).
|
|
44
|
+
-- =============================================================================
|
|
45
|
+
|
|
46
|
+
-- ---------------------------------------------------------------------------
|
|
47
|
+
-- 1. Scope provenance column. Existing rows were all service_role-written, so the
|
|
48
|
+
-- `default 'operator_vetted'` backfills them correctly on ALTER.
|
|
49
|
+
-- ---------------------------------------------------------------------------
|
|
50
|
+
alter table agent_board.delegation_policies
|
|
51
|
+
add column if not exists scope_attestation text not null default 'operator_vetted'
|
|
52
|
+
check (scope_attestation in ('operator_vetted', 'self_declared'));
|
|
53
|
+
|
|
54
|
+
comment on column agent_board.delegation_policies.scope_attestation is
|
|
55
|
+
'Provenance of allowed_scopes. self_declared = written by the root itself via the '
|
|
56
|
+
'authenticated upsert_delegation_policy self-service path — scopes are ADVISORY, NOT '
|
|
57
|
+
'operator-vetted authority. operator_vetted = written by service_role. A future gate '
|
|
58
|
+
'that consumes allowed_scopes as capability MUST require operator_vetted (or apply a '
|
|
59
|
+
'real ceiling at consumption); it must NEVER trust self_declared scopes as authority. '
|
|
60
|
+
'Stamped from the session, never from a caller argument. Existing rows backfilled '
|
|
61
|
+
'operator_vetted (all were service_role-written).';
|
|
62
|
+
|
|
63
|
+
-- ---------------------------------------------------------------------------
|
|
64
|
+
-- 2. upsert_delegation_policy — the authenticated self-service policy writer.
|
|
65
|
+
-- ---------------------------------------------------------------------------
|
|
66
|
+
create or replace function agent_board.upsert_delegation_policy(
|
|
67
|
+
p_root_agent_id uuid,
|
|
68
|
+
p_mode text default 'root_minted',
|
|
69
|
+
p_allowed_scopes jsonb default null,
|
|
70
|
+
p_max_active_children int default null,
|
|
71
|
+
p_child_ttl_ceiling interval default null,
|
|
72
|
+
p_require_child_signing_key boolean default null
|
|
73
|
+
)
|
|
74
|
+
returns agent_board.delegation_policies
|
|
75
|
+
language plpgsql
|
|
76
|
+
security definer
|
|
77
|
+
set search_path to 'agent_board', 'public'
|
|
78
|
+
as $$
|
|
79
|
+
declare
|
|
80
|
+
v_actor uuid;
|
|
81
|
+
v_is_service boolean := agent_board._is_service_role();
|
|
82
|
+
v_attestation text;
|
|
83
|
+
v_mode text;
|
|
84
|
+
v_max_children int;
|
|
85
|
+
v_ttl interval;
|
|
86
|
+
v_require_key boolean;
|
|
87
|
+
v_existing agent_board.delegation_policies%rowtype;
|
|
88
|
+
v_policy agent_board.delegation_policies%rowtype;
|
|
89
|
+
begin
|
|
90
|
+
-- Actor bound to the SESSION: authenticated -> current_agent_id() and p_root must
|
|
91
|
+
-- match it; service_role -> the supplied p_root (create-on-behalf-of). The binding is
|
|
92
|
+
-- derived from the session, never trusted from the parameter (Lane A's coalesce lesson).
|
|
93
|
+
v_actor := agent_board._resolved_actor_agent_id(p_root_agent_id, 'p_root_agent_id');
|
|
94
|
+
|
|
95
|
+
if not exists (select 1 from agent_board.agents where id = v_actor) then
|
|
96
|
+
raise exception 'root agent not found' using errcode = 'P0002';
|
|
97
|
+
end if;
|
|
98
|
+
|
|
99
|
+
-- Serialize concurrent self-service writes for the SAME root. The find-then-branch
|
|
100
|
+
-- below takes `for update`, but a `for update` over ZERO rows locks nothing, so two
|
|
101
|
+
-- concurrent FIRST upserts could each insert an active policy row (same-actor, not a
|
|
102
|
+
-- cross-root escalation, but it makes latest-policy selection ambiguous). A per-root
|
|
103
|
+
-- transaction advisory lock is the mutex — dedicated namespace, no coupling to the
|
|
104
|
+
-- agents row other flows may lock.
|
|
105
|
+
perform pg_advisory_xact_lock(hashtext('agent_board.delegation_policy:' || v_actor::text));
|
|
106
|
+
|
|
107
|
+
if p_mode is not null and p_mode not in ('independent_auth', 'root_minted') then
|
|
108
|
+
raise exception 'invalid delegation policy mode' using errcode = '22023';
|
|
109
|
+
end if;
|
|
110
|
+
v_mode := coalesce(p_mode, 'root_minted');
|
|
111
|
+
|
|
112
|
+
if v_is_service then
|
|
113
|
+
-- OPERATOR path: trusted. Scopes are operator-vetted; caps as supplied (table
|
|
114
|
+
-- defaults apply for nulls).
|
|
115
|
+
v_attestation := 'operator_vetted';
|
|
116
|
+
v_max_children := p_max_active_children;
|
|
117
|
+
v_ttl := coalesce(p_child_ttl_ceiling, interval '8 hours');
|
|
118
|
+
v_require_key := coalesce(p_require_child_signing_key, true);
|
|
119
|
+
else
|
|
120
|
+
-- SELF-SERVICE path: dangerous fields are CLAMPED, not trusted, and the scopes are
|
|
121
|
+
-- accepted but STAMPED self_declared.
|
|
122
|
+
v_attestation := 'self_declared';
|
|
123
|
+
-- require_child_signing_key: PINNED true regardless of input.
|
|
124
|
+
v_require_key := true;
|
|
125
|
+
-- max_active_children: NON-NULL, bounded 1..25.
|
|
126
|
+
v_max_children := coalesce(p_max_active_children, 5);
|
|
127
|
+
if v_max_children < 1 or v_max_children > 25 then
|
|
128
|
+
raise exception 'max_active_children must be between 1 and 25 for self-service delegation policies' using errcode = '22023';
|
|
129
|
+
end if;
|
|
130
|
+
-- child_ttl_ceiling: positive, capped at 24h (default 8h).
|
|
131
|
+
v_ttl := coalesce(p_child_ttl_ceiling, interval '8 hours');
|
|
132
|
+
if v_ttl <= interval '0' then
|
|
133
|
+
raise exception 'child_ttl_ceiling must be positive' using errcode = '22023';
|
|
134
|
+
end if;
|
|
135
|
+
if v_ttl > interval '24 hours' then
|
|
136
|
+
raise exception 'child_ttl_ceiling may not exceed 24 hours for self-service delegation policies' using errcode = '22023';
|
|
137
|
+
end if;
|
|
138
|
+
end if;
|
|
139
|
+
|
|
140
|
+
-- Upsert the root's CURRENT active policy. There is no unique on root_agent_id and the
|
|
141
|
+
-- mint reads the latest unrevoked row, so find-then-branch under a row lock rather than
|
|
142
|
+
-- ON CONFLICT: a concurrent self-service write serializes here. max_depth is PINNED 1 on
|
|
143
|
+
-- both branches — the mint enforces it structurally, but pinning the stored field stops a
|
|
144
|
+
-- future depth-aware mint inheriting an inflated value.
|
|
145
|
+
select * into v_existing
|
|
146
|
+
from agent_board.delegation_policies dp
|
|
147
|
+
where dp.root_agent_id = v_actor
|
|
148
|
+
and dp.revoked_at is null
|
|
149
|
+
order by dp.created_at desc
|
|
150
|
+
limit 1
|
|
151
|
+
for update;
|
|
152
|
+
|
|
153
|
+
if found then
|
|
154
|
+
update agent_board.delegation_policies dp
|
|
155
|
+
set mode = v_mode,
|
|
156
|
+
max_depth = 1,
|
|
157
|
+
max_active_children = v_max_children,
|
|
158
|
+
allowed_scopes = coalesce(p_allowed_scopes, '[]'::jsonb),
|
|
159
|
+
child_ttl_ceiling = v_ttl,
|
|
160
|
+
require_child_signing_key = v_require_key,
|
|
161
|
+
scope_attestation = v_attestation
|
|
162
|
+
where dp.id = v_existing.id
|
|
163
|
+
returning * into v_policy;
|
|
164
|
+
else
|
|
165
|
+
insert into agent_board.delegation_policies(
|
|
166
|
+
root_agent_id, mode, max_depth, max_active_children, allowed_scopes,
|
|
167
|
+
child_ttl_ceiling, require_child_signing_key, scope_attestation
|
|
168
|
+
)
|
|
169
|
+
values (
|
|
170
|
+
v_actor, v_mode, 1, v_max_children, coalesce(p_allowed_scopes, '[]'::jsonb),
|
|
171
|
+
v_ttl, v_require_key, v_attestation
|
|
172
|
+
)
|
|
173
|
+
returning * into v_policy;
|
|
174
|
+
end if;
|
|
175
|
+
|
|
176
|
+
return v_policy;
|
|
177
|
+
end;
|
|
178
|
+
$$;
|
|
179
|
+
|
|
180
|
+
comment on function agent_board.upsert_delegation_policy(uuid, text, jsonb, int, interval, boolean) is
|
|
181
|
+
'Self-service writer for a root''s delegation policy (ROOMDELEG-2 / #59). authenticated: '
|
|
182
|
+
'actor bound root == current_agent_id(); scopes accepted but stamped scope_attestation='
|
|
183
|
+
'''self_declared'' (ADVISORY — see the column comment); require_child_signing_key pinned '
|
|
184
|
+
'true, max_active_children clamped 1..25, child_ttl_ceiling clamped <=24h, max_depth pinned '
|
|
185
|
+
'1. service_role: create-on-behalf-of, operator_vetted, caps as supplied. Upserts the root''s '
|
|
186
|
+
'latest unrevoked policy row.';
|
|
187
|
+
|
|
188
|
+
revoke all on function agent_board.upsert_delegation_policy(uuid, text, jsonb, int, interval, boolean) from public, anon, authenticated;
|
|
189
|
+
grant execute on function agent_board.upsert_delegation_policy(uuid, text, jsonb, int, interval, boolean) to authenticated, service_role;
|
|
190
|
+
|
|
191
|
+
-- ---------------------------------------------------------------------------
|
|
192
|
+
-- 3. mint_delegated_child — DEFERRED.
|
|
193
|
+
-- ---------------------------------------------------------------------------
|
|
194
|
+
-- Opening mint to authenticated is INTENTIONALLY NOT done here. mint stamps a
|
|
195
|
+
-- caller-supplied child public key as proof_state='proven' with NO proof-of-possession,
|
|
196
|
+
-- and derives its fingerprint over the base64url STRING rather than the raw key bytes
|
|
197
|
+
-- (20260718001000:203). Under the current service_role-only gate that is operator-trusted;
|
|
198
|
+
-- but opening it to authenticated would let any root register a VICTIM's public key as its
|
|
199
|
+
-- child's "proven" key — and because the string-fingerprint differs from enroll's raw-byte
|
|
200
|
+
-- fingerprint (fixed for enroll in 20260719001200), the agent_signing_keys.fingerprint
|
|
201
|
+
-- UNIQUE backstop does not dedup across the two paths, so the squat sails past it.
|
|
202
|
+
--
|
|
203
|
+
-- Mint therefore stays service_role-only until that class is closed. The mint-opening +
|
|
204
|
+
-- child-key PoP + raw-byte fingerprint (catalog-enumerated across every fingerprint writer)
|
|
205
|
+
-- + the non-positive-TTL floor land together in the hardening tracked by #138. This PR ships
|
|
206
|
+
-- only the self-service POLICY writer, which handles no keys and adds no signing surface.
|