@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.
- package/fixtures/already-applied.json +136 -0
- package/migrations/20260729000100_agent_board_fedsession_autojoin_default_board.sql +364 -0
- package/migrations/20260730000100_agent_board_gatenull_service_role_bypass.sql +369 -0
- package/migrations/20260730000200_agent_board_membstate_authz.sql +195 -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 +309 -3
- package/src/tooling.test.mjs +123 -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_fedsession_autojoin.test.sql +181 -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_gatenull_membstate.test.sql +352 -0
- 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,514 @@
|
|
|
1
|
+
-- #146 operator machine reactivation — supersede-based un-revoke.
|
|
2
|
+
--
|
|
3
|
+
-- #141 (PR #145) correctly made machine revocation TERMINAL via re-register: a
|
|
4
|
+
-- re-enrollment no longer clears lifecycle_state/revoked_at, because revocation
|
|
5
|
+
-- authority lives in the append-only agent_board.fleet_revocation_events ledger that
|
|
6
|
+
-- evaluate_fleet_sender_policy reads. The side effect (issue #146) is that there was
|
|
7
|
+
-- NO authorized path to bring a legitimately-revoked machine back: setting
|
|
8
|
+
-- lifecycle_state='active' alone does not restore authority, because the machine's
|
|
9
|
+
-- revocation EVENT still exists and the policy denies on its mere presence.
|
|
10
|
+
--
|
|
11
|
+
-- Reactivation therefore has to SUPERSEDE the prior revocation event(s) for the machine
|
|
12
|
+
-- rather than delete them (the ledger stays append-only). We stamp a supersession point
|
|
13
|
+
-- on the machine row (reactivated_at) and teach the policy's MACHINE-revocation clause to
|
|
14
|
+
-- ignore machine events that are at-or-before that point. A LATER revocation event
|
|
15
|
+
-- (effective_at > reactivated_at) still denies, so a re-revocation after reactivation is
|
|
16
|
+
-- honored; and reactivation supersedes ONLY the machine subject — a revoked trust_domain
|
|
17
|
+
-- or a revoked sender agent stays denied (those clauses are unchanged).
|
|
18
|
+
--
|
|
19
|
+
-- MAINTAINER DECISION (locked): reactivation is OPERATOR-ONLY (service_role). An owner
|
|
20
|
+
-- cannot self-reactivate — a revocation may be a deliberate security response, so undoing
|
|
21
|
+
-- it is an operator action. reactivate_fleet_machine raises 42501 for any non-service_role
|
|
22
|
+
-- caller (and is granted only to service_role — #118 discipline).
|
|
23
|
+
--
|
|
24
|
+
-- SCOPE: machine reactivation only. The fingerprint-SQUATTING reclaim (a never-owned
|
|
25
|
+
-- fingerprint pre-registered by a squatter → ownership arbitration) is a DIFFERENT
|
|
26
|
+
-- mechanism and remains open #146 work — NOT built here.
|
|
27
|
+
|
|
28
|
+
-- (1) Supersession marker. Nullable: null means "never reactivated" (all prior machine
|
|
29
|
+
-- events apply). Set to now() on each reactivation.
|
|
30
|
+
alter table agent_board.fleet_machines
|
|
31
|
+
add column if not exists reactivated_at timestamptz;
|
|
32
|
+
|
|
33
|
+
comment on column agent_board.fleet_machines.reactivated_at is
|
|
34
|
+
'Supersession point (#146): machine-revocation events with effective_at <= this are ignored by evaluate_fleet_sender_policy. Set by reactivate_fleet_machine; null means never reactivated.';
|
|
35
|
+
|
|
36
|
+
-- (2) Operator-only reactivation RPC. Reconciles the machine row (active, revoked_at=null,
|
|
37
|
+
-- reactivated_at=now()) so the supersession clause in the policy lets it send again. Does
|
|
38
|
+
-- NOT write to the append-only ledger — supersession is by timestamp, not by a compensating
|
|
39
|
+
-- event (there is no 'reactivation' subject_kind).
|
|
40
|
+
create or replace function agent_board.reactivate_fleet_machine(
|
|
41
|
+
p_machine_id uuid,
|
|
42
|
+
p_reactivated_by_agent_id uuid default null,
|
|
43
|
+
p_reactivation_reason text default null
|
|
44
|
+
)
|
|
45
|
+
returns table(machine_id uuid, lifecycle_state text, reactivated_at timestamptz)
|
|
46
|
+
language plpgsql
|
|
47
|
+
security definer
|
|
48
|
+
set search_path = agent_board, public
|
|
49
|
+
as $$
|
|
50
|
+
declare
|
|
51
|
+
v_machine agent_board.fleet_machines%rowtype;
|
|
52
|
+
v_is_revoked boolean;
|
|
53
|
+
begin
|
|
54
|
+
-- Operator-only: an owner cannot self-reactivate (revocation may be a security response).
|
|
55
|
+
if not agent_board._is_service_role() then
|
|
56
|
+
raise exception 'reactivate_fleet_machine is operator-only (service_role)' using errcode = '42501';
|
|
57
|
+
end if;
|
|
58
|
+
|
|
59
|
+
-- Provenance is required and must name a real agent (audit lands in metadata).
|
|
60
|
+
if p_reactivated_by_agent_id is null then
|
|
61
|
+
raise exception 'p_reactivated_by_agent_id is required' using errcode = '22023';
|
|
62
|
+
end if;
|
|
63
|
+
if not exists (
|
|
64
|
+
select 1 from agent_board.agents a where a.id = p_reactivated_by_agent_id
|
|
65
|
+
) then
|
|
66
|
+
raise exception 'p_reactivated_by_agent_id is not a known agent' using errcode = '22023';
|
|
67
|
+
end if;
|
|
68
|
+
|
|
69
|
+
-- MUST-FIX 3 (#146 panel, concurrency): lock the machine row so this whole
|
|
70
|
+
-- read-decide-write serializes against a concurrent revoke_fleet_subject (whose
|
|
71
|
+
-- fleet_machines UPDATE also takes a row lock). Without the lock the still-revoked
|
|
72
|
+
-- re-check below could act on a stale snapshot and the unconditional UPDATE could
|
|
73
|
+
-- silently overwrite a revoke that committed mid-operation. With it, a revoke and a
|
|
74
|
+
-- reactivate on the same machine serialize deterministically (last-writer-wins is
|
|
75
|
+
-- coherent) and a revoke cannot be dropped.
|
|
76
|
+
select * into v_machine from agent_board.fleet_machines fm where fm.id = p_machine_id for update;
|
|
77
|
+
if v_machine.id is null then
|
|
78
|
+
raise exception 'fleet machine not found' using errcode = '22023';
|
|
79
|
+
end if;
|
|
80
|
+
|
|
81
|
+
-- Must currently be revoked (re-checked UNDER the row lock): lifecycle_state='revoked'
|
|
82
|
+
-- OR a machine-revocation event newer than the last supersession point. Otherwise there
|
|
83
|
+
-- is nothing to reactivate.
|
|
84
|
+
v_is_revoked := v_machine.lifecycle_state = 'revoked'
|
|
85
|
+
or exists (
|
|
86
|
+
select 1
|
|
87
|
+
from agent_board.fleet_revocation_events fre
|
|
88
|
+
where fre.subject_kind = 'machine'
|
|
89
|
+
and fre.subject_id = v_machine.id
|
|
90
|
+
and fre.effective_at > coalesce(v_machine.reactivated_at, '-infinity'::timestamptz)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if not v_is_revoked then
|
|
94
|
+
raise exception 'fleet machine is not revoked; nothing to reactivate' using errcode = '22023';
|
|
95
|
+
end if;
|
|
96
|
+
|
|
97
|
+
update agent_board.fleet_machines fm
|
|
98
|
+
set lifecycle_state = 'active',
|
|
99
|
+
revoked_at = null,
|
|
100
|
+
reactivated_at = now(),
|
|
101
|
+
metadata = coalesce(fm.metadata, '{}'::jsonb)
|
|
102
|
+
|| jsonb_build_object(
|
|
103
|
+
'reactivated_by', p_reactivated_by_agent_id,
|
|
104
|
+
'reactivation_reason', nullif(trim(coalesce(p_reactivation_reason, '')), ''),
|
|
105
|
+
'reactivated_at', now()
|
|
106
|
+
)
|
|
107
|
+
|| jsonb_build_object('redacted', true, 'metadata-safe', true),
|
|
108
|
+
updated_at = now()
|
|
109
|
+
where fm.id = p_machine_id
|
|
110
|
+
returning * into v_machine;
|
|
111
|
+
|
|
112
|
+
return query select v_machine.id, v_machine.lifecycle_state, v_machine.reactivated_at;
|
|
113
|
+
end;
|
|
114
|
+
$$;
|
|
115
|
+
|
|
116
|
+
-- (3) Policy: reproduce evaluate_fleet_sender_policy VERBATIM except the MACHINE clause of
|
|
117
|
+
-- the revocation existence check, which now honors reactivated_at. The trust_domain and
|
|
118
|
+
-- agent clauses are UNCHANGED — reactivating a machine must NOT un-revoke its domain or the
|
|
119
|
+
-- sender agent. The lifecycle_state='revoked' guard is kept (reactivate sets it active).
|
|
120
|
+
create or replace function agent_board.evaluate_fleet_sender_policy(
|
|
121
|
+
p_sender_agent_id uuid,
|
|
122
|
+
p_recipient_agent_id uuid,
|
|
123
|
+
p_sender_machine_id uuid default null,
|
|
124
|
+
p_recipient_board_id uuid default null,
|
|
125
|
+
p_operation text default 'send_direct_message',
|
|
126
|
+
p_signature_evidence_id uuid default null,
|
|
127
|
+
p_direct_rpc_compatibility boolean default false
|
|
128
|
+
)
|
|
129
|
+
returns table(
|
|
130
|
+
allowed boolean,
|
|
131
|
+
actionable boolean,
|
|
132
|
+
denial_class text,
|
|
133
|
+
receive_policy_id uuid,
|
|
134
|
+
trust_domain_id uuid,
|
|
135
|
+
trust_set_id uuid,
|
|
136
|
+
requires_signature boolean,
|
|
137
|
+
evaluated_at timestamptz
|
|
138
|
+
)
|
|
139
|
+
language plpgsql
|
|
140
|
+
security definer
|
|
141
|
+
set search_path = agent_board, public
|
|
142
|
+
as $$
|
|
143
|
+
declare
|
|
144
|
+
v_machine agent_board.fleet_machines%rowtype;
|
|
145
|
+
v_binding agent_board.fleet_machine_agents%rowtype;
|
|
146
|
+
v_policy agent_board.fleet_receive_policies%rowtype;
|
|
147
|
+
v_trust_set agent_board.fleet_trust_sets%rowtype;
|
|
148
|
+
v_signature agent_board.signature_evidence%rowtype;
|
|
149
|
+
v_actor_agent_id uuid;
|
|
150
|
+
v_action text;
|
|
151
|
+
v_denial text;
|
|
152
|
+
begin
|
|
153
|
+
if not agent_board._is_service_role() then
|
|
154
|
+
v_actor_agent_id := agent_board.current_agent_id();
|
|
155
|
+
if v_actor_agent_id is null
|
|
156
|
+
or (v_actor_agent_id <> p_sender_agent_id and v_actor_agent_id <> p_recipient_agent_id) then
|
|
157
|
+
raise exception 'fleet policy evaluation must involve current_agent_id()' using errcode = '42501';
|
|
158
|
+
end if;
|
|
159
|
+
end if;
|
|
160
|
+
|
|
161
|
+
select fma.*
|
|
162
|
+
into v_binding
|
|
163
|
+
from agent_board.fleet_machine_agents fma
|
|
164
|
+
join agent_board.fleet_machines fm on fm.id = fma.machine_id
|
|
165
|
+
where fma.agent_id = p_sender_agent_id
|
|
166
|
+
and fma.lifecycle_state = 'active'
|
|
167
|
+
and fm.lifecycle_state = 'active'
|
|
168
|
+
and (p_sender_machine_id is null or fma.machine_id = p_sender_machine_id)
|
|
169
|
+
order by fma.updated_at desc
|
|
170
|
+
limit 1;
|
|
171
|
+
|
|
172
|
+
if v_binding.id is not null then
|
|
173
|
+
select * into v_machine from agent_board.fleet_machines where id = v_binding.machine_id;
|
|
174
|
+
elsif p_sender_machine_id is not null then
|
|
175
|
+
select * into v_machine from agent_board.fleet_machines where id = p_sender_machine_id;
|
|
176
|
+
end if;
|
|
177
|
+
|
|
178
|
+
if v_machine.id is null then
|
|
179
|
+
return query select false, false, 'domain_denied'::text, null::uuid, null::uuid, null::uuid, false, now();
|
|
180
|
+
return;
|
|
181
|
+
end if;
|
|
182
|
+
|
|
183
|
+
if v_machine.lifecycle_state = 'revoked'
|
|
184
|
+
or exists (
|
|
185
|
+
select 1 from agent_board.fleet_revocation_events fre
|
|
186
|
+
where (fre.subject_kind = 'machine' and fre.subject_id = v_machine.id
|
|
187
|
+
and fre.effective_at > coalesce(v_machine.reactivated_at, '-infinity'::timestamptz))
|
|
188
|
+
or (fre.subject_kind = 'trust_domain' and fre.subject_id = v_machine.trust_domain_id)
|
|
189
|
+
or (fre.subject_kind = 'agent' and fre.subject_id = p_sender_agent_id)
|
|
190
|
+
) then
|
|
191
|
+
return query select false, false, 'machine_revoked'::text, null::uuid, v_machine.trust_domain_id, null::uuid, false, now();
|
|
192
|
+
return;
|
|
193
|
+
end if;
|
|
194
|
+
|
|
195
|
+
select frp.*
|
|
196
|
+
into v_policy
|
|
197
|
+
from agent_board.fleet_receive_policies frp
|
|
198
|
+
where frp.lifecycle_state = 'active'
|
|
199
|
+
and (frp.recipient_agent_id is null or frp.recipient_agent_id = p_recipient_agent_id)
|
|
200
|
+
and (frp.board_id is null or frp.board_id = p_recipient_board_id)
|
|
201
|
+
and (frp.trust_domain_id is null or frp.trust_domain_id = v_machine.trust_domain_id)
|
|
202
|
+
order by
|
|
203
|
+
case when frp.board_id is not null then 0 else 1 end,
|
|
204
|
+
case when frp.trust_domain_id is not null then 0 else 1 end,
|
|
205
|
+
frp.updated_at desc
|
|
206
|
+
limit 1;
|
|
207
|
+
|
|
208
|
+
if v_policy.id is null then
|
|
209
|
+
return query select false, false, 'recipient_policy_denied'::text, null::uuid, v_machine.trust_domain_id, null::uuid, false, now();
|
|
210
|
+
return;
|
|
211
|
+
end if;
|
|
212
|
+
|
|
213
|
+
if v_policy.require_signed_sender then
|
|
214
|
+
if p_signature_evidence_id is null then
|
|
215
|
+
return query select false, false, 'signature_required'::text, v_policy.id, v_machine.trust_domain_id, null::uuid, true, now();
|
|
216
|
+
return;
|
|
217
|
+
end if;
|
|
218
|
+
|
|
219
|
+
select * into v_signature from agent_board.signature_evidence where id = p_signature_evidence_id;
|
|
220
|
+
if v_signature.id is null or v_signature.agent_id <> p_sender_agent_id then
|
|
221
|
+
return query select false, false, 'signature_required'::text, v_policy.id, v_machine.trust_domain_id, null::uuid, true, now();
|
|
222
|
+
return;
|
|
223
|
+
end if;
|
|
224
|
+
|
|
225
|
+
if v_signature.failure_class = 'key_revoked'
|
|
226
|
+
or exists (
|
|
227
|
+
select 1
|
|
228
|
+
from agent_board.fleet_revocation_events fre
|
|
229
|
+
where fre.subject_kind = 'signing_key'
|
|
230
|
+
and fre.subject_id = v_signature.signing_key_id
|
|
231
|
+
)
|
|
232
|
+
or exists (
|
|
233
|
+
select 1
|
|
234
|
+
from agent_board.agent_signing_keys ask
|
|
235
|
+
where ask.id = v_signature.signing_key_id
|
|
236
|
+
and ask.status in ('revoked', 'compromised')
|
|
237
|
+
) then
|
|
238
|
+
return query select false, false, 'key_revoked'::text, v_policy.id, v_machine.trust_domain_id, null::uuid, true, now();
|
|
239
|
+
return;
|
|
240
|
+
end if;
|
|
241
|
+
|
|
242
|
+
if v_signature.verification_status not in ('verified', 'warn', 'observed') then
|
|
243
|
+
return query select false, false, 'signature_required'::text, v_policy.id, v_machine.trust_domain_id, null::uuid, true, now();
|
|
244
|
+
return;
|
|
245
|
+
end if;
|
|
246
|
+
end if;
|
|
247
|
+
|
|
248
|
+
select fts.*
|
|
249
|
+
into v_trust_set
|
|
250
|
+
from agent_board.fleet_trust_sets fts
|
|
251
|
+
where fts.receive_policy_id = v_policy.id
|
|
252
|
+
and fts.lifecycle_state = 'active'
|
|
253
|
+
and (
|
|
254
|
+
fts.trust_domain_id = v_machine.trust_domain_id
|
|
255
|
+
or fts.machine_id = v_machine.id
|
|
256
|
+
or fts.agent_id = p_sender_agent_id
|
|
257
|
+
or (v_binding.repo_scope_id is not null and fts.repo_scope_id = v_binding.repo_scope_id)
|
|
258
|
+
or exists (
|
|
259
|
+
select 1
|
|
260
|
+
from agent_board.fleet_repo_scopes frs
|
|
261
|
+
where frs.id = v_binding.repo_scope_id
|
|
262
|
+
and fts.harness_registration_id = frs.harness_registration_id
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
order by
|
|
266
|
+
case fts.effect
|
|
267
|
+
when 'deny' then 0
|
|
268
|
+
when 'quarantine' then 1
|
|
269
|
+
when 'non_actionable' then 2
|
|
270
|
+
else 3
|
|
271
|
+
end,
|
|
272
|
+
fts.updated_at desc
|
|
273
|
+
limit 1;
|
|
274
|
+
|
|
275
|
+
v_action := coalesce(v_trust_set.effect, v_policy.default_action);
|
|
276
|
+
v_denial := case
|
|
277
|
+
when v_action = 'deny' and v_trust_set.id is not null then 'sender_denied'
|
|
278
|
+
when v_action = 'deny' then 'recipient_policy_denied'
|
|
279
|
+
when v_action = 'quarantine' then 'quarantined'
|
|
280
|
+
else null
|
|
281
|
+
end;
|
|
282
|
+
|
|
283
|
+
return query
|
|
284
|
+
select
|
|
285
|
+
v_action in ('allow', 'non_actionable'),
|
|
286
|
+
v_action = 'allow',
|
|
287
|
+
v_denial,
|
|
288
|
+
v_policy.id,
|
|
289
|
+
v_machine.trust_domain_id,
|
|
290
|
+
v_trust_set.id,
|
|
291
|
+
v_policy.require_signed_sender,
|
|
292
|
+
now();
|
|
293
|
+
end;
|
|
294
|
+
$$;
|
|
295
|
+
|
|
296
|
+
-- #118 discipline: recreating a SECURITY DEFINER function grants EXECUTE to PUBLIC by
|
|
297
|
+
-- default. Restate the revokes/grants and the comments in this same migration.
|
|
298
|
+
revoke all on function agent_board.reactivate_fleet_machine(uuid, uuid, text) from public, anon, authenticated;
|
|
299
|
+
grant execute on function agent_board.reactivate_fleet_machine(uuid, uuid, text) to service_role;
|
|
300
|
+
|
|
301
|
+
revoke all on function agent_board.evaluate_fleet_sender_policy(uuid, uuid, uuid, uuid, text, uuid, boolean) from public, anon;
|
|
302
|
+
grant execute on function agent_board.evaluate_fleet_sender_policy(uuid, uuid, uuid, uuid, text, uuid, boolean) to authenticated, service_role;
|
|
303
|
+
|
|
304
|
+
comment on function agent_board.reactivate_fleet_machine(uuid, uuid, text) is
|
|
305
|
+
'Operator-only (#146): brings a revoked fleet machine back to active by stamping reactivated_at (supersession point) and clearing revoked_at, so evaluate_fleet_sender_policy ignores machine-revocation events at-or-before that point. service_role only (raises 42501 otherwise); never un-revokes the machine''s trust_domain or sender agent; does not write the append-only fleet_revocation_events ledger.';
|
|
306
|
+
|
|
307
|
+
comment on function agent_board.evaluate_fleet_sender_policy(uuid, uuid, uuid, uuid, text, uuid, boolean) is
|
|
308
|
+
'Fleet sender-policy evaluation. Machine-revocation clause honors fleet_machines.reactivated_at (#146): machine events at-or-before the supersession point are ignored; trust_domain and agent revocation clauses are unaffected by machine reactivation.';
|
|
309
|
+
|
|
310
|
+
-- (4) MUST-FIX 1 (#146 panel): keep the append-only ledger AUTHORITATIVE across
|
|
311
|
+
-- re-revocation. revoke_fleet_subject keeps ONE event row per (subject_kind, subject_id)
|
|
312
|
+
-- and its UPDATE branch previously refreshed reason/metadata but NOT effective_at, so a
|
|
313
|
+
-- revoke AFTER a reactivation left effective_at BEFORE reactivated_at — the machine
|
|
314
|
+
-- supersession clause then read the (real) re-revocation as "not revoked", leaving only the
|
|
315
|
+
-- mutable lifecycle_state guard to deny. That inverts #141's principle (authority in the
|
|
316
|
+
-- ledger, not the row). Reproduced VERBATIM except the UPDATE branch now also sets
|
|
317
|
+
-- `effective_at = now()`, so a re-revocation advances the event past reactivated_at and both
|
|
318
|
+
-- the ledger-derived clause and the lifecycle guard deny (2-of-2 depth). #118 grants/comment
|
|
319
|
+
-- restated.
|
|
320
|
+
create or replace function agent_board.revoke_fleet_subject(
|
|
321
|
+
p_subject_kind text,
|
|
322
|
+
p_subject_id uuid,
|
|
323
|
+
p_revoked_by_agent_id uuid,
|
|
324
|
+
p_revocation_reason text,
|
|
325
|
+
p_metadata jsonb default '{}'::jsonb
|
|
326
|
+
)
|
|
327
|
+
returns table(revocation_event_id uuid, subject_kind text, effective_at timestamptz)
|
|
328
|
+
language plpgsql
|
|
329
|
+
security definer
|
|
330
|
+
set search_path = agent_board, public
|
|
331
|
+
as $$
|
|
332
|
+
declare
|
|
333
|
+
v_event agent_board.fleet_revocation_events%rowtype;
|
|
334
|
+
v_actor_agent_id uuid;
|
|
335
|
+
begin
|
|
336
|
+
if p_subject_kind not in ('trust_domain', 'machine', 'harness', 'repo_scope', 'agent', 'signing_key', 'credential') then
|
|
337
|
+
raise exception 'unsupported fleet revocation subject' using errcode = '22023';
|
|
338
|
+
end if;
|
|
339
|
+
|
|
340
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_revoked_by_agent_id, 'p_revoked_by_agent_id');
|
|
341
|
+
|
|
342
|
+
if not agent_board._is_service_role() then
|
|
343
|
+
if p_subject_kind = 'trust_domain' and not exists (
|
|
344
|
+
select 1 from agent_board.fleet_trust_domains ftd
|
|
345
|
+
where ftd.id = p_subject_id and ftd.owner_agent_id = v_actor_agent_id
|
|
346
|
+
) then
|
|
347
|
+
raise exception 'trust domain is not owned by current_agent_id()' using errcode = '42501';
|
|
348
|
+
elsif p_subject_kind = 'machine' and not exists (
|
|
349
|
+
select 1 from agent_board.fleet_machines fm
|
|
350
|
+
where fm.id = p_subject_id and fm.owner_agent_id = v_actor_agent_id
|
|
351
|
+
) then
|
|
352
|
+
raise exception 'machine is not owned by current_agent_id()' using errcode = '42501';
|
|
353
|
+
elsif p_subject_kind = 'harness' and not exists (
|
|
354
|
+
select 1
|
|
355
|
+
from agent_board.fleet_harness_registrations fhr
|
|
356
|
+
join agent_board.fleet_machines fm on fm.id = fhr.machine_id
|
|
357
|
+
where fhr.id = p_subject_id and fm.owner_agent_id = v_actor_agent_id
|
|
358
|
+
) then
|
|
359
|
+
raise exception 'harness is not owned by current_agent_id()' using errcode = '42501';
|
|
360
|
+
elsif p_subject_kind = 'repo_scope' and not exists (
|
|
361
|
+
select 1
|
|
362
|
+
from agent_board.fleet_repo_scopes frs
|
|
363
|
+
join agent_board.fleet_harness_registrations fhr on fhr.id = frs.harness_registration_id
|
|
364
|
+
join agent_board.fleet_machines fm on fm.id = fhr.machine_id
|
|
365
|
+
where frs.id = p_subject_id and fm.owner_agent_id = v_actor_agent_id
|
|
366
|
+
) then
|
|
367
|
+
raise exception 'repo scope is not owned by current_agent_id()' using errcode = '42501';
|
|
368
|
+
elsif p_subject_kind = 'agent' and p_subject_id <> v_actor_agent_id then
|
|
369
|
+
raise exception 'agent revocation must target current_agent_id()' using errcode = '42501';
|
|
370
|
+
elsif p_subject_kind = 'signing_key' and not exists (
|
|
371
|
+
select 1 from agent_board.agent_signing_keys ask
|
|
372
|
+
where ask.id = p_subject_id and ask.agent_id = v_actor_agent_id
|
|
373
|
+
) then
|
|
374
|
+
raise exception 'signing key is not owned by current_agent_id()' using errcode = '42501';
|
|
375
|
+
elsif p_subject_kind = 'credential' and not exists (
|
|
376
|
+
select 1 from agent_board.agent_credentials ac
|
|
377
|
+
where ac.id = p_subject_id and ac.agent_id = v_actor_agent_id
|
|
378
|
+
) then
|
|
379
|
+
raise exception 'credential is not owned by current_agent_id()' using errcode = '42501';
|
|
380
|
+
end if;
|
|
381
|
+
end if;
|
|
382
|
+
|
|
383
|
+
select *
|
|
384
|
+
into v_event
|
|
385
|
+
from agent_board.fleet_revocation_events fre
|
|
386
|
+
where fre.subject_kind = p_subject_kind
|
|
387
|
+
and fre.subject_id = p_subject_id
|
|
388
|
+
limit 1;
|
|
389
|
+
|
|
390
|
+
if v_event.id is null then
|
|
391
|
+
insert into agent_board.fleet_revocation_events(
|
|
392
|
+
subject_kind,
|
|
393
|
+
subject_id,
|
|
394
|
+
trust_domain_id,
|
|
395
|
+
machine_id,
|
|
396
|
+
harness_registration_id,
|
|
397
|
+
repo_scope_id,
|
|
398
|
+
agent_id,
|
|
399
|
+
signing_key_id,
|
|
400
|
+
revoked_by_agent_id,
|
|
401
|
+
revocation_reason,
|
|
402
|
+
metadata
|
|
403
|
+
)
|
|
404
|
+
values (
|
|
405
|
+
p_subject_kind,
|
|
406
|
+
p_subject_id,
|
|
407
|
+
case when p_subject_kind = 'trust_domain' then p_subject_id end,
|
|
408
|
+
case when p_subject_kind = 'machine' then p_subject_id end,
|
|
409
|
+
case when p_subject_kind = 'harness' then p_subject_id end,
|
|
410
|
+
case when p_subject_kind = 'repo_scope' then p_subject_id end,
|
|
411
|
+
case when p_subject_kind = 'agent' then p_subject_id end,
|
|
412
|
+
case when p_subject_kind = 'signing_key' then p_subject_id end,
|
|
413
|
+
p_revoked_by_agent_id,
|
|
414
|
+
trim(coalesce(p_revocation_reason, 'operator revocation')),
|
|
415
|
+
coalesce(p_metadata, '{}'::jsonb) || jsonb_build_object('redacted', true, 'metadata-safe', true)
|
|
416
|
+
)
|
|
417
|
+
returning * into v_event;
|
|
418
|
+
else
|
|
419
|
+
update agent_board.fleet_revocation_events fre
|
|
420
|
+
set effective_at = now(),
|
|
421
|
+
revoked_by_agent_id = p_revoked_by_agent_id,
|
|
422
|
+
revocation_reason = trim(coalesce(p_revocation_reason, 'operator revocation')),
|
|
423
|
+
metadata = coalesce(p_metadata, '{}'::jsonb) || jsonb_build_object('redacted', true, 'metadata-safe', true)
|
|
424
|
+
where fre.id = v_event.id
|
|
425
|
+
returning * into v_event;
|
|
426
|
+
end if;
|
|
427
|
+
|
|
428
|
+
if p_subject_kind = 'trust_domain' then
|
|
429
|
+
update agent_board.fleet_trust_domains
|
|
430
|
+
set lifecycle_state = 'revoked', revoked_at = v_event.effective_at, updated_at = now()
|
|
431
|
+
where id = p_subject_id;
|
|
432
|
+
elsif p_subject_kind = 'machine' then
|
|
433
|
+
update agent_board.fleet_machines
|
|
434
|
+
set lifecycle_state = 'revoked', revoked_at = v_event.effective_at, updated_at = now()
|
|
435
|
+
where id = p_subject_id;
|
|
436
|
+
elsif p_subject_kind = 'harness' then
|
|
437
|
+
update agent_board.fleet_harness_registrations
|
|
438
|
+
set lifecycle_state = 'revoked', revoked_at = v_event.effective_at, updated_at = now()
|
|
439
|
+
where id = p_subject_id;
|
|
440
|
+
elsif p_subject_kind = 'repo_scope' then
|
|
441
|
+
update agent_board.fleet_repo_scopes
|
|
442
|
+
set lifecycle_state = 'revoked', revoked_at = v_event.effective_at, updated_at = now()
|
|
443
|
+
where id = p_subject_id;
|
|
444
|
+
elsif p_subject_kind = 'agent' then
|
|
445
|
+
update agent_board.fleet_machine_agents
|
|
446
|
+
set lifecycle_state = 'revoked', revoked_at = v_event.effective_at, updated_at = now()
|
|
447
|
+
where agent_id = p_subject_id;
|
|
448
|
+
elsif p_subject_kind = 'signing_key' then
|
|
449
|
+
update agent_board.agent_signing_keys
|
|
450
|
+
set status = case when status = 'compromised' then status else 'revoked' end,
|
|
451
|
+
proof_state = 'revoked',
|
|
452
|
+
revoked_at = coalesce(revoked_at, v_event.effective_at),
|
|
453
|
+
revoked_by_agent_id = p_revoked_by_agent_id,
|
|
454
|
+
revocation_reason = trim(coalesce(p_revocation_reason, 'operator revocation')),
|
|
455
|
+
updated_at = now()
|
|
456
|
+
where id = p_subject_id;
|
|
457
|
+
end if;
|
|
458
|
+
|
|
459
|
+
return query select v_event.id, v_event.subject_kind, v_event.effective_at;
|
|
460
|
+
end;
|
|
461
|
+
$$;
|
|
462
|
+
|
|
463
|
+
revoke all on function agent_board.revoke_fleet_subject(text, uuid, uuid, text, jsonb) from public, anon;
|
|
464
|
+
grant execute on function agent_board.revoke_fleet_subject(text, uuid, uuid, text, jsonb) to authenticated, service_role;
|
|
465
|
+
|
|
466
|
+
comment on function agent_board.revoke_fleet_subject(text, uuid, uuid, text, jsonb) is
|
|
467
|
+
'Records a fleet revocation into the append-only fleet_revocation_events ledger (one row per subject) and flips the subject''s lifecycle to revoked. Re-revoking a subject advances the event''s effective_at to now() (#146), so a revocation after a reactivation supersedes the reactivation''s reactivated_at and stays authoritative in the ledger.';
|
|
468
|
+
|
|
469
|
+
-- (5) MUST-FIX 2 (#146 panel): the read model must agree with the policy. v_fleet_machine_status
|
|
470
|
+
-- reported machine_revoked from bare event-existence, so a reactivated machine showed
|
|
471
|
+
-- machine_revoked=true forever while the policy let it send — contradictory state visible to
|
|
472
|
+
-- owners/operators. Apply the SAME supersession predicate the policy uses. Recreated with its
|
|
473
|
+
-- grants restated.
|
|
474
|
+
create or replace view agent_board.v_fleet_machine_status
|
|
475
|
+
with (security_invoker = true)
|
|
476
|
+
as
|
|
477
|
+
select
|
|
478
|
+
fm.id as machine_id,
|
|
479
|
+
fm.trust_domain_id,
|
|
480
|
+
ftd.slug as trust_domain_slug,
|
|
481
|
+
ftd.domain_kind,
|
|
482
|
+
fm.owner_agent_id,
|
|
483
|
+
fm.machine_label,
|
|
484
|
+
fm.machine_fingerprint,
|
|
485
|
+
fm.lifecycle_state as machine_lifecycle_state,
|
|
486
|
+
fm.enrollment_source,
|
|
487
|
+
fm.enrolled_at,
|
|
488
|
+
fm.revoked_at,
|
|
489
|
+
(
|
|
490
|
+
fm.lifecycle_state = 'revoked'
|
|
491
|
+
or exists (
|
|
492
|
+
select 1
|
|
493
|
+
from agent_board.fleet_revocation_events fre
|
|
494
|
+
where fre.subject_kind = 'machine'
|
|
495
|
+
and fre.subject_id = fm.id
|
|
496
|
+
and fre.effective_at > coalesce(fm.reactivated_at, '-infinity'::timestamptz)
|
|
497
|
+
)
|
|
498
|
+
) as machine_revoked,
|
|
499
|
+
coalesce(active_agents.active_agent_count, 0)::bigint as active_agent_count,
|
|
500
|
+
active_agents.latest_agent_binding_updated_at
|
|
501
|
+
from agent_board.fleet_machines fm
|
|
502
|
+
join agent_board.fleet_trust_domains ftd on ftd.id = fm.trust_domain_id
|
|
503
|
+
left join lateral (
|
|
504
|
+
select
|
|
505
|
+
count(*) filter (where fma.lifecycle_state = 'active') as active_agent_count,
|
|
506
|
+
max(fma.updated_at) as latest_agent_binding_updated_at
|
|
507
|
+
from agent_board.fleet_machine_agents fma
|
|
508
|
+
where fma.machine_id = fm.id
|
|
509
|
+
) active_agents on true;
|
|
510
|
+
|
|
511
|
+
grant select on agent_board.v_fleet_machine_status to authenticated, service_role;
|
|
512
|
+
|
|
513
|
+
comment on view agent_board.v_fleet_machine_status is
|
|
514
|
+
'Machine status read model. machine_revoked honors fleet_machines.reactivated_at (#146): a machine reactivated after its revocation reports machine_revoked=false, agreeing with evaluate_fleet_sender_policy.';
|