@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,443 @@
|
|
|
1
|
+
-- CLAIMGATE Phase 2 — SHADOW (#133): fleet-graph authority predicate + shadow stamping.
|
|
2
|
+
-- BLOCKS NOTHING on authority. Introduces `_can_claim_resource(kind, key)` (IF-0-SHADOW-1,
|
|
3
|
+
-- the frozen signature ENFORCE consumes unchanged), computes it in `acquire_claim`, and stamps
|
|
4
|
+
-- the verdict into `claims.metadata.claimgate_shadow = {kind, authorized}` so we get a live
|
|
5
|
+
-- authorized-vs-denied census BEFORE any enforcement.
|
|
6
|
+
--
|
|
7
|
+
-- This phase carries the OWNERLESS-tail guard (file/tool/custom, which has no fleet authority to
|
|
8
|
+
-- gate it): a per-agent active-claim QUOTA + a max-lease-horizon TTL CAP, in `acquire_claim` (NOT
|
|
9
|
+
-- in the predicate — the predicate consults ONLY the fleet graph; see NEGATIVE RULE below). Per
|
|
10
|
+
-- the design-panel amendment (specs/phase-plans-v10.md:329) the logic lives in SHADOW, but per the
|
|
11
|
+
-- census amendment (:333) it ships GATED OFF: the guard is behind `_claimgate_ownerless_guards_
|
|
12
|
+
-- enabled()`, which DEFAULTS FALSE, so SHADOW literally blocks NOTHING until an operator enables
|
|
13
|
+
-- it after the prod lease-length census confirms the cap is safe. (Maintainer decision 2026-07-31,
|
|
14
|
+
-- resolving codex's must-fix: enforcing 30d/50 unconditionally could reject a legit long-lease
|
|
15
|
+
-- consumer before the census.) The serialization + cap/quota code is in place for the flip.
|
|
16
|
+
--
|
|
17
|
+
-- =====================================================================================
|
|
18
|
+
-- AUTHORITY MODEL (fleet graph ONLY — NEGATIVE RULE: no board-admin fallback, EVER)
|
|
19
|
+
-- =====================================================================================
|
|
20
|
+
-- service_role -> true (operator carve-out; short-circuit).
|
|
21
|
+
-- machine (key = <machine_fingerprint>, bare per IF-0-CLAIMKEY-1)
|
|
22
|
+
-- authorized iff an ACTIVE fleet_machines row with that fingerprint exists in an ACTIVE
|
|
23
|
+
-- trust domain AND the actor either OWNS it (fleet_machines.owner_agent_id) or holds an
|
|
24
|
+
-- ACTIVE fleet_machine_agents binding on it. RATIFIED identity invariant: machine_fingerprint
|
|
25
|
+
-- is GLOBALLY UNIQUE (idx_agent_board_fleet_machines_fingerprint_unique) — so `<fp>` is
|
|
26
|
+
-- unambiguous; no owner-in-key hedge.
|
|
27
|
+
-- repository (key = <repo_fingerprint>) / repo_file (key = <repo_fingerprint>:<path>; the
|
|
28
|
+
-- FIRST ':' splits fp|path — same parse position CLAIMKEY froze)
|
|
29
|
+
-- authorized iff the actor holds an ACTIVE fleet_machine_agents binding whose repo_scope_id
|
|
30
|
+
-- resolves to an ACTIVE fleet_repo_scopes with repo_fingerprint = fp AND scope_role <>
|
|
31
|
+
-- 'observer', AND the FULL chain up from that scope is lifecycle-active at EVERY edge:
|
|
32
|
+
-- repo_scope -> harness_registration -> machine -> trust_domain.
|
|
33
|
+
-- CRITICAL PANEL AMENDMENT (specs/phase-plans-v10.md:323-328): revoke_fleet_subject('harness',…)
|
|
34
|
+
-- does NOT cascade to child repo-scopes, so a harness-only revoke leaves the scope 'active'
|
|
35
|
+
-- yet the harness dead — the chain walk MUST re-check fhr.lifecycle_state='active' or a
|
|
36
|
+
-- harness-revoked actor is wrongly authorized. RATIFIED: repo_fingerprint is NOT globally
|
|
37
|
+
-- unique, so authority over `<fp>` is the UNION of all non-observer scope holders across ALL
|
|
38
|
+
-- trust domains (shared-lock model) — the predicate does NOT scope by domain in the key.
|
|
39
|
+
-- task -> true (governed by acquire_claim's existing task gate).
|
|
40
|
+
-- file / tool / custom -> true (ownerless tail; governed by the TTL cap + quota
|
|
41
|
+
-- in acquire_claim, NOT by fleet authority).
|
|
42
|
+
--
|
|
43
|
+
-- SHADOW SEMANTICS: the predicate returns the HONEST verdict (a null/unresolved principal or a
|
|
44
|
+
-- non-fleet actor on a fleet-backed kind returns FALSE), but acquire_claim only STAMPS it — it
|
|
45
|
+
-- does not block on it. Fail-closed-on-unknown-KIND is an ENFORCE concern; here the catalog is
|
|
46
|
+
-- fully enumerated (7 kinds, all with an arm) and the defensive else returns true (blocks nothing).
|
|
47
|
+
--
|
|
48
|
+
-- #118 class (CLAUDE.md schema-mechanics): every NEWLY-DEFINED SECURITY DEFINER function below
|
|
49
|
+
-- restates `revoke ... from public, anon`, `grant ... to authenticated/service_role`, and a
|
|
50
|
+
-- `comment` in THIS migration. acquire_claim is a create-or-replace of an existing function, so
|
|
51
|
+
-- it preserves its prior ACL/comment and restates only revoke+grant (it carries no historical
|
|
52
|
+
-- comment to restate). Additive, forward-only. schema_version stays 8.
|
|
53
|
+
--
|
|
54
|
+
-- =====================================================================================
|
|
55
|
+
-- OWNERLESS-TAIL CENSUS (one-shot; run against the TARGET DB before relying on the TTL cap)
|
|
56
|
+
-- =====================================================================================
|
|
57
|
+
-- The TTL cap must not silently break a live long-lease consumer. This DB (local replay) has no
|
|
58
|
+
-- live claims; the maintainer runs this against the prod project to confirm the lease-length
|
|
59
|
+
-- distribution sits under the cap. This census GATES ENABLING the flag: only after it confirms the
|
|
60
|
+
-- distribution is safe does an operator set claimgate_enforcement_flags('ownerless_guards', true)
|
|
61
|
+
-- (or ENFORCE flip it with ratified values). Until then the guard is OFF and blocks nothing.
|
|
62
|
+
-- (Committed as the artifact for the SHADOW census exit criterion, mirroring CLAIMKEY's census.)
|
|
63
|
+
--
|
|
64
|
+
-- select resource_kind,
|
|
65
|
+
-- count(*) as active_claims,
|
|
66
|
+
-- min(lease_expires_at - now()) as min_remaining,
|
|
67
|
+
-- max(lease_expires_at - now()) as max_remaining,
|
|
68
|
+
-- percentile_disc(0.5) within group (order by lease_expires_at - now()) as p50_remaining,
|
|
69
|
+
-- percentile_disc(0.95) within group (order by lease_expires_at - now()) as p95_remaining,
|
|
70
|
+
-- count(*) filter (
|
|
71
|
+
-- where lease_expires_at > now() + agent_board._claimgate_ownerless_ttl_cap()
|
|
72
|
+
-- ) as over_cap
|
|
73
|
+
-- from agent_board.claims
|
|
74
|
+
-- where status = 'active' and released_at is null
|
|
75
|
+
-- and resource_kind in ('file', 'tool', 'custom')
|
|
76
|
+
-- group by resource_kind
|
|
77
|
+
-- order by resource_kind;
|
|
78
|
+
-- =====================================================================================
|
|
79
|
+
|
|
80
|
+
-- 0. Single-source policy constants for the ownerless tail. Immutable so the migration logic
|
|
81
|
+
-- AND the tests/isolation specs reference ONE value; ENFORCE tunes them here (and the max
|
|
82
|
+
-- horizon amendment extends the cap to acquire+renew for all non-task kinds). The chosen
|
|
83
|
+
-- defaults are GENEROUS placeholders pending the prod census above — flagged as a design
|
|
84
|
+
-- question, not a settled policy number.
|
|
85
|
+
create or replace function agent_board._claimgate_ownerless_ttl_cap()
|
|
86
|
+
returns interval
|
|
87
|
+
language sql
|
|
88
|
+
immutable
|
|
89
|
+
set search_path = ''
|
|
90
|
+
as $$ select interval '30 days' $$;
|
|
91
|
+
revoke all on function agent_board._claimgate_ownerless_ttl_cap() from public, anon;
|
|
92
|
+
grant execute on function agent_board._claimgate_ownerless_ttl_cap() to authenticated, service_role;
|
|
93
|
+
comment on function agent_board._claimgate_ownerless_ttl_cap() is
|
|
94
|
+
'CLAIMGATE/SHADOW (#133): max lease horizon for an ownerless (file/tool/custom) claim at acquire. '
|
|
95
|
+
'Single source for the migration logic and its tests. GENEROUS placeholder (30d) pending prod census; '
|
|
96
|
+
'ENFORCE ratifies the final value and extends the horizon to renew for all non-task kinds.';
|
|
97
|
+
|
|
98
|
+
create or replace function agent_board._claimgate_ownerless_quota()
|
|
99
|
+
returns integer
|
|
100
|
+
language sql
|
|
101
|
+
immutable
|
|
102
|
+
set search_path = ''
|
|
103
|
+
as $$ select 50 $$;
|
|
104
|
+
revoke all on function agent_board._claimgate_ownerless_quota() from public, anon;
|
|
105
|
+
grant execute on function agent_board._claimgate_ownerless_quota() to authenticated, service_role;
|
|
106
|
+
comment on function agent_board._claimgate_ownerless_quota() is
|
|
107
|
+
'CLAIMGATE/SHADOW (#133): max concurrent ACTIVE ownerless (file/tool/custom) claims per agent. '
|
|
108
|
+
'Single source for the migration logic and its tests. GENEROUS placeholder (50) pending prod census.';
|
|
109
|
+
|
|
110
|
+
-- 0b. Enforcement gate — OFF BY DEFAULT (SHADOW blocks nothing). ------------------------
|
|
111
|
+
-- The roadmap (phase-plans-v10.md:333) requires the live lease-length census BEFORE the TTL cap
|
|
112
|
+
-- is enabled, and the census is operator-owned (prod-only). So the ownerless TTL cap + quota are
|
|
113
|
+
-- gated behind this flag, which DEFAULTS OFF (absent row => disabled), mirroring the send-authz
|
|
114
|
+
-- config-flag pattern (20260718000100, which defaults fail-open). While OFF, acquire_claim does
|
|
115
|
+
-- NOT enforce the cap or quota — SHADOW blocks NOTHING; only the shadow-stamp remains. The
|
|
116
|
+
-- operator flips it ON post-census (or ENFORCE flips it with ratified values); the advisory-lock
|
|
117
|
+
-- serialization and the cap/quota logic below are already in place for that moment.
|
|
118
|
+
create table if not exists agent_board.claimgate_enforcement_flags (
|
|
119
|
+
scope_key text primary key,
|
|
120
|
+
enabled boolean not null default false,
|
|
121
|
+
updated_at timestamptz not null default now()
|
|
122
|
+
);
|
|
123
|
+
comment on table agent_board.claimgate_enforcement_flags is
|
|
124
|
+
'CLAIMGATE per-scope enforcement flags. Absent row => disabled (SHADOW default: blocks nothing). '
|
|
125
|
+
'scope_key ''ownerless_guards'' gates the ownerless (file/tool/custom) TTL cap + per-agent quota in '
|
|
126
|
+
'acquire_claim. Operator-managed (service_role); enabled only after the prod lease-length census.';
|
|
127
|
+
alter table agent_board.claimgate_enforcement_flags enable row level security;
|
|
128
|
+
grant select, insert, update, delete on agent_board.claimgate_enforcement_flags to service_role;
|
|
129
|
+
drop policy if exists agent_board_service_all_claimgate_enforcement_flags on agent_board.claimgate_enforcement_flags;
|
|
130
|
+
create policy agent_board_service_all_claimgate_enforcement_flags
|
|
131
|
+
on agent_board.claimgate_enforcement_flags
|
|
132
|
+
as permissive for all to service_role using (true) with check (true);
|
|
133
|
+
|
|
134
|
+
create or replace function agent_board._claimgate_ownerless_guards_enabled()
|
|
135
|
+
returns boolean
|
|
136
|
+
language sql
|
|
137
|
+
stable
|
|
138
|
+
security definer
|
|
139
|
+
set search_path = ''
|
|
140
|
+
as $$
|
|
141
|
+
select coalesce(
|
|
142
|
+
(select f.enabled
|
|
143
|
+
from agent_board.claimgate_enforcement_flags f
|
|
144
|
+
where f.scope_key = 'ownerless_guards'),
|
|
145
|
+
false)
|
|
146
|
+
$$;
|
|
147
|
+
revoke all on function agent_board._claimgate_ownerless_guards_enabled() from public, anon;
|
|
148
|
+
grant execute on function agent_board._claimgate_ownerless_guards_enabled() to authenticated, service_role;
|
|
149
|
+
comment on function agent_board._claimgate_ownerless_guards_enabled() is
|
|
150
|
+
'CLAIMGATE/SHADOW (#133): are the ownerless (file/tool/custom) TTL cap + quota ENABLED? '
|
|
151
|
+
'Reads claimgate_enforcement_flags(''ownerless_guards''); DEFAULTS FALSE (no row) so SHADOW blocks '
|
|
152
|
+
'nothing until an operator enables it post-census. SECURITY DEFINER; PUBLIC EXECUTE revoked.';
|
|
153
|
+
|
|
154
|
+
-- 1. _can_claim_resource — the frozen SHADOW authority predicate (IF-0-SHADOW-1) -----------
|
|
155
|
+
-- SECURITY DEFINER to break fleet-table RLS recursion (the fleet SELECT policies reference
|
|
156
|
+
-- each other). Session-derived actor via current_agent_id(); the actor-parameterized private
|
|
157
|
+
-- twin (for censusing EXISTING held claims) is an ENFORCE requirement, deferred.
|
|
158
|
+
create or replace function agent_board._can_claim_resource(
|
|
159
|
+
p_resource_kind text,
|
|
160
|
+
p_resource_key text
|
|
161
|
+
)
|
|
162
|
+
returns boolean
|
|
163
|
+
language plpgsql
|
|
164
|
+
stable
|
|
165
|
+
security definer
|
|
166
|
+
set search_path = 'agent_board', 'public'
|
|
167
|
+
as $function$
|
|
168
|
+
declare
|
|
169
|
+
v_actor uuid;
|
|
170
|
+
v_fp text;
|
|
171
|
+
v_sep int;
|
|
172
|
+
begin
|
|
173
|
+
-- Operator carve-out: service_role short-circuits authorized (matches every other gate).
|
|
174
|
+
if agent_board._is_service_role() then
|
|
175
|
+
return true;
|
|
176
|
+
end if;
|
|
177
|
+
|
|
178
|
+
v_actor := agent_board.current_agent_id();
|
|
179
|
+
-- An unresolved principal is honestly UNAUTHORIZED on any fleet-backed kind. (Blocks nothing
|
|
180
|
+
-- in SHADOW — acquire_claim only stamps this verdict.) The ownerless tail + task are not
|
|
181
|
+
-- fleet-gated, so they remain authorized even with a null actor.
|
|
182
|
+
if p_resource_kind = 'task' then
|
|
183
|
+
return true;
|
|
184
|
+
elsif p_resource_kind in ('file', 'tool', 'custom') then
|
|
185
|
+
return true;
|
|
186
|
+
end if;
|
|
187
|
+
|
|
188
|
+
if v_actor is null then
|
|
189
|
+
return false;
|
|
190
|
+
end if;
|
|
191
|
+
|
|
192
|
+
if p_resource_kind = 'machine' then
|
|
193
|
+
v_fp := btrim(p_resource_key);
|
|
194
|
+
if v_fp is null or v_fp = '' then
|
|
195
|
+
return false;
|
|
196
|
+
end if;
|
|
197
|
+
-- machine_fingerprint is globally unique; the machine AND its trust domain must be active,
|
|
198
|
+
-- and the actor must own it OR hold an active binding on it.
|
|
199
|
+
return exists (
|
|
200
|
+
select 1
|
|
201
|
+
from agent_board.fleet_machines fm
|
|
202
|
+
join agent_board.fleet_trust_domains ftd
|
|
203
|
+
on ftd.id = fm.trust_domain_id
|
|
204
|
+
where fm.machine_fingerprint = v_fp
|
|
205
|
+
and fm.lifecycle_state = 'active'
|
|
206
|
+
and ftd.lifecycle_state = 'active'
|
|
207
|
+
and (
|
|
208
|
+
fm.owner_agent_id = v_actor
|
|
209
|
+
or exists (
|
|
210
|
+
select 1
|
|
211
|
+
from agent_board.fleet_machine_agents fma
|
|
212
|
+
where fma.machine_id = fm.id
|
|
213
|
+
and fma.agent_id = v_actor
|
|
214
|
+
and fma.lifecycle_state = 'active'
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
elsif p_resource_kind = 'repository' or p_resource_kind = 'repo_file' then
|
|
220
|
+
if p_resource_kind = 'repo_file' then
|
|
221
|
+
-- FIRST ':' splits fp | path (the exact position CLAIMKEY froze). No colon / empty fp =
|
|
222
|
+
-- not resolvable -> deny (honest verdict; blocks nothing in SHADOW).
|
|
223
|
+
v_sep := position(':' in coalesce(p_resource_key, ''));
|
|
224
|
+
if v_sep <= 1 then
|
|
225
|
+
return false;
|
|
226
|
+
end if;
|
|
227
|
+
v_fp := btrim(substring(p_resource_key from 1 for v_sep - 1));
|
|
228
|
+
else
|
|
229
|
+
v_fp := btrim(p_resource_key);
|
|
230
|
+
end if;
|
|
231
|
+
if v_fp is null or v_fp = '' then
|
|
232
|
+
return false;
|
|
233
|
+
end if;
|
|
234
|
+
-- FULL chain, EVERY edge lifecycle-active (PANEL AMENDMENT: harness revoke does not cascade
|
|
235
|
+
-- to child scopes, so fhr.lifecycle_state='active' is load-bearing here). Anchored on the
|
|
236
|
+
-- actor's active binding + its repo_scope; UNION across trust domains (shared-lock model).
|
|
237
|
+
return exists (
|
|
238
|
+
select 1
|
|
239
|
+
from agent_board.fleet_machine_agents fma
|
|
240
|
+
join agent_board.fleet_repo_scopes frs
|
|
241
|
+
on frs.id = fma.repo_scope_id
|
|
242
|
+
join agent_board.fleet_harness_registrations fhr
|
|
243
|
+
on fhr.id = frs.harness_registration_id
|
|
244
|
+
join agent_board.fleet_machines fm
|
|
245
|
+
on fm.id = fhr.machine_id
|
|
246
|
+
join agent_board.fleet_trust_domains ftd
|
|
247
|
+
on ftd.id = fm.trust_domain_id
|
|
248
|
+
where fma.agent_id = v_actor
|
|
249
|
+
and fma.lifecycle_state = 'active'
|
|
250
|
+
and frs.repo_fingerprint = v_fp
|
|
251
|
+
and frs.scope_role <> 'observer'
|
|
252
|
+
and frs.lifecycle_state = 'active'
|
|
253
|
+
and fhr.lifecycle_state = 'active'
|
|
254
|
+
and fm.lifecycle_state = 'active'
|
|
255
|
+
and ftd.lifecycle_state = 'active'
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
else
|
|
259
|
+
-- Defensive: an unenumerated future kind. SHADOW blocks nothing; the completeness test
|
|
260
|
+
-- enumerates the 7 catalog kinds and asserts each is handled above, so this is unreachable
|
|
261
|
+
-- for today's constraint and never masks a missing arm.
|
|
262
|
+
return true;
|
|
263
|
+
end if;
|
|
264
|
+
end;
|
|
265
|
+
$function$;
|
|
266
|
+
revoke all on function agent_board._can_claim_resource(text, text) from public, anon;
|
|
267
|
+
grant execute on function agent_board._can_claim_resource(text, text) to authenticated, service_role;
|
|
268
|
+
comment on function agent_board._can_claim_resource(text, text) is
|
|
269
|
+
'CLAIMGATE/SHADOW (#133, IF-0-SHADOW-1): fleet-graph authority predicate ENFORCE consumes unchanged. '
|
|
270
|
+
'machine = actor owns/bound to the active machine in an active trust domain (fingerprint globally unique); '
|
|
271
|
+
'repository/repo_file = actor holds an active binding to an active non-observer repo_scope whose FULL chain '
|
|
272
|
+
'(scope->harness->machine->domain) is lifecycle-active (harness revoke does NOT cascade — checked explicitly); '
|
|
273
|
+
'task + file/tool/custom = true (gated elsewhere). Fleet graph ONLY — NO board-admin fallback. '
|
|
274
|
+
'service_role short-circuits true. SECURITY DEFINER to break fleet RLS recursion; PUBLIC EXECUTE revoked.';
|
|
275
|
+
|
|
276
|
+
-- 2. acquire_claim — compute + STAMP the shadow verdict; guard the ownerless tail -----------
|
|
277
|
+
-- Reproduced verbatim from 20260821000100 (CLAIMKEY) EXCEPT: (a) compute _can_claim_resource
|
|
278
|
+
-- after canonicalization and before the sweep (predicate-before-side-effects, so ENFORCE can
|
|
279
|
+
-- flip it fail-closed in place); (b) enforce the ownerless TTL cap + serialized quota; (c)
|
|
280
|
+
-- merge metadata.claimgate_shadow into the inserted row. BLOCKS NOTHING on authority.
|
|
281
|
+
create or replace function agent_board.acquire_claim(
|
|
282
|
+
p_agent_id uuid,
|
|
283
|
+
p_resource_kind text,
|
|
284
|
+
p_resource_key text,
|
|
285
|
+
p_lease_expires_at timestamp with time zone,
|
|
286
|
+
p_task_id uuid default null,
|
|
287
|
+
p_thread_id uuid default null,
|
|
288
|
+
p_metadata jsonb default '{}'::jsonb
|
|
289
|
+
)
|
|
290
|
+
returns table(claim_id uuid, task_id uuid, thread_id uuid, agent_id uuid, resource_kind text, resource_key text, status text, lease_token uuid, lease_expires_at timestamp with time zone, released_at timestamp with time zone, created_at timestamp with time zone, updated_at timestamp with time zone)
|
|
291
|
+
language plpgsql
|
|
292
|
+
security definer
|
|
293
|
+
set search_path to 'agent_board', 'public'
|
|
294
|
+
as $function$
|
|
295
|
+
declare
|
|
296
|
+
v_thread_id uuid;
|
|
297
|
+
v_claim_id uuid;
|
|
298
|
+
v_actor_agent_id uuid;
|
|
299
|
+
v_shadow_authorized boolean;
|
|
300
|
+
v_ownerless boolean;
|
|
301
|
+
v_active_ownerless int;
|
|
302
|
+
begin
|
|
303
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
|
|
304
|
+
|
|
305
|
+
if nullif(trim(p_resource_key), '') is null then
|
|
306
|
+
raise exception 'resource key is required' using errcode = '22023';
|
|
307
|
+
end if;
|
|
308
|
+
|
|
309
|
+
-- CLAIMKEY (#133): normalize the resource key to its canonical, fleet-fingerprint-carrying
|
|
310
|
+
-- form BEFORE any lookup/insert, so the row stored (and the uniqueness index contended)
|
|
311
|
+
-- carry canonical identity. NORMALIZE-ONLY — this never rejects (rejection is ENFORCE).
|
|
312
|
+
p_resource_key := agent_board._canonical_claim_key(p_resource_kind, p_resource_key);
|
|
313
|
+
|
|
314
|
+
if p_task_id is not null then
|
|
315
|
+
select tasks.thread_id into v_thread_id from agent_board.tasks where id = p_task_id;
|
|
316
|
+
if v_thread_id is null then
|
|
317
|
+
raise exception 'task not found' using errcode = 'P0002';
|
|
318
|
+
end if;
|
|
319
|
+
if not agent_board._current_agent_can_access_task(p_task_id) then
|
|
320
|
+
raise exception 'task not accessible to current agent' using errcode = '42501';
|
|
321
|
+
end if;
|
|
322
|
+
else
|
|
323
|
+
v_thread_id := p_thread_id;
|
|
324
|
+
end if;
|
|
325
|
+
|
|
326
|
+
if v_thread_id is not null and not agent_board._current_agent_can_access_thread(v_thread_id) then
|
|
327
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
328
|
+
end if;
|
|
329
|
+
|
|
330
|
+
-- OBSGATE-4 (#127): a claim on a TASK resource is authorized on the LOCKED task = the task
|
|
331
|
+
-- named by RESOURCE_KEY (resource_kind='task'). The active-resource uniqueness is on
|
|
332
|
+
-- (resource_kind, resource_key), so that is the task a squat contends for — gating resource_key
|
|
333
|
+
-- (NOT p_task_id, which only associates the claim/event) prevents locking task B's key while
|
|
334
|
+
-- authorizing on an unrelated task A passed as p_task_id. A non-uuid 'task' key cannot collide
|
|
335
|
+
-- with a uuid-keyed legitimate claim.
|
|
336
|
+
--
|
|
337
|
+
-- The composition MIRRORS append_task_event's: ACCESS then MUTATE. mutate alone is
|
|
338
|
+
-- insufficient — _current_agent_can_mutate_task admits any non-observer board member via
|
|
339
|
+
-- _current_agent_can_post_to_thread (board send-role), which is NOT thread access; on a
|
|
340
|
+
-- RESTRICTED thread a board member lacking thread access would pass mutate but must still be
|
|
341
|
+
-- denied (they cannot access the task). Requiring access too closes that gap and matches the
|
|
342
|
+
-- authorization a task-BOUND acquire already gets through append_task_event.
|
|
343
|
+
if p_resource_kind = 'task'
|
|
344
|
+
and p_resource_key ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' then
|
|
345
|
+
if not agent_board._current_agent_can_access_task(p_resource_key::uuid) then
|
|
346
|
+
raise exception 'task not accessible to current agent' using errcode = '42501';
|
|
347
|
+
end if;
|
|
348
|
+
if not agent_board._current_agent_can_mutate_task(p_resource_key::uuid) then
|
|
349
|
+
raise exception 'claiming a task resource requires a non-observer board role, or task assignment/creation'
|
|
350
|
+
using errcode = '42501';
|
|
351
|
+
end if;
|
|
352
|
+
end if;
|
|
353
|
+
|
|
354
|
+
-- CLAIMGATE/SHADOW (#133): compute the fleet-graph authority verdict BEFORE the sweep (so
|
|
355
|
+
-- ENFORCE can flip this same point fail-closed with no observable side effect leaking to an
|
|
356
|
+
-- unauthorized caller). SHADOW BLOCKS NOTHING on this verdict — it is only stamped below.
|
|
357
|
+
v_shadow_authorized := agent_board._can_claim_resource(p_resource_kind, p_resource_key);
|
|
358
|
+
|
|
359
|
+
-- Ownerless tail (file/tool/custom): the ONE guard SHADOW carries — GATED OFF BY DEFAULT.
|
|
360
|
+
-- _claimgate_ownerless_guards_enabled() is false unless an operator flips it post-census, so in
|
|
361
|
+
-- SHADOW this block is skipped entirely and NOTHING is blocked (only the stamp above lands).
|
|
362
|
+
-- service_role is also exempt (operator carve-out, consistent with the predicate short-circuit).
|
|
363
|
+
v_ownerless := p_resource_kind in ('file', 'tool', 'custom');
|
|
364
|
+
if v_ownerless
|
|
365
|
+
and not agent_board._is_service_role()
|
|
366
|
+
and agent_board._claimgate_ownerless_guards_enabled() then
|
|
367
|
+
-- TTL CAP: an ownerless lease may not exceed the max horizon. Rejects an over-long acquire;
|
|
368
|
+
-- this is a lease-length bound, NOT an authority block.
|
|
369
|
+
if p_lease_expires_at > now() + agent_board._claimgate_ownerless_ttl_cap() then
|
|
370
|
+
raise exception 'ownerless claim lease horizon exceeds cap (%)', agent_board._claimgate_ownerless_ttl_cap()
|
|
371
|
+
using errcode = '22023';
|
|
372
|
+
end if;
|
|
373
|
+
|
|
374
|
+
-- QUOTA (transactionally serialized): an actor-scoped advisory XACT lock serializes this
|
|
375
|
+
-- actor's concurrent acquisitions (on DIFFERENT keys, which the row-level uniqueness index
|
|
376
|
+
-- does NOT), so a count-before-insert cannot race two claims past the cap. The lock releases
|
|
377
|
+
-- at commit; it does not block other actors (the key mixes in the actor id).
|
|
378
|
+
perform pg_advisory_xact_lock(hashtext('agent_board.claimgate_ownerless_quota'), hashtext(v_actor_agent_id::text));
|
|
379
|
+
select count(*) into v_active_ownerless
|
|
380
|
+
from agent_board.claims c
|
|
381
|
+
where c.agent_id = v_actor_agent_id
|
|
382
|
+
and c.resource_kind in ('file', 'tool', 'custom')
|
|
383
|
+
and c.status = 'active'
|
|
384
|
+
and c.released_at is null
|
|
385
|
+
and c.lease_expires_at > now();
|
|
386
|
+
if v_active_ownerless >= agent_board._claimgate_ownerless_quota() then
|
|
387
|
+
raise exception 'ownerless active-claim quota reached (% per agent)', agent_board._claimgate_ownerless_quota()
|
|
388
|
+
using errcode = '54000';
|
|
389
|
+
end if;
|
|
390
|
+
end if;
|
|
391
|
+
|
|
392
|
+
-- CLAIMKEY (#133) MUST-FIX 3: the sweep AND the conflict check are ALIAS-AWARE. p_resource_key
|
|
393
|
+
-- is already canonical (above); comparing against the CANONICAL form of each existing row's key
|
|
394
|
+
-- matches both a canonical row AND an un-healed LEGACY row for the same physical resource. Without
|
|
395
|
+
-- this, acquire('machine','machine:fpDH') would normalize to 'fpDH', see no exact-'fpDH' conflict
|
|
396
|
+
-- against a still-active legacy 'machine:fpDH' row, and INSERT a second live hold — breaking mutual
|
|
397
|
+
-- exclusion during the legacy window. Closing it here in code does NOT depend on the operator-owned
|
|
398
|
+
-- live census.
|
|
399
|
+
update agent_board.claims c
|
|
400
|
+
set status = 'expired',
|
|
401
|
+
updated_at = now()
|
|
402
|
+
where c.resource_kind = p_resource_kind
|
|
403
|
+
and agent_board._canonical_claim_key(c.resource_kind, c.resource_key) = p_resource_key
|
|
404
|
+
and c.status = 'active'
|
|
405
|
+
and c.released_at is null
|
|
406
|
+
and c.lease_expires_at <= now();
|
|
407
|
+
|
|
408
|
+
if exists (
|
|
409
|
+
select 1 from agent_board.claims
|
|
410
|
+
where claims.resource_kind = p_resource_kind
|
|
411
|
+
and agent_board._canonical_claim_key(claims.resource_kind, claims.resource_key) = p_resource_key
|
|
412
|
+
and claims.status = 'active'
|
|
413
|
+
and claims.released_at is null
|
|
414
|
+
and claims.lease_expires_at > now()
|
|
415
|
+
) then
|
|
416
|
+
raise exception 'conflicting active claim' using errcode = '23505';
|
|
417
|
+
end if;
|
|
418
|
+
|
|
419
|
+
insert into agent_board.claims(task_id, thread_id, agent_id, resource_kind, resource_key, lease_expires_at, metadata)
|
|
420
|
+
values (
|
|
421
|
+
p_task_id, v_thread_id, v_actor_agent_id, p_resource_kind, p_resource_key, p_lease_expires_at,
|
|
422
|
+
-- SHADOW stamp: record the authority verdict alongside any caller metadata. Merged last so it
|
|
423
|
+
-- is authoritative over a caller-supplied claimgate_shadow key.
|
|
424
|
+
coalesce(p_metadata, '{}'::jsonb)
|
|
425
|
+
|| jsonb_build_object(
|
|
426
|
+
'claimgate_shadow',
|
|
427
|
+
jsonb_build_object('kind', p_resource_kind, 'authorized', v_shadow_authorized)
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
returning id into v_claim_id;
|
|
431
|
+
|
|
432
|
+
if p_task_id is not null then
|
|
433
|
+
perform agent_board.append_task_event(p_task_id, v_actor_agent_id, 'claimed', jsonb_build_object('claim_id', v_claim_id, 'resource_kind', p_resource_kind, 'resource_key', p_resource_key));
|
|
434
|
+
end if;
|
|
435
|
+
|
|
436
|
+
return query
|
|
437
|
+
select c.id, c.task_id, c.thread_id, c.agent_id, c.resource_kind, c.resource_key, c.status, c.lease_token, c.lease_expires_at, c.released_at, c.created_at, c.updated_at
|
|
438
|
+
from agent_board.claims c
|
|
439
|
+
where c.id = v_claim_id;
|
|
440
|
+
end;
|
|
441
|
+
$function$;
|
|
442
|
+
revoke all on function agent_board.acquire_claim(uuid, text, text, timestamp with time zone, uuid, uuid, jsonb) from public, anon;
|
|
443
|
+
grant execute on function agent_board.acquire_claim(uuid, text, text, timestamp with time zone, uuid, uuid, jsonb) to authenticated, service_role;
|