@consiliency/agent-board-schema 1.27.0 → 1.35.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 +32 -0
- package/migrations/20260909000100_agent_board_ledger_revoked_authority.sql +1036 -0
- package/migrations/20260910000100_agent_board_ceiling_lock_comment_truth.sql +89 -0
- package/migrations/20260911000100_agent_board_ledger_revoked_board_creation.sql +330 -0
- package/migrations/20260912000100_agent_board_receipt_subject_gate.sql +348 -0
- package/migrations/20260913000100_agent_board_task_thread_lock_order.sql +292 -0
- package/migrations/20260914000100_agent_board_claim_subject_gate.sql +500 -0
- package/migrations/20260915000100_agent_board_fedsession_stored_identity_gate.sql +353 -0
- package/migrations/20260916000100_agent_board_federated_identity_injective_slug.sql +1646 -0
- package/package.json +1 -1
- package/schema-manifest.json +75 -3
- package/src/tooling.test.mjs +47 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +161 -30
- package/tests/agent_board_claim_subject_gate.test.sql +1025 -0
- package/tests/agent_board_enroll_resumption.test.sql +7 -1
- package/tests/agent_board_federated_identity_injective_slug.test.sql +388 -0
- package/tests/agent_board_fedsession.test.sql +17 -2
- package/tests/agent_board_fedsession_stored_identity_gate.test.sql +606 -0
- package/tests/agent_board_fk_lock_edges.test.sql +422 -0
- package/tests/agent_board_ledger_authority.test.sql +1081 -0
- package/tests/agent_board_receipt_subject_gate.test.sql +624 -0
- package/tests/agent_board_signer_lifecycle.test.sql +29 -6
- package/tests/agent_board_source_strip_helper.test.sql +157 -1
- package/tests/agent_board_task_thread_lock_order.test.sql +337 -0
- package/tests/catalog/p_metadata_parameters.json +20 -0
- package/tests/fixtures/test-prereqs.sql +420 -11
- package/tests/isolation/expected/ledger_authority_revoke.out +701 -0
- package/tests/isolation/expected/signer_lifecycle_revoke.out +1 -1
- package/tests/isolation/expected/task_thread_lock_order.out +203 -0
- package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +5 -4
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +4 -4
- package/tests/isolation/specs/ledger_authority_revoke.spec +405 -0
- package/tests/isolation/specs/signer_lifecycle_revoke.spec +10 -0
- package/tests/isolation/specs/task_thread_lock_order.spec +197 -0
- package/tests/seeds/20260916000100_agent_board_federated_identity_injective_slug.sql.pre.sql +196 -0
|
@@ -39,6 +39,16 @@
|
|
|
39
39
|
# its MODE observable. The table's UPDATE privilege is the
|
|
40
40
|
# owner's alone (service_role has SELECT), so this models
|
|
41
41
|
# owner-level incident DML and any future key-only writer.
|
|
42
|
+
#
|
|
43
|
+
# GOLDEN CHANGE IN #276, recorded so it is not read as a #269 regression. Permutation 2's
|
|
44
|
+
# refusal line moved from `signer_not_active` to `fleet_revoked`. `revoke_agent_subtree`'s
|
|
45
|
+
# primitive produces BOTH conditions at once — it flips agents.status AND writes an
|
|
46
|
+
# agent-subject fleet_revocation_events row — and #276's gate, which lives inside
|
|
47
|
+
# grant_board_role, now runs before _assert_signed_authority_signer_live and wins the race to
|
|
48
|
+
# raise. The blocking behaviour this permutation exists for is unchanged (`<waiting ...>` /
|
|
49
|
+
# `<... completed>` are byte-identical), and `signer_not_active` is still exercised
|
|
50
|
+
# EXCLUSIVELY by permutations 3 and 4, whose `suspend` session touches only agents.status and
|
|
51
|
+
# writes no ledger row. Nothing here stopped being pinned.
|
|
42
52
|
|
|
43
53
|
setup
|
|
44
54
|
{
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Consiliency/message-board#281 (R1) — append_task_event must take THREADS BEFORE TASKS,
|
|
2
|
+
# and must still serialise its event_sequence computation.
|
|
3
|
+
#
|
|
4
|
+
# THE DEFECT. #290's FK-aware detector found 40 unserialised ABBA rows on
|
|
5
|
+
# `tasks <-> threads`, the largest block of the 186 and the only one whose operand
|
|
6
|
+
# coincidence is an ordinary workload:
|
|
7
|
+
# append_task_event: tasks (explicit FOR UPDATE) -> threads (KEY SHARE, task_events.thread_id FK)
|
|
8
|
+
# post_message: threads (UPDATE, in _next_thread_message_sequence) -> tasks (KEY SHARE, messages.task_id FK)
|
|
9
|
+
# The colliding operand is `postMessage({ threadId: task.threadId, taskId: task.id })`
|
|
10
|
+
# concurrent with any agent claiming/delegating/resolving that task.
|
|
11
|
+
#
|
|
12
|
+
# READ THIS BEFORE READING THE GOLDEN: THE REAL PAIR DOES NOT DEADLOCK, AND PERMUTATIONS 2
|
|
13
|
+
# AND 3 ARE THE EVIDENCE FOR THAT, NOT A FAILED REPRODUCTION.
|
|
14
|
+
# `_next_thread_message_sequence` updates threads.last_message_sequence / updated_at.
|
|
15
|
+
# `threads` has exactly one unique index (threads_pkey on id), so neither column is a key
|
|
16
|
+
# column and that UPDATE takes FOR NO KEY UPDATE — which does NOT conflict with the FOR
|
|
17
|
+
# KEY SHARE append_task_event takes on threads through task_events_thread_id_fkey. (The
|
|
18
|
+
# matrix is pinned verbatim as FKL-2 in agent_board_fk_lock_edges.test.sql:
|
|
19
|
+
# `no key update/key share=false`.) So post_message blocks on append_task_event's
|
|
20
|
+
# `tasks FOR UPDATE`, and append_task_event never blocks on post_message's threads lock:
|
|
21
|
+
# ONE-WAY BLOCKING, NO CYCLE. The detector reports 40 rows because `_acq` aggregates any
|
|
22
|
+
# write-update to mode `update` — it over-reports by design.
|
|
23
|
+
# Permutations 2 and 3 therefore show block-then-complete in both directions, and they
|
|
24
|
+
# showed the same thing BEFORE the fix. No pre-fix reproduction of the real pair is
|
|
25
|
+
# claimed here, because none exists.
|
|
26
|
+
#
|
|
27
|
+
# THE "probe" SESSION IS AN ORDER INSTRUMENT, NOT A post_message STAND-IN. State it that
|
|
28
|
+
# way or a reader will draw the wrong conclusion from permutation 1.
|
|
29
|
+
# * It takes `threads FOR UPDATE` then `tasks FOR UPDATE`. No DUAL-RELATION shipped path
|
|
30
|
+
# takes threads FOR UPDATE — and the scope of that claim is load-bearing, so read it
|
|
31
|
+
# against its evidence rather than past it. agent_board_task_thread_lock_order.test.sql
|
|
32
|
+
# TT-1 enumerates the functions that touch BOTH relations, and across that class the
|
|
33
|
+
# only threads modes are `key share` and `update`(write-update, i.e. FOR NO KEY UPDATE).
|
|
34
|
+
# TT-1's class is dual-relation BY CONSTRUCTION, so a threads-only caller is outside it
|
|
35
|
+
# — out of scope, not a counterexample.
|
|
36
|
+
# * AND THERE IS SUCH A CALLER. `create_or_find_thread` takes a real
|
|
37
|
+
# `select … from agent_board.threads … for update`
|
|
38
|
+
# (20260517000000_agent_board_addressing.sql:265-273), and it is the ONLY function in
|
|
39
|
+
# the bundle holding an EXPLICIT lock on threads — catalog-derived: exactly one row with
|
|
40
|
+
# `rel_name = agent_board.threads and via = 'textual'`, everything else that conflicts
|
|
41
|
+
# with KEY SHARE on threads does so through `write-update`. It cannot close a cycle here
|
|
42
|
+
# because it takes NO lock on `tasks` at all (its only other edges are FK KEY SHARE on
|
|
43
|
+
# `agents` and `boards`), so it is one-way blocking against append_task_event in either
|
|
44
|
+
# direction. An earlier draft of this header claimed "NO shipped caller takes threads
|
|
45
|
+
# FOR UPDATE" and cited TT-1 for it: a universal claim resting on a class-scoped
|
|
46
|
+
# assertion that never covered it. The citation made it look verified. It was false.
|
|
47
|
+
# * It is deliberately STRONGER than the real pair's modes. That is its purpose: those
|
|
48
|
+
# modes do not conflict, so post_message vs append_task_event cannot make the ORDER
|
|
49
|
+
# observable. FOR UPDATE conflicts with FOR KEY SHARE, which turns the order into
|
|
50
|
+
# something isolationtester can see and a golden can pin.
|
|
51
|
+
# * BUT DO NOT READ "INSTRUMENT" AS "HYPOTHETICAL" — it is closer to a shipped caller than
|
|
52
|
+
# that word suggests, and this is the reason not to prune this permutation as synthetic.
|
|
53
|
+
# `create_or_find_thread` ALREADY performs the probe's first step for real, on its reuse
|
|
54
|
+
# path. The probe is that function PLUS a `tasks` lock, and nothing else. Measured with a
|
|
55
|
+
# three-session run (real create_or_find_thread, real append_task_event, and a
|
|
56
|
+
# `tasks … for key share` observer): PRE-FIX the observer WAITS, because append_task_event
|
|
57
|
+
# blocked on the thread lock WHILE HOLDING `tasks FOR UPDATE` and convoyed every other
|
|
58
|
+
# writer of that task; POST-FIX the observer completes immediately, because the appender
|
|
59
|
+
# now waits holding nothing. So the ordering this permutation pins already pays off
|
|
60
|
+
# today — the synthetic part is only the second step.
|
|
61
|
+
# * What it proves is exactly this: with append_task_event tasks-first, a threads-first
|
|
62
|
+
# adversary deadlocks; with append_task_event threads-first, the same adversary merely
|
|
63
|
+
# serialises. That is the property the fix claims, and it is the property that becomes
|
|
64
|
+
# load-bearing the day any real function strengthens its threads lock.
|
|
65
|
+
# * VERIFIED PRE-FIX, and quoted at the width it was actually observed. Against the
|
|
66
|
+
# 20260816000100 body, permutation 1 emits `step probe_tasks: <waiting ...>` and then
|
|
67
|
+
# `step append_run: <... completed>` / `ERROR: deadlock detected` -- isolationtester
|
|
68
|
+
# reports the primary line only, not the `while locking tuple in relation ...` DETAIL,
|
|
69
|
+
# so no DETAIL is quoted here. Against the 20260913000100 body the same permutation
|
|
70
|
+
# produces the serialisation in this golden. Permutations 2, 3 and 4 are BYTE-IDENTICAL
|
|
71
|
+
# under both bodies, which is the reachability finding above stated as an observation.
|
|
72
|
+
#
|
|
73
|
+
# PERMUTATION 4 IS THE GUARD THE REORDER COULD HAVE BROKEN. The `tasks FOR UPDATE` is not
|
|
74
|
+
# decoration: it is the mutual exclusion behind `select coalesce(max(event_sequence),0)+1`
|
|
75
|
+
# against task_events_task_id_event_sequence_key. Two concurrent appends on one task must
|
|
76
|
+
# serialise and produce 1 then 2. Mutation-verified: delete the FOR UPDATE and this
|
|
77
|
+
# permutation stops blocking and ends in a duplicate-key error instead.
|
|
78
|
+
|
|
79
|
+
setup
|
|
80
|
+
{
|
|
81
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
82
|
+
values ('7a810000-0000-4000-8000-000000000001','agent','agent','r1iso-actor','R1 ISO Actor','active');
|
|
83
|
+
|
|
84
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
85
|
+
values ('7a820000-0000-4000-8000-000000000001','coordination',
|
|
86
|
+
'7a810000-0000-4000-8000-000000000001','R1 ISO thread');
|
|
87
|
+
|
|
88
|
+
insert into agent_board.tasks(id, thread_id, created_by_agent_id, title, status)
|
|
89
|
+
values ('7a830000-0000-4000-8000-000000000001','7a820000-0000-4000-8000-000000000001',
|
|
90
|
+
'7a810000-0000-4000-8000-000000000001','R1 ISO task','pending');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
teardown
|
|
94
|
+
{
|
|
95
|
+
delete from agent_board.messages where thread_id = '7a820000-0000-4000-8000-000000000001';
|
|
96
|
+
delete from agent_board.task_events where task_id = '7a830000-0000-4000-8000-000000000001';
|
|
97
|
+
delete from agent_board.tasks where id = '7a830000-0000-4000-8000-000000000001';
|
|
98
|
+
delete from agent_board.threads where id = '7a820000-0000-4000-8000-000000000001';
|
|
99
|
+
delete from agent_board.agents where id = '7a810000-0000-4000-8000-000000000001';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# The real append, unmodified. Only deterministic columns reach the golden — the event id is
|
|
103
|
+
# a fresh uuid and the timestamps are wall-clock.
|
|
104
|
+
session "append"
|
|
105
|
+
step "append_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
106
|
+
step "append_run" {
|
|
107
|
+
select e.event_sequence,
|
|
108
|
+
(e.thread_id = '7a820000-0000-4000-8000-000000000001') as thread_is_task_thread
|
|
109
|
+
from agent_board.append_task_event(
|
|
110
|
+
'7a830000-0000-4000-8000-000000000001',
|
|
111
|
+
'7a810000-0000-4000-8000-000000000001',
|
|
112
|
+
'commented') e;
|
|
113
|
+
}
|
|
114
|
+
step "append_commit" { COMMIT; }
|
|
115
|
+
|
|
116
|
+
# A second real append, for the event_sequence guard in permutation 4.
|
|
117
|
+
session "append2"
|
|
118
|
+
step "append2_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
119
|
+
step "append2_run" {
|
|
120
|
+
select e.event_sequence
|
|
121
|
+
from agent_board.append_task_event(
|
|
122
|
+
'7a830000-0000-4000-8000-000000000001',
|
|
123
|
+
'7a810000-0000-4000-8000-000000000001',
|
|
124
|
+
'status_changed') e;
|
|
125
|
+
}
|
|
126
|
+
step "append2_commit" { COMMIT; }
|
|
127
|
+
|
|
128
|
+
# The real post_message, task-attached into the task's own thread — the exact operand
|
|
129
|
+
# coincidence #281 R1 is about.
|
|
130
|
+
session "post"
|
|
131
|
+
step "post_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
132
|
+
step "post_run" {
|
|
133
|
+
select m.message_sequence,
|
|
134
|
+
(m.task_id = '7a830000-0000-4000-8000-000000000001') as task_attached
|
|
135
|
+
from agent_board.post_message(
|
|
136
|
+
'7a820000-0000-4000-8000-000000000001',
|
|
137
|
+
'7a810000-0000-4000-8000-000000000001',
|
|
138
|
+
'r1 iso body',
|
|
139
|
+
'comment',
|
|
140
|
+
'7a830000-0000-4000-8000-000000000001') m;
|
|
141
|
+
}
|
|
142
|
+
step "post_commit" { COMMIT; }
|
|
143
|
+
|
|
144
|
+
# THE ORDER INSTRUMENT. See the header: threads FOR UPDATE is stronger than any shipped
|
|
145
|
+
# caller's threads lock, on purpose, because the shipped modes do not conflict.
|
|
146
|
+
session "probe"
|
|
147
|
+
step "probe_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
148
|
+
step "probe_threads" {
|
|
149
|
+
select count(*)::int as locked_threads from (
|
|
150
|
+
select 1 from agent_board.threads t
|
|
151
|
+
where t.id = '7a820000-0000-4000-8000-000000000001' for update) s;
|
|
152
|
+
}
|
|
153
|
+
step "probe_tasks" {
|
|
154
|
+
select count(*)::int as locked_tasks from (
|
|
155
|
+
select 1 from agent_board.tasks tk
|
|
156
|
+
where tk.id = '7a830000-0000-4000-8000-000000000001' for update) s;
|
|
157
|
+
}
|
|
158
|
+
step "probe_commit" { COMMIT; }
|
|
159
|
+
|
|
160
|
+
session "check"
|
|
161
|
+
step "check" {
|
|
162
|
+
select (select array_agg(te.event_sequence order by te.event_sequence)
|
|
163
|
+
from agent_board.task_events te
|
|
164
|
+
where te.task_id = '7a830000-0000-4000-8000-000000000001') as event_sequences,
|
|
165
|
+
(select count(*)::int from agent_board.messages
|
|
166
|
+
where thread_id = '7a820000-0000-4000-8000-000000000001') as messages,
|
|
167
|
+
(select count(*)::int from agent_board.task_events te
|
|
168
|
+
where te.task_id = '7a830000-0000-4000-8000-000000000001'
|
|
169
|
+
and te.thread_id <> '7a820000-0000-4000-8000-000000000001') as events_on_wrong_thread;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# 1. THE ORDER, made observable. probe holds threads(H) FOR UPDATE; append then runs.
|
|
173
|
+
# TASKS-FIRST (pre-fix), append takes tasks(T) FOR UPDATE and blocks wanting threads(H)
|
|
174
|
+
# KEY SHARE; probe_tasks then wants that same task row and the server aborts one with
|
|
175
|
+
# `deadlock detected`. THREADS-FIRST (fixed), append blocks at its FIRST lock statement
|
|
176
|
+
# holding nothing, probe completes, and the append drains afterwards. The difference
|
|
177
|
+
# between those two worlds is the whole golden.
|
|
178
|
+
permutation "probe_begin" "probe_threads" "append_begin" "append_run" "probe_tasks" "probe_commit" "append_commit" "check"
|
|
179
|
+
|
|
180
|
+
# 2. THE REAL PAIR, append first. append holds tasks FOR UPDATE; post_message needs
|
|
181
|
+
# tasks KEY SHARE for the messages.task_id FK and blocks. It is released by the commit,
|
|
182
|
+
# never by a deadlock. This is one-way blocking and it is the same before and after the
|
|
183
|
+
# fix — see the header.
|
|
184
|
+
permutation "append_begin" "append_run" "post_begin" "post_run" "append_commit" "post_commit" "check"
|
|
185
|
+
|
|
186
|
+
# 3. THE REAL PAIR, post first. post holds threads(H) FOR NO KEY UPDATE and tasks(T) KEY
|
|
187
|
+
# SHARE. append's threads KEY SHARE is GRANTED — the two modes do not conflict, which is
|
|
188
|
+
# the reachability finding stated as an observation rather than as prose — and it then
|
|
189
|
+
# blocks on tasks FOR UPDATE until post commits. Again no cycle in either direction.
|
|
190
|
+
permutation "post_begin" "post_run" "append_begin" "append_run" "post_commit" "append_commit" "check"
|
|
191
|
+
|
|
192
|
+
# 4. THE event_sequence GUARD, positively. Two concurrent appends on ONE task: the second
|
|
193
|
+
# blocks on the first's `tasks FOR UPDATE` and only then computes its sequence, so the
|
|
194
|
+
# two events are 1 and 2 and the unique constraint is never tested. Remove that FOR
|
|
195
|
+
# UPDATE and this permutation stops blocking and ends in a duplicate-key error — which is
|
|
196
|
+
# the mutation that pins this assertion.
|
|
197
|
+
permutation "append_begin" "append_run" "append2_begin" "append2_run" "append_commit" "append2_commit" "check"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
-- Pre-migration seed for 20260916000100 (message-board#246, change 2 of 3).
|
|
2
|
+
--
|
|
3
|
+
-- WHY A SEED AND NOT A FIXTURE INSIDE THE TEST. The thing under test is the SHIPPED
|
|
4
|
+
-- backfill, which runs once, inside the migration. A fixture created inside the pgTAP
|
|
5
|
+
-- transaction would be created AFTER the migration had already run and would prove
|
|
6
|
+
-- nothing about it. The message-board#103 pre-migration seed hook exists for exactly
|
|
7
|
+
-- this: these rows are inserted immediately BEFORE 20260916000100 applies, in the
|
|
8
|
+
-- migration-N-1 schema, so the backfill meets real pre-existing rows and
|
|
9
|
+
-- agent_board_federated_identity_injective_slug.test.sql asserts what it actually
|
|
10
|
+
-- wrote.
|
|
11
|
+
--
|
|
12
|
+
-- These rows model the production shape measured on mqccjfqngptkemnchlko (2026-08-16):
|
|
13
|
+
-- one federated owner uid, its own human agent row whose handle IS the slug, and
|
|
14
|
+
-- several agents that reference it only by owner_user_id. The real pair is a live
|
|
15
|
+
-- person's identity and is deliberately not reproduced here; these are synthetic pairs
|
|
16
|
+
-- whose slugs were computed with the same two expressions the migration uses.
|
|
17
|
+
--
|
|
18
|
+
-- Three named identities, one per recovery arm the backfill has to get right:
|
|
19
|
+
--
|
|
20
|
+
-- F1 d2460001-… HUMAN ARM pair P1, recoverable via agents.auth_user_id.
|
|
21
|
+
-- Carries a SECOND agent stamped with the SAME pair, so
|
|
22
|
+
-- a backfill counting rows instead of DISTINCT pairs
|
|
23
|
+
-- refuses it -- which is what production's six-agent
|
|
24
|
+
-- owner would do.
|
|
25
|
+
-- F2 d2460002-… OWNER-ONLY ARM pair P2, NO agent on that auth_user_id at all. The
|
|
26
|
+
-- only route to the pair is owner_user_id. Drop that
|
|
27
|
+
-- arm and this row becomes unrecoverable.
|
|
28
|
+
-- F3 d2460003-… DECOY ARM pair P3, plus an owned agent stamped with an unrelated
|
|
29
|
+
-- pair that does NOT reproduce F3's slug. Drop the
|
|
30
|
+
-- "reproduces the current slug" filter and F3 suddenly
|
|
31
|
+
-- has two candidate pairs and is refused.
|
|
32
|
+
-- F4 d2460004-… DIVERGENT ARM pair P4, but its raw_user_meta_data.slug and its
|
|
33
|
+
-- identity_data sub/slug hold values that are NOT its old
|
|
34
|
+
-- slug. The backfill rewrites those two only when they
|
|
35
|
+
-- EQUAL the old slug (a `case` arm each), and F1-F3 all
|
|
36
|
+
-- have them equal -- so before F4 existed, making both arms
|
|
37
|
+
-- UNCONDITIONAL survived every assertion. F4 is the
|
|
38
|
+
-- complement of BF-2d (which pins the same narrowness for
|
|
39
|
+
-- agents.handle) and it is the assertion that was missing.
|
|
40
|
+
--
|
|
41
|
+
-- Slugs, computed from the pairs below (old = the ambiguous `issuer || '|' || subject`,
|
|
42
|
+
-- new = the netstring form the migration introduces):
|
|
43
|
+
-- P1 https://fixture-246.example/idp-a fixture-246-subject-a
|
|
44
|
+
-- old fed-human-d50f81b6913e066dba4c19f5cb6b202a
|
|
45
|
+
-- new fed-human-6c056fe12e1fb58fb2c072ff8bd9a613
|
|
46
|
+
-- P2 https://fixture-246.example/idp-b fixture-246-subject-b
|
|
47
|
+
-- old fed-human-7ab9c8b527e1cce671305defc65fe7c0
|
|
48
|
+
-- new fed-human-ec861cc1407f8f585e94cb9e8ece6039
|
|
49
|
+
-- P3 https://fixture-246.example/idp-c fixture-246-subject-c
|
|
50
|
+
-- old fed-human-def7953eb096d786392090ce33952d9d
|
|
51
|
+
-- new fed-human-d4211005ca6d96ac0f8a90d2065dceda
|
|
52
|
+
-- P3-decoy https://fixture-246.example/idp-decoy fixture-246-subject-decoy
|
|
53
|
+
-- old fed-human-0897be463845203cbc1c6099e8befcb0 (reproduces nothing here)
|
|
54
|
+
-- P4 https://fixture-246.example/idp-d fixture-246-subject-d
|
|
55
|
+
-- old fed-human-38b45a2207cfb4847a4c62c9973cd676
|
|
56
|
+
-- new fed-human-25bf058f71a25339995a9a2012ced65b
|
|
57
|
+
--
|
|
58
|
+
-- The literals are spelled out rather than computed so that the seed and the test agree
|
|
59
|
+
-- on a NAMED value: a seed that derived the slug with the same expression as the code
|
|
60
|
+
-- under test would move silently whenever that expression moved.
|
|
61
|
+
|
|
62
|
+
-- The owner auth.users rows, in the pre-migration (old-slug) form _ensure_agent_auth_user
|
|
63
|
+
-- would have written them.
|
|
64
|
+
insert into auth.users (
|
|
65
|
+
id, instance_id, email, email_confirmed_at, encrypted_password,
|
|
66
|
+
created_at, updated_at, raw_app_meta_data, raw_user_meta_data, aud, role
|
|
67
|
+
)
|
|
68
|
+
values
|
|
69
|
+
('d2460001-0000-4000-8000-000000000001', '00000000-0000-0000-0000-000000000000',
|
|
70
|
+
'agent+fed-human-d50f81b6913e066dba4c19f5cb6b202a@agent-board.local', now(), '',
|
|
71
|
+
now(), now(),
|
|
72
|
+
jsonb_build_object('provider', 'agent_board', 'providers', jsonb_build_array('agent_board')),
|
|
73
|
+
jsonb_build_object('slug', 'fed-human-d50f81b6913e066dba4c19f5cb6b202a',
|
|
74
|
+
'issuer', 'agent_board.provision_agent'),
|
|
75
|
+
'authenticated', 'authenticated'),
|
|
76
|
+
('d2460002-0000-4000-8000-000000000002', '00000000-0000-0000-0000-000000000000',
|
|
77
|
+
'agent+fed-human-7ab9c8b527e1cce671305defc65fe7c0@agent-board.local', now(), '',
|
|
78
|
+
now(), now(),
|
|
79
|
+
jsonb_build_object('provider', 'agent_board', 'providers', jsonb_build_array('agent_board')),
|
|
80
|
+
jsonb_build_object('slug', 'fed-human-7ab9c8b527e1cce671305defc65fe7c0',
|
|
81
|
+
'issuer', 'agent_board.provision_agent'),
|
|
82
|
+
'authenticated', 'authenticated'),
|
|
83
|
+
('d2460003-0000-4000-8000-000000000003', '00000000-0000-0000-0000-000000000000',
|
|
84
|
+
'agent+fed-human-def7953eb096d786392090ce33952d9d@agent-board.local', now(), '',
|
|
85
|
+
now(), now(),
|
|
86
|
+
jsonb_build_object('provider', 'agent_board', 'providers', jsonb_build_array('agent_board')),
|
|
87
|
+
jsonb_build_object('slug', 'fed-human-def7953eb096d786392090ce33952d9d',
|
|
88
|
+
'issuer', 'agent_board.provision_agent'),
|
|
89
|
+
'authenticated', 'authenticated'),
|
|
90
|
+
-- F2's owned agent needs an auth_user_id of its own, so that F2's owner uid is
|
|
91
|
+
-- reachable ONLY through owner_user_id. This row is not a fed-human identity.
|
|
92
|
+
('d2460002-0000-4000-8000-0000000000a2', '00000000-0000-0000-0000-000000000000',
|
|
93
|
+
'agent+mb246-fixture-a2@agent-board.local', now(), '',
|
|
94
|
+
now(), now(),
|
|
95
|
+
jsonb_build_object('provider', 'agent_board', 'providers', jsonb_build_array('agent_board')),
|
|
96
|
+
jsonb_build_object('slug', 'mb246-fixture-a2', 'issuer', 'agent_board.provision_agent'),
|
|
97
|
+
'authenticated', 'authenticated'),
|
|
98
|
+
-- F4: the email carries the old slug, but the meta slug deliberately does NOT. An
|
|
99
|
+
-- unconditional rewrite of raw_user_meta_data.slug would clobber this value; the
|
|
100
|
+
-- shipped `case` arm leaves it alone.
|
|
101
|
+
('d2460004-0000-4000-8000-000000000004', '00000000-0000-0000-0000-000000000000',
|
|
102
|
+
'agent+fed-human-38b45a2207cfb4847a4c62c9973cd676@agent-board.local', now(), '',
|
|
103
|
+
now(), now(),
|
|
104
|
+
jsonb_build_object('provider', 'agent_board', 'providers', jsonb_build_array('agent_board')),
|
|
105
|
+
jsonb_build_object('slug', 'mb246-f4-legacy-meta-slug', 'issuer', 'agent_board.provision_agent'),
|
|
106
|
+
'authenticated', 'authenticated')
|
|
107
|
+
-- RESET, not insert-if-absent. The replay database is reused between runs and the
|
|
108
|
+
-- previous run's backfill already moved these rows to the new slug; an insert that
|
|
109
|
+
-- skipped on conflict would leave the second run asserting a move that had already
|
|
110
|
+
-- happened. Restoring the OLD form here makes every run exercise the backfill.
|
|
111
|
+
on conflict (id) do update
|
|
112
|
+
set email = excluded.email,
|
|
113
|
+
raw_user_meta_data = excluded.raw_user_meta_data;
|
|
114
|
+
|
|
115
|
+
insert into auth.identities (
|
|
116
|
+
id, user_id, provider_id, provider, identity_data, last_sign_in_at, created_at, updated_at
|
|
117
|
+
)
|
|
118
|
+
values
|
|
119
|
+
('d2460011-0000-4000-8000-000000000001', 'd2460001-0000-4000-8000-000000000001',
|
|
120
|
+
'fed-human-d50f81b6913e066dba4c19f5cb6b202a', 'agent_board',
|
|
121
|
+
jsonb_build_object('sub', 'fed-human-d50f81b6913e066dba4c19f5cb6b202a',
|
|
122
|
+
'slug', 'fed-human-d50f81b6913e066dba4c19f5cb6b202a'),
|
|
123
|
+
now(), now(), now()),
|
|
124
|
+
('d2460012-0000-4000-8000-000000000002', 'd2460002-0000-4000-8000-000000000002',
|
|
125
|
+
'fed-human-7ab9c8b527e1cce671305defc65fe7c0', 'agent_board',
|
|
126
|
+
jsonb_build_object('sub', 'fed-human-7ab9c8b527e1cce671305defc65fe7c0',
|
|
127
|
+
'slug', 'fed-human-7ab9c8b527e1cce671305defc65fe7c0'),
|
|
128
|
+
now(), now(), now()),
|
|
129
|
+
('d2460013-0000-4000-8000-000000000003', 'd2460003-0000-4000-8000-000000000003',
|
|
130
|
+
'fed-human-def7953eb096d786392090ce33952d9d', 'agent_board',
|
|
131
|
+
jsonb_build_object('sub', 'fed-human-def7953eb096d786392090ce33952d9d',
|
|
132
|
+
'slug', 'fed-human-def7953eb096d786392090ce33952d9d'),
|
|
133
|
+
now(), now(), now()),
|
|
134
|
+
-- F4: provider_id DOES carry the old slug (so the update's WHERE matches and the row is
|
|
135
|
+
-- genuinely visited), while identity_data.sub/slug deliberately do not. The two `case`
|
|
136
|
+
-- arms must leave those alone; provider_id, which is rewritten unconditionally, moves.
|
|
137
|
+
('d2460014-0000-4000-8000-000000000004', 'd2460004-0000-4000-8000-000000000004',
|
|
138
|
+
'fed-human-38b45a2207cfb4847a4c62c9973cd676', 'agent_board',
|
|
139
|
+
jsonb_build_object('sub', 'mb246-f4-legacy-sub', 'slug', 'mb246-f4-legacy-slug'),
|
|
140
|
+
now(), now(), now())
|
|
141
|
+
on conflict (id) do update
|
|
142
|
+
set provider_id = excluded.provider_id,
|
|
143
|
+
identity_data = excluded.identity_data;
|
|
144
|
+
|
|
145
|
+
-- F1: the human agent whose HANDLE is the slug (the surface a relabel would move), plus
|
|
146
|
+
-- a second agent carrying the SAME stamp so DISTINCT is load-bearing.
|
|
147
|
+
insert into agent_board.agents (
|
|
148
|
+
id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id, metadata
|
|
149
|
+
)
|
|
150
|
+
values
|
|
151
|
+
('d2460101-0000-4000-8000-000000000001', 'human', 'human',
|
|
152
|
+
'fed-human-d50f81b6913e066dba4c19f5cb6b202a', 'MB246 fixture F1 human', 'active',
|
|
153
|
+
'd2460001-0000-4000-8000-000000000001', 'd2460001-0000-4000-8000-000000000001',
|
|
154
|
+
jsonb_build_object('federation', jsonb_build_object(
|
|
155
|
+
'issuer', 'https://fixture-246.example/idp-a',
|
|
156
|
+
'subject', 'fixture-246-subject-a'))),
|
|
157
|
+
('d2460102-0000-4000-8000-000000000001', 'agent', 'agent',
|
|
158
|
+
'mb246-fixture-f1-owned', 'MB246 fixture F1 owned agent', 'active',
|
|
159
|
+
null, 'd2460001-0000-4000-8000-000000000001',
|
|
160
|
+
jsonb_build_object('federation', jsonb_build_object(
|
|
161
|
+
'issuer', 'https://fixture-246.example/idp-a',
|
|
162
|
+
'subject', 'fixture-246-subject-a'))),
|
|
163
|
+
|
|
164
|
+
-- F2: owner reachable ONLY via owner_user_id.
|
|
165
|
+
('d2460201-0000-4000-8000-000000000002', 'agent', 'agent',
|
|
166
|
+
'mb246-fixture-a2', 'MB246 fixture F2 owned agent', 'active',
|
|
167
|
+
'd2460002-0000-4000-8000-0000000000a2', 'd2460002-0000-4000-8000-000000000002',
|
|
168
|
+
jsonb_build_object('federation', jsonb_build_object(
|
|
169
|
+
'issuer', 'https://fixture-246.example/idp-b',
|
|
170
|
+
'subject', 'fixture-246-subject-b'))),
|
|
171
|
+
|
|
172
|
+
-- F3: the true pair, plus a decoy stamp that does not reproduce F3's slug.
|
|
173
|
+
('d2460301-0000-4000-8000-000000000003', 'human', 'human',
|
|
174
|
+
'fed-human-def7953eb096d786392090ce33952d9d', 'MB246 fixture F3 human', 'active',
|
|
175
|
+
'd2460003-0000-4000-8000-000000000003', 'd2460003-0000-4000-8000-000000000003',
|
|
176
|
+
jsonb_build_object('federation', jsonb_build_object(
|
|
177
|
+
'issuer', 'https://fixture-246.example/idp-c',
|
|
178
|
+
'subject', 'fixture-246-subject-c'))),
|
|
179
|
+
('d2460302-0000-4000-8000-000000000003', 'agent', 'agent',
|
|
180
|
+
'mb246-fixture-f3-decoy', 'MB246 fixture F3 decoy-stamped agent', 'active',
|
|
181
|
+
null, 'd2460003-0000-4000-8000-000000000003',
|
|
182
|
+
jsonb_build_object('federation', jsonb_build_object(
|
|
183
|
+
'issuer', 'https://fixture-246.example/idp-decoy',
|
|
184
|
+
'subject', 'fixture-246-subject-decoy'))),
|
|
185
|
+
|
|
186
|
+
-- F4: an ordinary human row whose handle IS the slug, so the row is recovered and
|
|
187
|
+
-- re-keyed normally. Its value is entirely in the divergent meta/identity_data above.
|
|
188
|
+
('d2460401-0000-4000-8000-000000000004', 'human', 'human',
|
|
189
|
+
'fed-human-38b45a2207cfb4847a4c62c9973cd676', 'MB246 fixture F4 human', 'active',
|
|
190
|
+
'd2460004-0000-4000-8000-000000000004', 'd2460004-0000-4000-8000-000000000004',
|
|
191
|
+
jsonb_build_object('federation', jsonb_build_object(
|
|
192
|
+
'issuer', 'https://fixture-246.example/idp-d',
|
|
193
|
+
'subject', 'fixture-246-subject-d')))
|
|
194
|
+
on conflict (id) do update
|
|
195
|
+
set handle = excluded.handle,
|
|
196
|
+
metadata = excluded.metadata;
|