@consiliency/agent-board-schema 1.10.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fixtures/already-applied.json +136 -0
- package/migrations/20260729000100_agent_board_fedsession_autojoin_default_board.sql +364 -0
- package/migrations/20260730000100_agent_board_gatenull_service_role_bypass.sql +369 -0
- package/migrations/20260730000200_agent_board_membstate_authz.sql +195 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/package.json +1 -1
- package/schema-manifest.json +309 -3
- package/src/tooling.test.mjs +123 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_fedsession.test.sql +6 -4
- package/tests/agent_board_fedsession_autojoin.test.sql +181 -0
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_gatenull_membstate.test.sql +352 -0
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +29 -12
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +25 -9
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
|
@@ -316,6 +316,142 @@
|
|
|
316
316
|
{
|
|
317
317
|
"id": "20260728000100_agent_board_enroll_federated_human",
|
|
318
318
|
"sha256": "5f300449c29ec05c2cd3c2cd7eca622ca41e8b32b8856c5bc0695b5750a196e7"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"id": "20260729000100_agent_board_fedsession_autojoin_default_board",
|
|
322
|
+
"sha256": "6f3a2db6c7349119b3893fe530788236ef5ccf4e6b0f3e21235497b8b7b232c0"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"id": "20260730000100_agent_board_gatenull_service_role_bypass",
|
|
326
|
+
"sha256": "db8b2a93996c1d61bd34fc58a5e4b3056563c53bb2eb20cbb4a3b0b799a0d91e"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
"id": "20260730000200_agent_board_membstate_authz",
|
|
330
|
+
"sha256": "a4d935614f970a946f27a5925c0166e1d41510a82078e559fb769f4ea39afc2e"
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"id": "20260731000100_agent_board_agentsdir_column_scope",
|
|
334
|
+
"sha256": "e05ddbbcf778828dbc4ba2631a5a288567d361371a60373c7e9090b718aa7433"
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"id": "20260801000100_agent_board_leaveidem_terminal_noop",
|
|
338
|
+
"sha256": "72b20a3a365d0c84969cd1129cbc04578ee487a70e28a4245171c2abc68ec779"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"id": "20260802000100_agent_board_rolereset_reentry_clamp",
|
|
342
|
+
"sha256": "d04f596cdc97dc2ddc83348ee49c48888b334d12c31394586e7058409fa48cce"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"id": "20260803000100_agent_board_rowid_total_order",
|
|
346
|
+
"sha256": "a3c58ff1f55d3134c6096d10bf56df09db573938e7b4b92c13b7389451dd4c3c"
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"id": "20260804000100_agent_board_hygiene_actor_binding_rls",
|
|
350
|
+
"sha256": "3feac0b5002bbff56c89adc3b9f071e21e1bb5ab2c4eea1571cfb61f254caa88"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"id": "20260806000100_agent_board_consent_substrate",
|
|
354
|
+
"sha256": "91ba2ec5885dfcaaa138ab3da3665a6779b6149147045773dfcbb707cfd9db07"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"id": "20260807000100_agent_board_consent_emission",
|
|
358
|
+
"sha256": "b6fb6f8254169d4fc3ab8016ab2f9ec2a2800c800178aebb602ff1db4aa0e7ee"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"id": "20260808000100_agent_board_rejectsem",
|
|
362
|
+
"sha256": "945a719e127573816cb9d18e318cf0778a99f3b0f8239b8e6ae0f8e62b04d919"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"id": "20260809000100_agent_board_consent_gating",
|
|
366
|
+
"sha256": "666e413ae1474e5e00de4d2bf2e7f7733f6ad149c91c3ef9a938e049115e387c"
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"id": "20260810000100_agent_board_consent_locking",
|
|
370
|
+
"sha256": "d027468d3adb8563563f4fac4b13140013b68f28f39abe37c1de487c7fd36978"
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"id": "20260811000100_agent_board_retention_metadata_revoke",
|
|
374
|
+
"sha256": "8d08dc986d532862d6593fe49b2e38e7447e57006360cae3406127c820ec19d6"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"id": "20260812000100_agent_board_pubexec_class_revoke",
|
|
378
|
+
"sha256": "34dabae96c11ab7fce68d71f1aa782eccef1bce5de4ad0567013d73949815606"
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"id": "20260813000100_agent_board_invitepolicy_block",
|
|
382
|
+
"sha256": "8ee82c196483bf7d80091a46f6189328934fda2345947aea905505e37090719d"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"id": "20260814000100_agent_board_obsgate_thread_post_policy",
|
|
386
|
+
"sha256": "b36433d8aa78413b3d1e5119c45f8a24d37de053d03f4c4fd47b449814c4becb"
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"id": "20260815000100_agent_board_obsgate2_authoring_gates",
|
|
390
|
+
"sha256": "7e76aa13aef8450aba50eb08d9da66b75a55457f41e3c92662137a1f6d7e2850"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"id": "20260816000100_agent_board_obsgate3_task_lifecycle",
|
|
394
|
+
"sha256": "921cc40511e4070c61f7b164a7528362a9662da2b902252a3e92091f01ebf0fd"
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"id": "20260817000100_agent_board_obsgate4_claims_lifecycle",
|
|
398
|
+
"sha256": "6dd2d7928df2dff87c61b52e7edb75c5ec781fec5a85dd7c4273dadfd162f8ef"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"id": "20260818000100_agent_board_fleet_machines_rls_correlation",
|
|
402
|
+
"sha256": "f8bca2d75755589a1fa72d0c30ca799bd20db776d73a3a5e27832e112b57ea5d"
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"id": "20260819000100_agent_board_resolve_noop_retire",
|
|
406
|
+
"sha256": "68bcb6d1b7e8ee8424e6d1d13a5a7d2c7d4554f348cc2cbf946ec95999517ac5"
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"id": "20260820000100_agent_board_roomdeleg_lane_b",
|
|
410
|
+
"sha256": "2b10b8507b9bc704e9419b93d6d5d0af6d24f0ceef28c3eaa520516158f6c3e2"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"id": "20260821000100_agent_board_claimgate_claimkey",
|
|
414
|
+
"sha256": "caf8a83d4f39fc0a3d76d5b97077f0c3cf281305e47c50d370e940497a211022"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"id": "20260822000100_agent_board_mint_harden",
|
|
418
|
+
"sha256": "67585b17791598391515e63fa680c35cedaf27edc7ba3296f98242b925b9f490"
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"id": "20260822000200_agent_board_mint_harden_fingerprint_backfill",
|
|
422
|
+
"sha256": "8bfd8e1bde5dbe85f21f5e0eb6c8f34462d16b92a763977ad9930539ac259698"
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"id": "20260823000100_agent_board_fleet_machine_hijack",
|
|
426
|
+
"sha256": "73dcb65af8f8c754e2c6cc1c29f8649619a7653519a36a6b3acddf1a344a2bee"
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
"id": "20260824000100_agent_board_claimgate_shadow",
|
|
430
|
+
"sha256": "da42126b6cf40b0b044513313b0921394cd25de13426b16bcebd09bbcf5310a1"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
"id": "20260825000100_agent_board_claimgate_enforce",
|
|
434
|
+
"sha256": "7264c8ff93f366238788f1a68c72a7ca0651da6e1b0cd9e27516e9acc01e6d3f"
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"id": "20260826000100_agent_board_fleet_machine_reactivation",
|
|
438
|
+
"sha256": "fe437413046fba73b5306f1e21de81116369989a5d6e934a57c83c91bd5abb4b"
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"id": "20260828000100_agent_board_delegation_cap_ceiling",
|
|
442
|
+
"sha256": "b9556374b6e512cc4f2ffc6a6d80d2abceca9ae1963e0f0ecdcd0485f6580e9e"
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"id": "20260829000100_agent_board_fleet_machine_reclaim",
|
|
446
|
+
"sha256": "6ba17d49d5ef9ced3824f4f7ef7dbb40dd9ac3564576dad8b479545b2bab3b19"
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
"id": "20260830000100_agent_board_delegation_sole_writer",
|
|
450
|
+
"sha256": "26f5febc3c63f665eeb6eca2909d72187f16bae2c2643065e8ddb7c919086a86"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"id": "20260831000100_agent_board_fleet_domain_reclaim",
|
|
454
|
+
"sha256": "8d24ec4d682969a5d9339aec0573fad65ee95993dce2f8dca214906549b5ebe8"
|
|
319
455
|
}
|
|
320
456
|
]
|
|
321
457
|
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
-- FEDSESSION auto-join: give a freshly provisioned federated human BASELINE board standing.
|
|
2
|
+
--
|
|
3
|
+
-- WHY: a Portal human who signs in for the first time gets a kind='human' fed-agent via
|
|
4
|
+
-- enroll_federated_human, but with NO board_memberships — so the Portal room-ops UI renders
|
|
5
|
+
-- over an empty dashboard and every owner-loop write 403s until they create/join a board.
|
|
6
|
+
-- The self-service path (create_board self-seeds the caller as owner) covers OWNED boards;
|
|
7
|
+
-- this migration adds the complementary default: an OPERATOR-DESIGNATED, per-issuer default
|
|
8
|
+
-- board that fresh fed-humans auto-join at provision time so they land with participation
|
|
9
|
+
-- standing.
|
|
10
|
+
--
|
|
11
|
+
-- MODEL (chosen over per-human manual grants): the default board is designated PER ISSUER on
|
|
12
|
+
-- the identity_providers row (new nullable default_board_id). NULL => no auto-join (the exact
|
|
13
|
+
-- current behavior; this migration is inert until an operator designates a board). The
|
|
14
|
+
-- auto-join is:
|
|
15
|
+
-- * BASELINE role only ('member' — never owner/admin): merely landing in the default board
|
|
16
|
+
-- confers no board authority beyond participation. Owner authority stays with create_board
|
|
17
|
+
-- / grant_board_role.
|
|
18
|
+
-- * FIRST-TIME-ONLY & idempotent: it inserts only when the fed-human has NO membership row
|
|
19
|
+
-- for that board AT ALL (any state). A human who deliberately LEFT the default board is
|
|
20
|
+
-- NOT silently re-joined, and re-provisioning never duplicates a membership. The NOT EXISTS
|
|
21
|
+
-- guard is intentionally BROADER than the live partial unique index
|
|
22
|
+
-- idx_agent_board_board_memberships_active_unique ((board_id, agent_id) WHERE
|
|
23
|
+
-- membership_state in ('joined','subscribed'), 20260505000000): the guard also covers
|
|
24
|
+
-- 'left'/'removed' rows (which the partial index does NOT), which is what gives the
|
|
25
|
+
-- first-time-only / left-respect semantics. That partial index is the CONCURRENCY backstop:
|
|
26
|
+
-- two concurrent re-provisions of the same already-provisioned human can both pass NOT
|
|
27
|
+
-- EXISTS, and the second insert then trips the index — the helper catches that
|
|
28
|
+
-- unique_violation to a no-op (mirroring enroll_federated_human's step-8 agent-insert race
|
|
29
|
+
-- handling) so auto-join stays non-fatal (never duplicates AND never aborts the mint).
|
|
30
|
+
-- * NEVER fatal: it runs only when the designated board EXISTS and is status='active'. A
|
|
31
|
+
-- missing/inactive/mis-designated board silently skips the join so provisioning (and the
|
|
32
|
+
-- Option A session mint) still succeeds. Auto-join is a convenience, never a gate.
|
|
33
|
+
--
|
|
34
|
+
-- SCOPE: additive + forward-only. Does NOT change the enroll_federated_human signature, the
|
|
35
|
+
-- IF-0-FEDSESSION-1 session response, the synthetic-uid/IDLINK derivation, the provider/kind
|
|
36
|
+
-- gates, the PII handling, or the audit. Only NEW: the default_board_id column + the auto-join
|
|
37
|
+
-- block. The /portal-session Edge Function is unchanged (it calls the same RPC). Writes no
|
|
38
|
+
-- agent_board.schema_version (stays 8).
|
|
39
|
+
|
|
40
|
+
-- (A) Per-issuer default board designation. NULL => no auto-join (inert). ON DELETE SET NULL so
|
|
41
|
+
-- retiring the board gracefully disables auto-join instead of blocking the delete.
|
|
42
|
+
alter table agent_board.identity_providers
|
|
43
|
+
add column if not exists default_board_id uuid
|
|
44
|
+
references agent_board.boards(id) on delete set null;
|
|
45
|
+
|
|
46
|
+
comment on column agent_board.identity_providers.default_board_id is
|
|
47
|
+
'FEDSESSION auto-join target: the board a freshly provisioned federated human is auto-joined '
|
|
48
|
+
'to (baseline member role) at enroll_federated_human time. NULL disables auto-join for this '
|
|
49
|
+
'issuer (default). Operator/service_role-only (authenticated has no INSERT/UPDATE on '
|
|
50
|
+
'identity_providers); the board must exist and be active for the join to fire. OPERATOR '
|
|
51
|
+
'CAVEAT: member = RLS-gated read AND post, and designation grants that standing to EVERY '
|
|
52
|
+
'future federated human of this issuer WITHOUT the board owner''s consent (a compromised/'
|
|
53
|
+
'high-volume approved issuer thus inflates membership, bounded by /portal-session''s '
|
|
54
|
+
'per-source rate cap). Designate ONLY a board intended as a shared landing/commons for this '
|
|
55
|
+
'issuer and whose owner you control.';
|
|
56
|
+
|
|
57
|
+
-- (B) Recreate enroll_federated_human with the auto-join block. Body is byte-identical to
|
|
58
|
+
-- 20260728000100 EXCEPT: step (7) sets v_agent := v_existing to unify the return, and a new
|
|
59
|
+
-- step (auto-join) runs before BOTH return paths.
|
|
60
|
+
create or replace function agent_board.enroll_federated_human(
|
|
61
|
+
p_issuer text,
|
|
62
|
+
p_audience text,
|
|
63
|
+
p_subject text,
|
|
64
|
+
p_human_identity_claim jsonb,
|
|
65
|
+
p_display_name text default null
|
|
66
|
+
)
|
|
67
|
+
returns table(
|
|
68
|
+
agent_id uuid,
|
|
69
|
+
auth_user_id uuid,
|
|
70
|
+
handle text,
|
|
71
|
+
kind text,
|
|
72
|
+
status text,
|
|
73
|
+
provider_id text,
|
|
74
|
+
created_at timestamptz
|
|
75
|
+
)
|
|
76
|
+
language plpgsql
|
|
77
|
+
security definer
|
|
78
|
+
set search_path = agent_board, public
|
|
79
|
+
as $$
|
|
80
|
+
declare
|
|
81
|
+
v_provider agent_board.identity_providers%rowtype;
|
|
82
|
+
v_handle text;
|
|
83
|
+
v_uid uuid;
|
|
84
|
+
v_agent agent_board.agents%rowtype;
|
|
85
|
+
v_existing agent_board.agents%rowtype;
|
|
86
|
+
v_email text := nullif(trim(coalesce(p_human_identity_claim->>'email', '')), '');
|
|
87
|
+
v_email_verified boolean;
|
|
88
|
+
begin
|
|
89
|
+
-- Derive the verified-email boolean from the claim (never trust it as board-proven
|
|
90
|
+
-- possession; it only records what the Portal asserted). A non-boolean/absent value
|
|
91
|
+
-- collapses to false.
|
|
92
|
+
begin
|
|
93
|
+
v_email_verified := coalesce((p_human_identity_claim->>'email_verified')::boolean, false);
|
|
94
|
+
exception when others then
|
|
95
|
+
v_email_verified := false;
|
|
96
|
+
end;
|
|
97
|
+
|
|
98
|
+
-- (1) service_role-only (defense in depth on top of the execute-grant revocation).
|
|
99
|
+
if not agent_board._is_service_role() then
|
|
100
|
+
raise exception 'service_role required for enroll_federated_human' using errcode = '42501';
|
|
101
|
+
end if;
|
|
102
|
+
|
|
103
|
+
-- (2) A verified subject + human identity claim are always present from the EF.
|
|
104
|
+
if p_subject is null then
|
|
105
|
+
raise exception 'verified subject is required' using errcode = '22023';
|
|
106
|
+
end if;
|
|
107
|
+
if p_human_identity_claim is null then
|
|
108
|
+
raise exception 'human identity claim is required' using errcode = '22023';
|
|
109
|
+
end if;
|
|
110
|
+
|
|
111
|
+
-- (3) F-11: the synthetic-uid preimage is issuer||'|'||subject. Guard the separator so a
|
|
112
|
+
-- subject cannot inject a '|' to collide two distinct (issuer, subject) pairs onto one
|
|
113
|
+
-- owner uid. ALSO reject an email-shaped subject ('@'): the subject is stored VERBATIM in
|
|
114
|
+
-- agents.metadata.federation.subject, and the agents SELECT RLS is not row-scoped, so an
|
|
115
|
+
-- email `sub` would be a fleet-harvestable PII leak — the exact hazard the email is kept
|
|
116
|
+
-- out of agents.metadata to avoid. The operator's ENROLL_SUBJECT_CLAIM must resolve to an
|
|
117
|
+
-- opaque id; this is the fail-closed backstop, and the pgTAP PII guard asserts BOTH the
|
|
118
|
+
-- '@'-rejection and metadata carrying no '@'.
|
|
119
|
+
if position('|' in p_subject) > 0 then
|
|
120
|
+
raise exception 'verified subject must not contain a pipe separator character'
|
|
121
|
+
using errcode = '22023';
|
|
122
|
+
end if;
|
|
123
|
+
if position('@' in p_subject) > 0 then
|
|
124
|
+
raise exception 'verified subject must be an opaque id, not an email/@-bearing value'
|
|
125
|
+
using errcode = '22023';
|
|
126
|
+
end if;
|
|
127
|
+
|
|
128
|
+
-- (4) provider gate on (issuer, audience): must exist + supported + policy_approved.
|
|
129
|
+
-- Defense in depth atop the EF's signature verify (mirrors 20260719001200:154-181).
|
|
130
|
+
select *
|
|
131
|
+
into v_provider
|
|
132
|
+
from agent_board.identity_providers
|
|
133
|
+
where issuer = p_issuer
|
|
134
|
+
and audience = p_audience
|
|
135
|
+
limit 1;
|
|
136
|
+
if v_provider.id is null then
|
|
137
|
+
raise exception 'issuer/audience not in the identity-provider allowlist' using errcode = '42501';
|
|
138
|
+
end if;
|
|
139
|
+
if not v_provider.supported then
|
|
140
|
+
raise exception 'identity provider is not supported' using errcode = '42501';
|
|
141
|
+
end if;
|
|
142
|
+
if not v_provider.policy_approved then
|
|
143
|
+
raise exception 'identity provider is not policy-approved' using errcode = '42501';
|
|
144
|
+
end if;
|
|
145
|
+
|
|
146
|
+
-- (5) HUMAN-ONLY ceiling. allowed_agent_kinds is permissive on empty/null (the ceiling
|
|
147
|
+
-- is "unset"); reject ONLY when it is non-empty AND does not include 'human'.
|
|
148
|
+
if array_length(v_provider.allowed_agent_kinds, 1) is not null
|
|
149
|
+
and not ('human' = any (v_provider.allowed_agent_kinds)) then
|
|
150
|
+
raise exception 'provider ceiling does not permit the human kind' using errcode = '42501';
|
|
151
|
+
end if;
|
|
152
|
+
|
|
153
|
+
-- (6) SYNTHETIC uid — byte-identical derivation to enroll_federated_identity:198-199
|
|
154
|
+
-- (extensions.digest, since the pinned search_path lacks extensions). Idempotent per
|
|
155
|
+
-- (issuer, sub) so re-provisioning converges to ONE owner auth.users row = the shared
|
|
156
|
+
-- IDLINK uid. Pass issuer/subject VERBATIM (no trim/normalize) to stay byte-identical.
|
|
157
|
+
v_handle := 'fed-human-' || substr(
|
|
158
|
+
encode(extensions.digest(convert_to(p_issuer || '|' || p_subject, 'UTF8'), 'sha256'), 'hex'),
|
|
159
|
+
1, 32);
|
|
160
|
+
v_uid := agent_board._ensure_agent_auth_user(v_handle);
|
|
161
|
+
|
|
162
|
+
-- (7) Idempotent + no-self-reactivation, keyed on auth_user_id = the synthetic uid (ANY
|
|
163
|
+
-- status). An ACTIVE row -> return it (idempotent). A NON-active row -> fail closed
|
|
164
|
+
-- (42501): a suspended/revoked human must NOT re-mint a session (mirrors
|
|
165
|
+
-- 20260726000100:78-81 and reinforces the F-13 revocation lever). F-5: assert the row is
|
|
166
|
+
-- human before returning it as a human session (a non-human agent on this owner uid must
|
|
167
|
+
-- never yield a "human" provision).
|
|
168
|
+
select *
|
|
169
|
+
into v_existing
|
|
170
|
+
from agent_board.agents a
|
|
171
|
+
where a.auth_user_id = v_uid
|
|
172
|
+
order by (a.status = 'active') desc, a.created_at asc
|
|
173
|
+
limit 1;
|
|
174
|
+
if found then
|
|
175
|
+
if v_existing.status = 'active' then
|
|
176
|
+
if v_existing.kind <> 'human' then
|
|
177
|
+
raise exception 'existing agent on this identity is not human; refusing human session'
|
|
178
|
+
using errcode = '42501';
|
|
179
|
+
end if;
|
|
180
|
+
v_agent := v_existing;
|
|
181
|
+
-- Refresh the verified-email snapshot on re-provision (MERGE, see step 9).
|
|
182
|
+
if v_email is not null then
|
|
183
|
+
update auth.users u
|
|
184
|
+
set raw_user_meta_data = coalesce(u.raw_user_meta_data, '{}'::jsonb)
|
|
185
|
+
|| jsonb_build_object(
|
|
186
|
+
'fedsession_email', v_email,
|
|
187
|
+
'fedsession_email_verified', v_email_verified,
|
|
188
|
+
'fedsession_email_provenance', 'portal-claim-unverified')
|
|
189
|
+
where u.id = v_uid;
|
|
190
|
+
end if;
|
|
191
|
+
-- (auto-join before the idempotent return — see step 11 for the rationale).
|
|
192
|
+
perform agent_board._fedsession_autojoin_default_board(v_provider, v_agent.id);
|
|
193
|
+
insert into agent_board.enrollment_federation_audit(
|
|
194
|
+
issuer, audience, subject, human_identity_claim, provider_id,
|
|
195
|
+
agent_id, auth_user_id, requested_scopes, granted_scopes, mode)
|
|
196
|
+
values (
|
|
197
|
+
p_issuer, p_audience, p_subject, p_human_identity_claim, v_provider.id,
|
|
198
|
+
v_agent.id, v_uid, '{}', '{}', 'fedsession');
|
|
199
|
+
return query
|
|
200
|
+
select v_agent.id, v_agent.auth_user_id, v_agent.handle,
|
|
201
|
+
v_agent.kind::text, v_agent.status, v_provider.id, v_agent.created_at;
|
|
202
|
+
return;
|
|
203
|
+
else
|
|
204
|
+
raise exception 'a non-active identity already exists for this human; contact an operator'
|
|
205
|
+
using errcode = '42501';
|
|
206
|
+
end if;
|
|
207
|
+
end if;
|
|
208
|
+
|
|
209
|
+
-- (8) Create the kind='human' agent (self-owned: auth_user_id = owner_user_id = uid).
|
|
210
|
+
-- No agent_signing_keys row (humans hold no key). metadata.federation carries ONLY the
|
|
211
|
+
-- boolean email_verified — never the address (PII / fleet-readable RLS).
|
|
212
|
+
begin
|
|
213
|
+
insert into agent_board.agents(
|
|
214
|
+
agent_type,
|
|
215
|
+
kind,
|
|
216
|
+
handle,
|
|
217
|
+
display_name,
|
|
218
|
+
status,
|
|
219
|
+
auth_user_id,
|
|
220
|
+
owner_user_id,
|
|
221
|
+
metadata
|
|
222
|
+
)
|
|
223
|
+
values (
|
|
224
|
+
'human',
|
|
225
|
+
'human'::agent_board.agent_kind,
|
|
226
|
+
v_handle,
|
|
227
|
+
coalesce(nullif(trim(p_display_name), ''), 'Human'),
|
|
228
|
+
'active',
|
|
229
|
+
v_uid,
|
|
230
|
+
v_uid,
|
|
231
|
+
jsonb_build_object(
|
|
232
|
+
'federation', jsonb_build_object(
|
|
233
|
+
'mode', 'fedsession',
|
|
234
|
+
'issuer', p_issuer,
|
|
235
|
+
'audience', p_audience,
|
|
236
|
+
'subject', p_subject,
|
|
237
|
+
'providerId', v_provider.id,
|
|
238
|
+
'email_verified', v_email_verified,
|
|
239
|
+
'enrolledAt', now()
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
returning * into v_agent;
|
|
244
|
+
exception when unique_violation then
|
|
245
|
+
-- Concurrent first-provision race on the synthetic uid. F-5: re-select the ACTIVE row
|
|
246
|
+
-- and ASSERT it is human (fail closed otherwise) — never return a non-human agent as a
|
|
247
|
+
-- human session.
|
|
248
|
+
select *
|
|
249
|
+
into v_existing
|
|
250
|
+
from agent_board.agents a
|
|
251
|
+
where a.auth_user_id = v_uid
|
|
252
|
+
and a.status = 'active'
|
|
253
|
+
and a.kind = 'human'
|
|
254
|
+
limit 1;
|
|
255
|
+
if found then
|
|
256
|
+
v_agent := v_existing;
|
|
257
|
+
else
|
|
258
|
+
raise exception 'could not provision federated human (race or non-human on owner uid), please retry'
|
|
259
|
+
using errcode = '40001';
|
|
260
|
+
end if;
|
|
261
|
+
end;
|
|
262
|
+
|
|
263
|
+
-- (9) F-12/F-4: store the verified email ONLY in auth.users.raw_user_meta_data
|
|
264
|
+
-- (service-role-only), MERGED so _ensure_agent_auth_user's {slug, issuer} is preserved,
|
|
265
|
+
-- tagged provenance='portal-claim-unverified'. The definer owner (postgres) owns auth.*.
|
|
266
|
+
if v_email is not null then
|
|
267
|
+
update auth.users u
|
|
268
|
+
set raw_user_meta_data = coalesce(u.raw_user_meta_data, '{}'::jsonb)
|
|
269
|
+
|| jsonb_build_object(
|
|
270
|
+
'fedsession_email', v_email,
|
|
271
|
+
'fedsession_email_verified', v_email_verified,
|
|
272
|
+
'fedsession_email_provenance', 'portal-claim-unverified')
|
|
273
|
+
where u.id = v_uid;
|
|
274
|
+
end if;
|
|
275
|
+
|
|
276
|
+
-- (10) FEDSESSION auto-join (new). Baseline default-board membership for a fresh fed-human.
|
|
277
|
+
perform agent_board._fedsession_autojoin_default_board(v_provider, v_agent.id);
|
|
278
|
+
|
|
279
|
+
-- (11) F-6: append the audit row (mode='fedsession'). No requested/granted scopes on the
|
|
280
|
+
-- human path (authority is membership/role, not enrollment scopes) -> empty arrays.
|
|
281
|
+
insert into agent_board.enrollment_federation_audit(
|
|
282
|
+
issuer, audience, subject, human_identity_claim, provider_id,
|
|
283
|
+
agent_id, auth_user_id, requested_scopes, granted_scopes, mode)
|
|
284
|
+
values (
|
|
285
|
+
p_issuer, p_audience, p_subject, p_human_identity_claim, v_provider.id,
|
|
286
|
+
v_agent.id, v_uid, '{}', '{}', 'fedsession');
|
|
287
|
+
|
|
288
|
+
return query
|
|
289
|
+
select v_agent.id, v_agent.auth_user_id, v_agent.handle,
|
|
290
|
+
v_agent.kind::text, v_agent.status, v_provider.id, v_agent.created_at;
|
|
291
|
+
end;
|
|
292
|
+
$$;
|
|
293
|
+
|
|
294
|
+
-- (C) The auto-join helper. SECURITY DEFINER internal (service_role/postgres only — it is
|
|
295
|
+
-- only ever called from enroll_federated_human, itself service_role-gated). Idempotent,
|
|
296
|
+
-- first-time-only, active-board-only, non-fatal. Kept as a helper so BOTH provision paths
|
|
297
|
+
-- (idempotent re-provision + first insert) share one byte-identical implementation.
|
|
298
|
+
create or replace function agent_board._fedsession_autojoin_default_board(
|
|
299
|
+
p_provider agent_board.identity_providers,
|
|
300
|
+
p_agent_id uuid
|
|
301
|
+
)
|
|
302
|
+
returns void
|
|
303
|
+
language plpgsql
|
|
304
|
+
security definer
|
|
305
|
+
set search_path = agent_board, public
|
|
306
|
+
as $$
|
|
307
|
+
begin
|
|
308
|
+
if p_provider.default_board_id is null then
|
|
309
|
+
return;
|
|
310
|
+
end if;
|
|
311
|
+
-- The NOT EXISTS below is FIRST-TIME-ONLY (any existing membership row, incl. 'left'/
|
|
312
|
+
-- 'removed', suppresses the auto-join so a human who deliberately left is not re-joined).
|
|
313
|
+
-- Under concurrency it is NOT sufficient on its own: two concurrent re-provisions of the
|
|
314
|
+
-- same already-provisioned human can both pass NOT EXISTS, and the second insert then trips
|
|
315
|
+
-- the partial unique index idx_agent_board_board_memberships_active_unique. Catch that
|
|
316
|
+
-- unique_violation to a no-op (mirrors enroll_federated_human's step-8 agent-insert race
|
|
317
|
+
-- handling) so auto-join is NEVER fatal to the session mint and never duplicates.
|
|
318
|
+
begin
|
|
319
|
+
insert into agent_board.board_memberships(
|
|
320
|
+
board_id, agent_id, role, membership_state, created_by_agent_id, metadata)
|
|
321
|
+
select p_provider.default_board_id, p_agent_id, 'member', 'joined', p_agent_id,
|
|
322
|
+
jsonb_build_object('source', 'fedsession-autojoin', 'providerId', p_provider.id)
|
|
323
|
+
where exists (
|
|
324
|
+
select 1 from agent_board.boards b
|
|
325
|
+
where b.id = p_provider.default_board_id
|
|
326
|
+
and b.status = 'active')
|
|
327
|
+
and not exists (
|
|
328
|
+
select 1 from agent_board.board_memberships m
|
|
329
|
+
where m.board_id = p_provider.default_board_id
|
|
330
|
+
and m.agent_id = p_agent_id);
|
|
331
|
+
exception when unique_violation then
|
|
332
|
+
null; -- concurrent auto-join won the race; its membership stands. Non-fatal.
|
|
333
|
+
end;
|
|
334
|
+
end;
|
|
335
|
+
$$;
|
|
336
|
+
|
|
337
|
+
comment on function agent_board._fedsession_autojoin_default_board(agent_board.identity_providers, uuid) is
|
|
338
|
+
'FEDSESSION internal: idempotently auto-join a freshly provisioned federated human to the '
|
|
339
|
+
'provider''s designated default_board_id (baseline member role, joined). First-time-only '
|
|
340
|
+
'(any existing membership row suppresses it, so a human who left is not re-joined), '
|
|
341
|
+
'active-board-only, non-fatal. service_role/postgres only (called from enroll_federated_human).';
|
|
342
|
+
|
|
343
|
+
revoke all on function agent_board._fedsession_autojoin_default_board(agent_board.identity_providers, uuid)
|
|
344
|
+
from public, anon, authenticated;
|
|
345
|
+
grant execute on function agent_board._fedsession_autojoin_default_board(agent_board.identity_providers, uuid)
|
|
346
|
+
to service_role;
|
|
347
|
+
|
|
348
|
+
comment on function agent_board.enroll_federated_human(text, text, text, jsonb, text) is
|
|
349
|
+
'FEDSESSION (Option A): service_role no-PoP federated-HUMAN provisioning. Re-gates the '
|
|
350
|
+
'(issuer,audience) provider allowlist/policy + the human-only ceiling, establishes a '
|
|
351
|
+
'kind=human self-owned agent (auth_user_id = owner_user_id = a SYNTHETIC uid derived '
|
|
352
|
+
'byte-identically to enroll_federated_identity per (issuer,sub) = the IDLINK substrate), '
|
|
353
|
+
'stores the verified email ONLY in auth.users.raw_user_meta_data (provenance '
|
|
354
|
+
'portal-claim-unverified) with only the boolean email_verified in agents.metadata, '
|
|
355
|
+
'auto-joins the provider''s designated default board (baseline member; first-time-only, '
|
|
356
|
+
'non-fatal), and writes a mode=fedsession audit row. No signing key. Idempotent for an '
|
|
357
|
+
'active identity; a non-active identity fails closed (42501, no self-reactivation) — the '
|
|
358
|
+
'F-13 revocation lever (suspend the agent -> current_agent_id null -> RLS denies + re-mint '
|
|
359
|
+
'blocked). service_role-only; never returns or touches a Portal token.';
|
|
360
|
+
|
|
361
|
+
revoke all on function agent_board.enroll_federated_human(text, text, text, jsonb, text)
|
|
362
|
+
from public, anon, authenticated;
|
|
363
|
+
grant execute on function agent_board.enroll_federated_human(text, text, text, jsonb, text)
|
|
364
|
+
to service_role;
|