@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.
Files changed (156) hide show
  1. package/fixtures/already-applied.json +160 -0
  2. package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
  3. package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
  4. package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
  5. package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
  6. package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
  7. package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
  8. package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
  9. package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
  10. package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
  11. package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
  12. package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
  13. package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
  14. package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
  15. package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
  16. package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
  17. package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
  18. package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
  19. package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
  20. package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
  21. package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
  22. package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
  23. package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
  24. package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
  25. package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
  26. package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
  27. package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
  28. package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
  29. package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
  30. package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
  31. package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
  32. package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
  33. package/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
  34. package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
  35. package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
  36. package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
  37. package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
  38. package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
  39. package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
  40. package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
  41. package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
  42. package/package.json +1 -1
  43. package/schema-manifest.json +363 -3
  44. package/src/tooling.test.mjs +113 -2
  45. package/tests/agent_board_actorspoof.test.sql +349 -0
  46. package/tests/agent_board_agentsdir.test.sql +154 -0
  47. package/tests/agent_board_atomic_fleet_onboarding.test.sql +623 -0
  48. package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
  49. package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -0
  50. package/tests/agent_board_authbound_boundary.test.sql +9 -1
  51. package/tests/agent_board_claimgate_enforce.test.sql +464 -0
  52. package/tests/agent_board_claimgate_predicate.test.sql +360 -0
  53. package/tests/agent_board_claimkey.test.sql +229 -0
  54. package/tests/agent_board_consent_backfill.test.sql +48 -0
  55. package/tests/agent_board_consent_emission.test.sql +601 -0
  56. package/tests/agent_board_consent_gating.test.sql +489 -0
  57. package/tests/agent_board_consent_substrate.test.sql +720 -0
  58. package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
  59. package/tests/agent_board_delegation_enforcement.test.sql +55 -6
  60. package/tests/agent_board_delegation_policies.test.sql +107 -1
  61. package/tests/agent_board_enroll_federated_identity.test.sql +3 -3
  62. package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
  63. package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
  64. package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
  65. package/tests/agent_board_fedsession.test.sql +51 -5
  66. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
  67. package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
  68. package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
  69. package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
  70. package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
  71. package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
  72. package/tests/agent_board_fleet_policy.test.sql +2 -2
  73. package/tests/agent_board_grants_hygiene.test.sql +408 -0
  74. package/tests/agent_board_identity_providers.test.sql +9 -0
  75. package/tests/agent_board_inbox_reads.test.sql +45 -31
  76. package/tests/agent_board_inbox_rpcs.test.sql +44 -34
  77. package/tests/agent_board_invitepolicy.test.sql +205 -0
  78. package/tests/agent_board_leaveidem.test.sql +194 -0
  79. package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
  80. package/tests/agent_board_mint_harden.test.sql +303 -0
  81. package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
  82. package/tests/agent_board_obsgate.test.sql +178 -0
  83. package/tests/agent_board_obsgate2.test.sql +153 -0
  84. package/tests/agent_board_obsgate3.test.sql +170 -0
  85. package/tests/agent_board_obsgate4.test.sql +196 -0
  86. package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
  87. package/tests/agent_board_portal_offline_invariant.test.sql +14 -3
  88. package/tests/agent_board_pubexec_class.test.sql +53 -0
  89. package/tests/agent_board_rejectsem.test.sql +271 -0
  90. package/tests/agent_board_resigning_refresh.test.sql +1 -1
  91. package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
  92. package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
  93. package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
  94. package/tests/agent_board_rolereset.test.sql +351 -0
  95. package/tests/agent_board_roomdeleg.test.sql +5 -3
  96. package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
  97. package/tests/agent_board_roomlink.test.sql +27 -11
  98. package/tests/agent_board_rowid.test.sql +1274 -0
  99. package/tests/agent_board_signature_rls_reads.test.sql +3 -3
  100. package/tests/agent_board_signed_write_gateway.test.sql +3 -3
  101. package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
  102. package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
  103. package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
  104. package/tests/agent_board_signing_store.test.sql +6 -6
  105. package/tests/agent_board_wake_recovery.test.sql +756 -0
  106. package/tests/fixtures/isolation-substrate.sql +93 -0
  107. package/tests/fixtures/test-prereqs.sql +33 -0
  108. package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
  109. package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
  110. package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
  111. package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
  112. package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
  113. package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
  114. package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
  115. package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
  116. package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
  117. package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
  118. package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -0
  119. package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
  120. package/tests/isolation/expected/claimkey_renew_race.out +38 -0
  121. package/tests/isolation/expected/consent_approval_lock.out +43 -0
  122. package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
  123. package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
  124. package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +55 -0
  125. package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
  126. package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
  127. package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
  128. package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
  129. package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
  130. package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
  131. package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
  132. package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
  133. package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
  134. package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
  135. package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
  136. package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
  137. package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
  138. package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
  139. package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
  140. package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
  141. package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -0
  142. package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
  143. package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
  144. package/tests/isolation/specs/consent_approval_lock.spec +98 -0
  145. package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
  146. package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
  147. package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +117 -0
  148. package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
  149. package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
  150. package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
  151. package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
  152. package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
  153. package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -0
  154. package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
  155. package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
  156. package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
