@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
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
-- Consiliency/message-board#246 (change 1 of 3): the federated-human return path
|
|
2
|
+
-- compares the caller's verified (issuer, subject) against the pair STORED on the
|
|
3
|
+
-- agent it is about to hand back.
|
|
4
|
+
--
|
|
5
|
+
-- WHAT WAS WRONG. The owner uid is derived as
|
|
6
|
+
-- sha256(p_issuer || '|' || p_subject), a concatenation with no length prefix and
|
|
7
|
+
-- no escaping, so the pair is not recoverable from the preimage and two distinct
|
|
8
|
+
-- principals can derive the SAME uid. Having derived it, step (7) selected ANY
|
|
9
|
+
-- agent on that uid and returned it after checking exactly two things: that it was
|
|
10
|
+
-- 'active' and that its kind was 'human'. NEITHER of those says the row belongs to
|
|
11
|
+
-- the caller. Under a collision the second principal is handed a live session as
|
|
12
|
+
-- the first, and step (11)'s audit insert then stamps the ATTACKER's (issuer,
|
|
13
|
+
-- subject) onto the VICTIM's agent_id -- poisoning the stored evidence that
|
|
14
|
+
-- enroll_federated_identity's #176/#244 resumption conjunction reads.
|
|
15
|
+
--
|
|
16
|
+
-- WHY THIS SHIPS BEFORE THE DERIVATION IS FIXED. The equality check defeats the
|
|
17
|
+
-- collision WITH TODAY'S AMBIGUOUS ENCODING STILL IN PLACE: whichever principal
|
|
18
|
+
-- reaches the uid second no longer matches the stamp and is refused. Making the
|
|
19
|
+
-- derivation injective is a separate change -- it moves every existing
|
|
20
|
+
-- fed-human-* handle, so it needs its own migration, an auth.users.email re-key
|
|
21
|
+
-- for the live owner, and Portal coordination. This change needs none of that and
|
|
22
|
+
-- closes the damage path on its own.
|
|
23
|
+
--
|
|
24
|
+
-- TWO RETURN SITES, NOT ONE. Step (7) is the one the issue quotes. Step (8)'s
|
|
25
|
+
-- `exception when unique_violation` handler is a SECOND return of an existing row
|
|
26
|
+
-- on the same uid, reached when a concurrent transaction commits the agent between
|
|
27
|
+
-- (7)'s select and this insert; it re-selected on (uid, active, human) and returned
|
|
28
|
+
-- that row with no stamp comparison either. Gating only (7) would reproduce the
|
|
29
|
+
-- defect one level down, so both sites carry the same gate, in the same shape.
|
|
30
|
+
--
|
|
31
|
+
-- The shapes being identical does NOT mean one assertion can pin both, and an
|
|
32
|
+
-- earlier draft of this comment said it could. A COUNT of the gate's occurrences is
|
|
33
|
+
-- satisfied by two gates at (7) and none at (8) -- the pgTAP file builds exactly
|
|
34
|
+
-- that body as a mutation, and the count stayed green. Each site is pinned
|
|
35
|
+
-- POSITIONALLY instead, by splitting the catalog body on this handler's own
|
|
36
|
+
-- `exception when unique_violation`, and the RAISE is pinned per site too: gating
|
|
37
|
+
-- both sites while raising a different message at one of them is a distinct
|
|
38
|
+
-- regression that the gate's position alone does not see.
|
|
39
|
+
--
|
|
40
|
+
-- FAIL CLOSED ON A MISSING STAMP, and the population was measured rather than
|
|
41
|
+
-- assumed. Both gates are written `if not coalesce(<equality>, false)`: a row whose
|
|
42
|
+
-- metadata->'federation' lacks issuer, or lacks subject, or lacks the object
|
|
43
|
+
-- entirely, yields NULL, coalesces to false, and is REFUSED. All three shapes have
|
|
44
|
+
-- their own fixture in the pgTAP file -- the missing-ISSUER-only case was added
|
|
45
|
+
-- because this sentence named it while no fixture covered it, and the surviving
|
|
46
|
+
-- subject conjunct would have refused a differing caller regardless. This mirrors enroll_federated_identity, whose ownership
|
|
47
|
+
-- conjunction is coalesced to false for the same reason -- unproven ownership must
|
|
48
|
+
-- read as not-owned. On the canonical project (mqccjfqngptkemnchlko, 2026-08-16)
|
|
49
|
+
-- there is exactly 1 agent reachable on this path, it carries BOTH issuer and
|
|
50
|
+
-- subject, and 0 rows reachable here lack a stamp; all 110 of its
|
|
51
|
+
-- enrollment_federation_audit rows agree with that stamp on both fields, over 1
|
|
52
|
+
-- distinct pair. The stamp has been written unconditionally by this path's own
|
|
53
|
+
-- creation branch since it shipped (20260728000100:243), so a stamp-less row is not
|
|
54
|
+
-- a shape this code produces.
|
|
55
|
+
--
|
|
56
|
+
-- THE GATE IS FIRST, DELIBERATELY. It precedes the status branch and the kind
|
|
57
|
+
-- check, so a caller presenting a colliding pair gets ONE refusal regardless of
|
|
58
|
+
-- whether the victim is active, suspended, human or not -- the ordering
|
|
59
|
+
-- enroll_federated_identity uses for the key-squat, and for the same reason: a
|
|
60
|
+
-- caller that does not own the identity must not learn anything else about it.
|
|
61
|
+
--
|
|
62
|
+
-- THE MESSAGE IS READ BY THE CALLER, not just by an operator.
|
|
63
|
+
-- supabase/functions/portal-session/index.ts:374-378 catches ANY error from this
|
|
64
|
+
-- RPC and returns `{error:"provision_rejected", detail:<message>}` with HTTP 403.
|
|
65
|
+
-- So the message names NO handle, agent id, status, kind, or the other principal's
|
|
66
|
+
-- issuer/subject -- only that the derived owner is already bound elsewhere, which
|
|
67
|
+
-- the refusal itself already discloses. Log distinguishability comes from the
|
|
68
|
+
-- phrase, which is unique in this schema, not from the SQLSTATE: 42501 is the
|
|
69
|
+
-- honest class and is what every other AUTHORIZATION refusal in this function
|
|
70
|
+
-- raises (the body also raises 22023 four times for malformed input and 40001 once
|
|
71
|
+
-- for a retryable race -- neither is this refusal's class), while
|
|
72
|
+
-- 23505 would be actively wrong (the client maps it to "conflicting active claim",
|
|
73
|
+
-- packages/agent-board-client/src/client.ts:349).
|
|
74
|
+
--
|
|
75
|
+
-- NOT RESCOPED, AND THAT IS THE POINT. An obvious-looking alternative is to filter
|
|
76
|
+
-- (7)'s select by the stamp so the owner always finds its own row. That is a HOLE:
|
|
77
|
+
-- the mismatching caller would then fall through to step (8), whose insert
|
|
78
|
+
-- unique-violates on the shared handle, whose handler re-selects on (uid, active,
|
|
79
|
+
-- human) -- and hands back the victim's agent anyway. Refusing is the correct
|
|
80
|
+
-- verdict, because under an ambiguous derivation the second principal has no owner
|
|
81
|
+
-- of its own to be given.
|
|
82
|
+
|
|
83
|
+
create or replace function agent_board._enroll_federated_human_after_revocation_gate(
|
|
84
|
+
p_issuer text,
|
|
85
|
+
p_audience text,
|
|
86
|
+
p_subject text,
|
|
87
|
+
p_human_identity_claim jsonb,
|
|
88
|
+
p_display_name text default null
|
|
89
|
+
)
|
|
90
|
+
returns table(
|
|
91
|
+
agent_id uuid,
|
|
92
|
+
auth_user_id uuid,
|
|
93
|
+
handle text,
|
|
94
|
+
kind text,
|
|
95
|
+
status text,
|
|
96
|
+
provider_id text,
|
|
97
|
+
created_at timestamptz
|
|
98
|
+
)
|
|
99
|
+
language plpgsql
|
|
100
|
+
security definer
|
|
101
|
+
set search_path = agent_board, public
|
|
102
|
+
as $$
|
|
103
|
+
declare
|
|
104
|
+
v_provider agent_board.identity_providers%rowtype;
|
|
105
|
+
v_handle text;
|
|
106
|
+
v_uid uuid;
|
|
107
|
+
v_agent agent_board.agents%rowtype;
|
|
108
|
+
v_existing agent_board.agents%rowtype;
|
|
109
|
+
v_email text := nullif(trim(coalesce(p_human_identity_claim->>'email', '')), '');
|
|
110
|
+
v_email_verified boolean;
|
|
111
|
+
begin
|
|
112
|
+
-- Derive the verified-email boolean from the claim (never trust it as board-proven
|
|
113
|
+
-- possession; it only records what the Portal asserted). A non-boolean/absent value
|
|
114
|
+
-- collapses to false.
|
|
115
|
+
begin
|
|
116
|
+
v_email_verified := coalesce((p_human_identity_claim->>'email_verified')::boolean, false);
|
|
117
|
+
exception when others then
|
|
118
|
+
v_email_verified := false;
|
|
119
|
+
end;
|
|
120
|
+
|
|
121
|
+
-- (1) service_role-only (defense in depth on top of the execute-grant revocation).
|
|
122
|
+
if not agent_board._is_service_role() then
|
|
123
|
+
raise exception 'service_role required for enroll_federated_human' using errcode = '42501';
|
|
124
|
+
end if;
|
|
125
|
+
|
|
126
|
+
-- (2) A verified subject + human identity claim are always present from the EF.
|
|
127
|
+
if p_subject is null then
|
|
128
|
+
raise exception 'verified subject is required' using errcode = '22023';
|
|
129
|
+
end if;
|
|
130
|
+
if p_human_identity_claim is null then
|
|
131
|
+
raise exception 'human identity claim is required' using errcode = '22023';
|
|
132
|
+
end if;
|
|
133
|
+
|
|
134
|
+
-- (3) F-11: the synthetic-uid preimage is issuer||'|'||subject. Guard the separator so a
|
|
135
|
+
-- subject cannot inject a '|' to collide two distinct (issuer, subject) pairs onto one
|
|
136
|
+
-- owner uid. ALSO reject an email-shaped subject ('@'): the subject is stored VERBATIM in
|
|
137
|
+
-- agents.metadata.federation.subject, and the agents SELECT RLS is not row-scoped, so an
|
|
138
|
+
-- email `sub` would be a fleet-harvestable PII leak — the exact hazard the email is kept
|
|
139
|
+
-- out of agents.metadata to avoid. The operator's ENROLL_SUBJECT_CLAIM must resolve to an
|
|
140
|
+
-- opaque id; this is the fail-closed backstop, and the pgTAP PII guard asserts BOTH the
|
|
141
|
+
-- '@'-rejection and metadata carrying no '@'.
|
|
142
|
+
if position('|' in p_subject) > 0 then
|
|
143
|
+
raise exception 'verified subject must not contain a pipe separator character'
|
|
144
|
+
using errcode = '22023';
|
|
145
|
+
end if;
|
|
146
|
+
if position('@' in p_subject) > 0 then
|
|
147
|
+
raise exception 'verified subject must be an opaque id, not an email/@-bearing value'
|
|
148
|
+
using errcode = '22023';
|
|
149
|
+
end if;
|
|
150
|
+
|
|
151
|
+
-- (4) provider gate on (issuer, audience): must exist + supported + policy_approved.
|
|
152
|
+
-- Defense in depth atop the EF's signature verify (mirrors 20260719001200:154-181).
|
|
153
|
+
select *
|
|
154
|
+
into v_provider
|
|
155
|
+
from agent_board.identity_providers
|
|
156
|
+
where issuer = p_issuer
|
|
157
|
+
and audience = p_audience
|
|
158
|
+
limit 1;
|
|
159
|
+
if v_provider.id is null then
|
|
160
|
+
raise exception 'issuer/audience not in the identity-provider allowlist' using errcode = '42501';
|
|
161
|
+
end if;
|
|
162
|
+
if not v_provider.supported then
|
|
163
|
+
raise exception 'identity provider is not supported' using errcode = '42501';
|
|
164
|
+
end if;
|
|
165
|
+
if not v_provider.policy_approved then
|
|
166
|
+
raise exception 'identity provider is not policy-approved' using errcode = '42501';
|
|
167
|
+
end if;
|
|
168
|
+
|
|
169
|
+
-- (5) HUMAN-ONLY ceiling. allowed_agent_kinds is permissive on empty/null (the ceiling
|
|
170
|
+
-- is "unset"); reject ONLY when it is non-empty AND does not include 'human'.
|
|
171
|
+
if array_length(v_provider.allowed_agent_kinds, 1) is not null
|
|
172
|
+
and not ('human' = any (v_provider.allowed_agent_kinds)) then
|
|
173
|
+
raise exception 'provider ceiling does not permit the human kind' using errcode = '42501';
|
|
174
|
+
end if;
|
|
175
|
+
|
|
176
|
+
-- (6) SYNTHETIC uid — byte-identical derivation to enroll_federated_identity:198-199
|
|
177
|
+
-- (extensions.digest, since the pinned search_path lacks extensions). Idempotent per
|
|
178
|
+
-- (issuer, sub) so re-provisioning converges to ONE owner auth.users row = the shared
|
|
179
|
+
-- IDLINK uid. Pass issuer/subject VERBATIM (no trim/normalize) to stay byte-identical.
|
|
180
|
+
v_handle := 'fed-human-' || substr(
|
|
181
|
+
encode(extensions.digest(convert_to(p_issuer || '|' || p_subject, 'UTF8'), 'sha256'), 'hex'),
|
|
182
|
+
1, 32);
|
|
183
|
+
v_uid := agent_board._ensure_agent_auth_user(v_handle);
|
|
184
|
+
|
|
185
|
+
-- (7) Idempotent + no-self-reactivation, keyed on auth_user_id = the synthetic uid (ANY
|
|
186
|
+
-- status). An ACTIVE row -> return it (idempotent). A NON-active row -> fail closed
|
|
187
|
+
-- (42501): a suspended/revoked human must NOT re-mint a session (mirrors
|
|
188
|
+
-- 20260726000100:78-81 and reinforces the F-13 revocation lever). F-5: assert the row is
|
|
189
|
+
-- human before returning it as a human session (a non-human agent on this owner uid must
|
|
190
|
+
-- never yield a "human" provision).
|
|
191
|
+
select *
|
|
192
|
+
into v_existing
|
|
193
|
+
from agent_board.agents a
|
|
194
|
+
where a.auth_user_id = v_uid
|
|
195
|
+
order by (a.status = 'active') desc, a.created_at asc
|
|
196
|
+
limit 1;
|
|
197
|
+
if found then
|
|
198
|
+
-- (7a) #246 STORED-IDENTITY EQUALITY. The select above keys on the DERIVED uid
|
|
199
|
+
-- and nothing else, and the derivation is not injective, so `found` does not
|
|
200
|
+
-- mean "the caller's own agent". Require the pair this row was stamped with at
|
|
201
|
+
-- creation to equal the pair the caller presented. coalesce(..., false): a row
|
|
202
|
+
-- with no stored stamp yields NULL, which must read as NOT-mine, not as
|
|
203
|
+
-- unchecked. First, before the status and kind branches, so a mismatching
|
|
204
|
+
-- caller learns only that it was refused.
|
|
205
|
+
if not coalesce(
|
|
206
|
+
v_existing.metadata #>> '{federation,issuer}' = p_issuer
|
|
207
|
+
and v_existing.metadata #>> '{federation,subject}' = p_subject, false) then
|
|
208
|
+
raise exception 'federated identity mismatch: the owner derived for this issuer/subject is already bound to a different verified identity; refusing to issue a session'
|
|
209
|
+
using errcode = '42501';
|
|
210
|
+
end if;
|
|
211
|
+
if v_existing.status = 'active' then
|
|
212
|
+
if v_existing.kind <> 'human' then
|
|
213
|
+
raise exception 'existing agent on this identity is not human; refusing human session'
|
|
214
|
+
using errcode = '42501';
|
|
215
|
+
end if;
|
|
216
|
+
v_agent := v_existing;
|
|
217
|
+
-- Refresh the verified-email snapshot on re-provision (MERGE, see step 9).
|
|
218
|
+
if v_email is not null then
|
|
219
|
+
update auth.users u
|
|
220
|
+
set raw_user_meta_data = coalesce(u.raw_user_meta_data, '{}'::jsonb)
|
|
221
|
+
|| jsonb_build_object(
|
|
222
|
+
'fedsession_email', v_email,
|
|
223
|
+
'fedsession_email_verified', v_email_verified,
|
|
224
|
+
'fedsession_email_provenance', 'portal-claim-unverified')
|
|
225
|
+
where u.id = v_uid;
|
|
226
|
+
end if;
|
|
227
|
+
-- (auto-join before the idempotent return — see step 11 for the rationale).
|
|
228
|
+
perform agent_board._fedsession_autojoin_default_board(v_provider, v_agent.id);
|
|
229
|
+
insert into agent_board.enrollment_federation_audit(
|
|
230
|
+
issuer, audience, subject, human_identity_claim, provider_id,
|
|
231
|
+
agent_id, auth_user_id, requested_scopes, granted_scopes, mode)
|
|
232
|
+
values (
|
|
233
|
+
p_issuer, p_audience, p_subject, p_human_identity_claim, v_provider.id,
|
|
234
|
+
v_agent.id, v_uid, '{}', '{}', 'fedsession');
|
|
235
|
+
return query
|
|
236
|
+
select v_agent.id, v_agent.auth_user_id, v_agent.handle,
|
|
237
|
+
v_agent.kind::text, v_agent.status, v_provider.id, v_agent.created_at;
|
|
238
|
+
return;
|
|
239
|
+
else
|
|
240
|
+
raise exception 'a non-active identity already exists for this human; contact an operator'
|
|
241
|
+
using errcode = '42501';
|
|
242
|
+
end if;
|
|
243
|
+
end if;
|
|
244
|
+
|
|
245
|
+
-- (8) Create the kind='human' agent (self-owned: auth_user_id = owner_user_id = uid).
|
|
246
|
+
-- No agent_signing_keys row (humans hold no key). metadata.federation carries ONLY the
|
|
247
|
+
-- boolean email_verified — never the address (PII / fleet-readable RLS).
|
|
248
|
+
begin
|
|
249
|
+
insert into agent_board.agents(
|
|
250
|
+
agent_type,
|
|
251
|
+
kind,
|
|
252
|
+
handle,
|
|
253
|
+
display_name,
|
|
254
|
+
status,
|
|
255
|
+
auth_user_id,
|
|
256
|
+
owner_user_id,
|
|
257
|
+
metadata
|
|
258
|
+
)
|
|
259
|
+
values (
|
|
260
|
+
'human',
|
|
261
|
+
'human'::agent_board.agent_kind,
|
|
262
|
+
v_handle,
|
|
263
|
+
coalesce(nullif(trim(p_display_name), ''), 'Human'),
|
|
264
|
+
'active',
|
|
265
|
+
v_uid,
|
|
266
|
+
v_uid,
|
|
267
|
+
jsonb_build_object(
|
|
268
|
+
'federation', jsonb_build_object(
|
|
269
|
+
'mode', 'fedsession',
|
|
270
|
+
'issuer', p_issuer,
|
|
271
|
+
'audience', p_audience,
|
|
272
|
+
'subject', p_subject,
|
|
273
|
+
'providerId', v_provider.id,
|
|
274
|
+
'email_verified', v_email_verified,
|
|
275
|
+
'enrolledAt', now()
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
returning * into v_agent;
|
|
280
|
+
exception when unique_violation then
|
|
281
|
+
-- Concurrent first-provision race on the synthetic uid. F-5: re-select the ACTIVE row
|
|
282
|
+
-- and ASSERT it is human (fail closed otherwise) — never return a non-human agent as a
|
|
283
|
+
-- human session.
|
|
284
|
+
select *
|
|
285
|
+
into v_existing
|
|
286
|
+
from agent_board.agents a
|
|
287
|
+
where a.auth_user_id = v_uid
|
|
288
|
+
and a.status = 'active'
|
|
289
|
+
and a.kind = 'human'
|
|
290
|
+
limit 1;
|
|
291
|
+
if found then
|
|
292
|
+
-- (8a) #246 STORED-IDENTITY EQUALITY, second return site. Same gate, same
|
|
293
|
+
-- shape, same message as (7a): this handler returns an EXISTING row on the
|
|
294
|
+
-- derived uid, so it needs the same proof that the row is the caller's. The
|
|
295
|
+
-- interleaving that reaches it is concurrency-only -- (7)'s unfiltered select
|
|
296
|
+
-- intercepts any already-committed row on this uid -- which is why this site
|
|
297
|
+
-- is pinned structurally by the class assertion rather than differentially by
|
|
298
|
+
-- a single-session test.
|
|
299
|
+
if not coalesce(
|
|
300
|
+
v_existing.metadata #>> '{federation,issuer}' = p_issuer
|
|
301
|
+
and v_existing.metadata #>> '{federation,subject}' = p_subject, false) then
|
|
302
|
+
raise exception 'federated identity mismatch: the owner derived for this issuer/subject is already bound to a different verified identity; refusing to issue a session'
|
|
303
|
+
using errcode = '42501';
|
|
304
|
+
end if;
|
|
305
|
+
v_agent := v_existing;
|
|
306
|
+
else
|
|
307
|
+
raise exception 'could not provision federated human (race or non-human on owner uid), please retry'
|
|
308
|
+
using errcode = '40001';
|
|
309
|
+
end if;
|
|
310
|
+
end;
|
|
311
|
+
|
|
312
|
+
-- (9) F-12/F-4: store the verified email ONLY in auth.users.raw_user_meta_data
|
|
313
|
+
-- (service-role-only), MERGED so _ensure_agent_auth_user's {slug, issuer} is preserved,
|
|
314
|
+
-- tagged provenance='portal-claim-unverified'. The definer owner (postgres) owns auth.*.
|
|
315
|
+
if v_email is not null then
|
|
316
|
+
update auth.users u
|
|
317
|
+
set raw_user_meta_data = coalesce(u.raw_user_meta_data, '{}'::jsonb)
|
|
318
|
+
|| jsonb_build_object(
|
|
319
|
+
'fedsession_email', v_email,
|
|
320
|
+
'fedsession_email_verified', v_email_verified,
|
|
321
|
+
'fedsession_email_provenance', 'portal-claim-unverified')
|
|
322
|
+
where u.id = v_uid;
|
|
323
|
+
end if;
|
|
324
|
+
|
|
325
|
+
-- (10) FEDSESSION auto-join (new). Baseline default-board membership for a fresh fed-human.
|
|
326
|
+
perform agent_board._fedsession_autojoin_default_board(v_provider, v_agent.id);
|
|
327
|
+
|
|
328
|
+
-- (11) F-6: append the audit row (mode='fedsession'). No requested/granted scopes on the
|
|
329
|
+
-- human path (authority is membership/role, not enrollment scopes) -> empty arrays.
|
|
330
|
+
insert into agent_board.enrollment_federation_audit(
|
|
331
|
+
issuer, audience, subject, human_identity_claim, provider_id,
|
|
332
|
+
agent_id, auth_user_id, requested_scopes, granted_scopes, mode)
|
|
333
|
+
values (
|
|
334
|
+
p_issuer, p_audience, p_subject, p_human_identity_claim, v_provider.id,
|
|
335
|
+
v_agent.id, v_uid, '{}', '{}', 'fedsession');
|
|
336
|
+
|
|
337
|
+
return query
|
|
338
|
+
select v_agent.id, v_agent.auth_user_id, v_agent.handle,
|
|
339
|
+
v_agent.kind::text, v_agent.status, v_provider.id, v_agent.created_at;
|
|
340
|
+
end;
|
|
341
|
+
$$;
|
|
342
|
+
|
|
343
|
+
-- `create or replace` with the UNCHANGED signature preserves both the ACL and the
|
|
344
|
+
-- COMMENT (a `drop function` would discard both, and recreating a SECURITY DEFINER
|
|
345
|
+
-- function grants EXECUTE to PUBLIC by default). The revoke below is therefore
|
|
346
|
+
-- redundant TODAY and is restated anyway, so the migration remains correct if it is
|
|
347
|
+
-- ever replayed against a catalog where this function does not already exist. The
|
|
348
|
+
-- comment is not redundant: it is amended, because the gate this migration adds is
|
|
349
|
+
-- part of the contract a reader of `\df+` should see.
|
|
350
|
+
revoke all on function agent_board._enroll_federated_human_after_revocation_gate(text, text, text, jsonb, text)
|
|
351
|
+
from public, anon, authenticated, service_role;
|
|
352
|
+
comment on function agent_board._enroll_federated_human_after_revocation_gate(text, text, text, jsonb, text) is
|
|
353
|
+
'Internal federated-human provisioner. Call only through enroll_federated_human, which holds the deterministic auth.users owner lock and rejects durable issuer/subject revocation markers. Returns an EXISTING agent on the derived owner uid only when that agent''s stored metadata.federation issuer AND subject equal the caller''s verified pair (message-board#246); a missing stamp is a refusal, not a pass, and both return sites -- the idempotent branch and the unique_violation race handler -- carry the gate.';
|