@@ -0,0 +1,207 @@
1
+ -- =============================================================================
2
+ -- MINTHARDEN (#138) — data-derived signing-key fingerprint BACKFILL.
3
+ -- =============================================================================
4
+ -- 20260822000100 routed every fingerprint WRITER through _raw_key_fingerprint (raw
5
+ -- 32 bytes) and installed a table-boundary integrity trigger. This migration corrects
6
+ -- the EXISTING rows: string-era mint child keys (20260718001000 derived over the
7
+ -- base64url STRING) and any stored non-canonical public_key alias. 20260719001200
8
+ -- shipped NO backfill, so string-era ENROLL rows are in scope too — the predicate is
9
+ -- data-derived (every raw row where the stored fingerprint/public_key diverges from the
10
+ -- canonical raw derivation), NOT a path- or key_id-scoped list (CLAUDE.md rule #1).
11
+ --
12
+ -- ⚠ OPERATOR RUNBOOK — READ BEFORE DEPLOYING TO A LIVE FLEET ⚠
13
+ -- Re-deriving a fingerprint to the canonical raw-byte value ALIGNS the DB with what
14
+ -- clients already compute (child_keygen keyRef + the enroll/session EFs derive the raw
15
+ -- fingerprint), so a correctly-behaving agent is NOT locked out by the re-derivation.
16
+ -- The two hazards are gated fail-closed:
17
+ --
18
+ -- 1. CROSS-OWNER COLLISION (fatal, always). If two keys owned by DIFFERENT owners
19
+ -- compute to the SAME raw fingerprint, one keypair backs two identities — live
20
+ -- squat evidence. The migration ABORTS with the collision listing and does NOT
21
+ -- auto-revoke. Resolve by hand FIRST: possession cannot distinguish the rightful
22
+ -- holder, so revoke ALL bindings for the colliding fingerprint (status='revoked')
23
+ -- and tombstone it (a permanently-revoked placeholder row) so it cannot be
24
+ -- re-registered, then re-run. Plan any agent re-keying BEFORE deploying.
25
+ --
26
+ -- 2. SAME-OWNER ALIAS DUPLICATES (gated). If the SAME owner holds the same key under
27
+ -- two rows (a canonical key + its non-canonical alias), the redundant alias rows
28
+ -- are DELETED (keeping the earliest) — a destructive step, so it runs ONLY when
29
+ -- the operator sets SET agent_board.fingerprint_backfill_dedup = 'on'; in the
30
+ -- migration session. Absent the guard, the migration ABORTS with the listing so
31
+ -- the operator decides explicitly. (A fresh replay has no such duplicates.)
32
+ --
33
+ -- The safe re-derivation runs unconditionally (it is corrective). schema_version stays 8.
34
+ -- =============================================================================
35
+
36
+ -- ---------------------------------------------------------------------------
37
+ -- 1. _raw_key_canonical — TOLERANT canonical form of a stored public_key (returns NULL
38
+ -- rather than raising on a non-32-byte value), so the backfill can canonicalize a
39
+ -- legacy alias without _raw_key_fingerprint's strict canonical rejection tripping.
40
+ -- ---------------------------------------------------------------------------
41
+ create or replace function agent_board._raw_key_canonical(p_public_key text)
42
+ returns text
43
+ language plpgsql
44
+ immutable
45
+ set search_path = agent_board, public
46
+ as $$
47
+ declare
48
+ v_raw bytea;
49
+ begin
50
+ if p_public_key is null then
51
+ return null;
52
+ end if;
53
+ begin
54
+ v_raw := decode(translate(p_public_key, '-_', '+/') || '=', 'base64');
55
+ exception when others then
56
+ return null;
57
+ end;
58
+ if octet_length(v_raw) <> 32 then
59
+ return null;
60
+ end if;
61
+ return rtrim(translate(encode(v_raw, 'base64'), '+/', '-_'), '=');
62
+ end;
63
+ $$;
64
+
65
+ comment on function agent_board._raw_key_canonical(text) is
66
+ 'Tolerant canonical base64url form of a raw Ed25519 public_key (NULL if it does not '
67
+ 'decode to 32 bytes). Backfill-only: lets a legacy non-canonical alias be canonicalized '
68
+ 'without _raw_key_fingerprint''s strict alias rejection aborting the pass.';
69
+
70
+ revoke all on function agent_board._raw_key_canonical(text) from public, anon, authenticated;
71
+ grant execute on function agent_board._raw_key_canonical(text) to service_role;
72
+
73
+ -- ---------------------------------------------------------------------------
74
+ -- 2. Cross-owner collision report (reusable + pgTAP-pinned). Groups raw signing keys by
75
+ -- their canonical raw-byte fingerprint and surfaces any fingerprint spanning >1 owner.
76
+ -- ---------------------------------------------------------------------------
77
+ create or replace function agent_board._raw_fingerprint_backfill_collisions()
78
+ returns table(
79
+ computed_fingerprint text,
80
+ owner_count bigint,
81
+ agent_ids uuid[],
82
+ owner_user_ids uuid[],
83
+ key_ids uuid[]
84
+ )
85
+ language sql
86
+ stable
87
+ security definer
88
+ set search_path = agent_board, public
89
+ as $$
90
+ with fp as (
91
+ select k.id as key_id, k.agent_id, a.owner_user_id,
92
+ agent_board._raw_key_fingerprint(agent_board._raw_key_canonical(k.public_key)) as computed_fingerprint
93
+ from agent_board.agent_signing_keys k
94
+ join agent_board.agents a on a.id = k.agent_id
95
+ where k.public_key_format = 'raw_ed25519_base64url'
96
+ and agent_board._raw_key_canonical(k.public_key) is not null
97
+ )
98
+ select computed_fingerprint,
99
+ count(distinct owner_user_id) as owner_count,
100
+ array_agg(distinct agent_id) as agent_ids,
101
+ array_agg(distinct owner_user_id) as owner_user_ids,
102
+ array_agg(key_id) as key_ids
103
+ from fp
104
+ group by computed_fingerprint
105
+ having count(distinct agent_id) > 1;
106
+ $$;
107
+
108
+ comment on function agent_board._raw_fingerprint_backfill_collisions() is
109
+ 'Backfill preflight: raw signing keys grouped by canonical raw-byte fingerprint where the '
110
+ 'group spans more than one AGENT (one keypair backing multiple identities — whether across '
111
+ 'owners or two agents of the same owner). Auto-dedup must never delete a DIFFERENT agent''s '
112
+ 'binding, so any such group aborts the backfill fail-closed for an operator decision.';
113
+
114
+ revoke all on function agent_board._raw_fingerprint_backfill_collisions() from public, anon, authenticated;
115
+ grant execute on function agent_board._raw_fingerprint_backfill_collisions() to service_role;
116
+
117
+ -- ---------------------------------------------------------------------------
118
+ -- 3. The backfill pass.
119
+ -- ---------------------------------------------------------------------------
120
+ do $$
121
+ declare
122
+ v_listing text;
123
+ v_dup_listing text;
124
+ v_dup_groups int;
125
+ v_deleted int := 0;
126
+ v_updated int := 0;
127
+ v_dedup_enabled boolean := (nullif(current_setting('agent_board.fingerprint_backfill_dedup', true), '') = 'on');
128
+ begin
129
+ -- (a) CROSS-IDENTITY collisions (one keypair spanning >1 AGENT, whether across owners
130
+ -- or two agents of one owner) -> fail closed (always). Auto-dedup must never delete a
131
+ -- DIFFERENT agent's active binding, so the operator resolves these by hand.
132
+ if exists (select 1 from agent_board._raw_fingerprint_backfill_collisions()) then
133
+ select string_agg(
134
+ format(' %s : agents %s owners %s (keys %s)',
135
+ computed_fingerprint, agent_ids, owner_user_ids, key_ids),
136
+ E'\n' order by computed_fingerprint)
137
+ into v_listing
138
+ from agent_board._raw_fingerprint_backfill_collisions();
139
+ raise exception E'FINGERPRINT BACKFILL ABORTED — one keypair backs multiple AGENTS (cross-identity collision). Resolve per the migration runbook (decide which agent keeps the key; revoke the other bindings + tombstone the fingerprint) BEFORE re-running:\n%', v_listing
140
+ using errcode = '23505';
141
+ end if;
142
+
143
+ -- (b) SAME-AGENT alias duplicates only. Group raw keys by (agent, canonical fingerprint);
144
+ -- a group with >1 row is the SAME agent holding the SAME key twice (a canonical row + its
145
+ -- alias) — safe to collapse. (Cross-agent sharing already aborted in (a), so every group
146
+ -- here is single-agent.) Deleting rows is destructive, so it is GATED behind an explicit
147
+ -- session guard; absent it, abort with the listing so the operator decides.
148
+ with grp as (
149
+ select k.id as key_id, k.agent_id, k.created_at,
150
+ agent_board._raw_key_fingerprint(agent_board._raw_key_canonical(k.public_key)) as fp
151
+ from agent_board.agent_signing_keys k
152
+ where k.public_key_format = 'raw_ed25519_base64url'
153
+ and agent_board._raw_key_canonical(k.public_key) is not null
154
+ ),
155
+ dups as (
156
+ select agent_id, fp, count(*) as n
157
+ from grp
158
+ group by agent_id, fp
159
+ having count(*) > 1
160
+ )
161
+ select count(*), string_agg(format(' agent %s fingerprint %s (%s rows)', agent_id, fp, n), E'\n')
162
+ into v_dup_groups, v_dup_listing
163
+ from dups;
164
+
165
+ if coalesce(v_dup_groups, 0) > 0 then
166
+ if not v_dedup_enabled then
167
+ raise exception E'FINGERPRINT BACKFILL ABORTED — same-agent duplicate key rows (alias pairs on one agent). Set SET agent_board.fingerprint_backfill_dedup = ''on''; to delete the redundant aliases (earliest row kept), or resolve by hand:\n%', v_dup_listing
168
+ using errcode = '23505';
169
+ end if;
170
+ -- Guarded destructive dedup: keep the earliest row per (agent, fingerprint), delete the
171
+ -- rest (byte-identical aliases of the SAME agent's own key).
172
+ with grp as (
173
+ select k.id as key_id, k.agent_id, k.created_at,
174
+ agent_board._raw_key_fingerprint(agent_board._raw_key_canonical(k.public_key)) as fp
175
+ from agent_board.agent_signing_keys k
176
+ where k.public_key_format = 'raw_ed25519_base64url'
177
+ and agent_board._raw_key_canonical(k.public_key) is not null
178
+ ),
179
+ ranked as (
180
+ select key_id,
181
+ row_number() over (partition by agent_id, fp order by created_at, key_id) as rn
182
+ from grp
183
+ )
184
+ delete from agent_board.agent_signing_keys k
185
+ using ranked r
186
+ where k.id = r.key_id and r.rn > 1;
187
+ get diagnostics v_deleted = row_count;
188
+ raise notice 'MINTHARDEN fingerprint backfill: deleted % same-agent alias duplicate row(s)', v_deleted;
189
+ end if;
190
+
191
+ -- (c) Data-derived re-derivation (corrective, always). Canonicalize public_key and set
192
+ -- fingerprint to the raw derivation for every raw row where either diverges. The
193
+ -- integrity trigger validates each corrected row (session_replication_role unchanged).
194
+ update agent_board.agent_signing_keys k
195
+ set public_key = agent_board._raw_key_canonical(k.public_key),
196
+ fingerprint = agent_board._raw_key_fingerprint(agent_board._raw_key_canonical(k.public_key)),
197
+ updated_at = now()
198
+ where k.public_key_format = 'raw_ed25519_base64url'
199
+ and agent_board._raw_key_canonical(k.public_key) is not null
200
+ and (
201
+ k.public_key is distinct from agent_board._raw_key_canonical(k.public_key)
202
+ or k.fingerprint is distinct from agent_board._raw_key_fingerprint(agent_board._raw_key_canonical(k.public_key))
203
+ );
204
+ get diagnostics v_updated = row_count;
205
+ raise notice 'MINTHARDEN fingerprint backfill: re-derived % raw signing-key row(s)', v_updated;
206
+ end;
207
+ $$;
@@ -0,0 +1,106 @@
1
+ -- #141 register_fleet_machine fleet-ownership hijack fix.
2
+ --
3
+ -- machine_fingerprint is GLOBALLY unique (idx_agent_board_fleet_machines_fingerprint_unique),
4
+ -- so the prior `on conflict (machine_fingerprint) do update set owner_agent_id = excluded...,
5
+ -- lifecycle_state = 'active', revoked_at = null` let ANY trust-domain owner re-register an
6
+ -- existing fingerprint and (a) steal owner_agent_id of another owner's machine and
7
+ -- (b) un-revoke a deliberately revoked machine. Once #133 CLAIMGATE makes fingerprint
8
+ -- ownership equal claim authority, that is an authority-transfer + lockout primitive.
9
+ --
10
+ -- This migration reproduces register_fleet_machine VERBATIM except the conflict handling:
11
+ -- * The `do update` is gated `where owner_agent_id = <resolved actor>`. A cross-owner
12
+ -- fingerprint conflict matches no row, updates nothing, `returning` yields no row, and
13
+ -- we then RAISE 42501. This is evaluated atomically against the conflicting row, so it
14
+ -- is also free of the select-then-insert TOCTOU that a pre-check guard would carry.
15
+ -- * owner_agent_id is NEVER in the update set — ownership is immutable via re-register.
16
+ -- * lifecycle_state / revoked_at are NEVER in the update set — a re-register does NOT
17
+ -- resurrect a deliberately revoked machine (revocation authority lives in the
18
+ -- append-only fleet_revocation_events; flipping lifecycle here would only desync it).
19
+ -- Same-owner re-register still updates descriptive fields (label / domain / source /
20
+ -- metadata) exactly as before.
21
+ -- The fn is granted to authenticated (#118 discipline); create-or-replace preserves the
22
+ -- ACL, but revoke/grant are restated for clarity and the comment records the guard.
23
+
24
+ create or replace function agent_board.register_fleet_machine(
25
+ p_owner_agent_id uuid,
26
+ p_trust_domain_id uuid,
27
+ p_machine_label text,
28
+ p_machine_fingerprint text,
29
+ p_enrollment_source text default 'portal',
30
+ p_metadata jsonb default '{}'::jsonb
31
+ )
32
+ returns table(machine_id uuid, trust_domain_id uuid, lifecycle_state text, enrolled_at timestamptz)
33
+ language plpgsql
34
+ security definer
35
+ set search_path = agent_board, public
36
+ as $$
37
+ declare
38
+ v_machine agent_board.fleet_machines%rowtype;
39
+ v_actor_agent_id uuid;
40
+ begin
41
+ if nullif(trim(p_machine_label), '') is null or nullif(trim(p_machine_fingerprint), '') is null then
42
+ raise exception 'machine label and fingerprint are required' using errcode = '22023';
43
+ end if;
44
+
45
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_owner_agent_id, 'p_owner_agent_id');
46
+
47
+ if not exists (
48
+ select 1
49
+ from agent_board.fleet_trust_domains ftd
50
+ where ftd.id = p_trust_domain_id
51
+ and ftd.owner_agent_id = v_actor_agent_id
52
+ and ftd.lifecycle_state in ('active', 'pending')
53
+ ) then
54
+ raise exception 'trust domain is not active for owner' using errcode = '42501';
55
+ end if;
56
+
57
+ insert into agent_board.fleet_machines(
58
+ trust_domain_id,
59
+ owner_agent_id,
60
+ machine_label,
61
+ machine_fingerprint,
62
+ lifecycle_state,
63
+ enrollment_source,
64
+ enrolled_at,
65
+ revoked_at,
66
+ metadata
67
+ )
68
+ values (
69
+ p_trust_domain_id,
70
+ v_actor_agent_id,
71
+ trim(p_machine_label),
72
+ trim(p_machine_fingerprint),
73
+ 'active',
74
+ coalesce(nullif(p_enrollment_source, ''), 'portal'),
75
+ now(),
76
+ null,
77
+ coalesce(p_metadata, '{}'::jsonb) || jsonb_build_object('redacted', true, 'metadata-safe', true)
78
+ )
79
+ on conflict (machine_fingerprint)
80
+ do update set
81
+ trust_domain_id = excluded.trust_domain_id,
82
+ machine_label = excluded.machine_label,
83
+ enrollment_source = excluded.enrollment_source,
84
+ enrolled_at = coalesce(agent_board.fleet_machines.enrolled_at, excluded.enrolled_at),
85
+ metadata = excluded.metadata,
86
+ -- owner_agent_id, lifecycle_state, revoked_at intentionally NOT updated (#141):
87
+ -- ownership is immutable via re-register and a revoked machine is not resurrected.
88
+ updated_at = now()
89
+ where agent_board.fleet_machines.owner_agent_id = v_actor_agent_id
90
+ returning * into v_machine;
91
+
92
+ -- A fingerprint conflict on a row owned by a DIFFERENT owner matches no update row
93
+ -- above, so v_machine is null here: refuse the ownership-hijack attempt (#141).
94
+ if v_machine.id is null then
95
+ raise exception 'machine fingerprint is registered to another owner' using errcode = '42501';
96
+ end if;
97
+
98
+ return query select v_machine.id, v_machine.trust_domain_id, v_machine.lifecycle_state, v_machine.enrolled_at;
99
+ end;
100
+ $$;
101
+
102
+ revoke all on function agent_board.register_fleet_machine(uuid, uuid, text, text, text, jsonb) from public, anon;
103
+ grant execute on function agent_board.register_fleet_machine(uuid, uuid, text, text, text, jsonb) to authenticated, service_role;
104
+
105
+ comment on function agent_board.register_fleet_machine(uuid, uuid, text, text, text, jsonb) is
106
+ 'Registers/updates a fleet machine for the resolved actor. Anti-hijack (#141): a fingerprint conflict owned by a different owner raises 42501 (ownership is immutable via re-register); re-register never resurrects a revoked machine (revocation authority lives in fleet_revocation_events).';