@consiliency/agent-board-schema 1.28.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 +28 -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 +66 -3
- package/src/tooling.test.mjs +47 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +144 -29
- 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 +142 -71
- package/tests/agent_board_receipt_subject_gate.test.sql +624 -0
- package/tests/agent_board_signer_lifecycle.test.sql +23 -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 +12 -0
- package/tests/fixtures/test-prereqs.sql +420 -11
- package/tests/isolation/expected/ledger_authority_revoke.out +219 -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 +115 -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
|
@@ -508,6 +508,34 @@
|
|
|
508
508
|
{
|
|
509
509
|
"id": "20260909000100_agent_board_ledger_revoked_authority",
|
|
510
510
|
"sha256": "7522e32c61f8845e511255d94d273d98eb7b72b2f906976c3a2494629927462d"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
"id": "20260910000100_agent_board_ceiling_lock_comment_truth",
|
|
514
|
+
"sha256": "b8ddc658be347d9e7207111968d0b8de7a88759cb3cf181f61c4ef306086f384"
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"id": "20260911000100_agent_board_ledger_revoked_board_creation",
|
|
518
|
+
"sha256": "4291af2bd6c93b7d9f8ec596f41dc62edf941ea10a51529df04e223d1cbc50c5"
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
"id": "20260912000100_agent_board_receipt_subject_gate",
|
|
522
|
+
"sha256": "1083f7720ccab9c9b422415f2a8eb743de5d84b0ab8d8689059bd355514e9aa9"
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"id": "20260913000100_agent_board_task_thread_lock_order",
|
|
526
|
+
"sha256": "259a05c4a6603518ab449e799fcb235b0e16a6650954cb2db19b436c0a7a1e83"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
"id": "20260914000100_agent_board_claim_subject_gate",
|
|
530
|
+
"sha256": "88207b2e3f28ec8022ad4ca921721ebba877668042c66b5ac74c85e4c8cefc21"
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
"id": "20260915000100_agent_board_fedsession_stored_identity_gate",
|
|
534
|
+
"sha256": "5a8f4abb822e30c2a695a310bb1742f2565bb2afd0b6a06cad43fcbdd106ad5e"
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"id": "20260916000100_agent_board_federated_identity_injective_slug",
|
|
538
|
+
"sha256": "d9f05e18094954f2e4ee32bf7b129f0a9ffc39bc800a5efa9cf7938e1555c8dc"
|
|
511
539
|
}
|
|
512
540
|
]
|
|
513
541
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
-- Consiliency/message-board#277 -- the ceiling lock COMMENT claimed a property
|
|
2
|
+
-- three of its four readers did not have, and the suite already said so.
|
|
3
|
+
--
|
|
4
|
+
-- WHAT WAS FALSE. 20260901000110:110 commented _lock_board_authority_ceiling as
|
|
5
|
+
--
|
|
6
|
+
-- every RPC that writes the ceiling and every RPC that reads it for
|
|
7
|
+
-- authorization takes this FIRST, before any boards row lock
|
|
8
|
+
--
|
|
9
|
+
-- while the catalog-derived class assertion in
|
|
10
|
+
-- agent_board_atomic_fleet_onboarding.test.sql enumerated the ceiling-authorization
|
|
11
|
+
-- readers from pg_proc and declared most of them UNSERIALIZED. The COMMENT is the
|
|
12
|
+
-- most discoverable statement of intent about this lock -- it is attached to the
|
|
13
|
+
-- lock itself -- so the next person reasoning about a ceiling race would have read
|
|
14
|
+
-- it and concluded their path was serialized while the suite said the opposite.
|
|
15
|
+
--
|
|
16
|
+
-- THIS IS A COMMENT FIX, AND ONLY A COMMENT FIX. The ceiling-ABSENCE race the old
|
|
17
|
+
-- text implied was closed is real for the UNSERIALIZED readers and was knowingly
|
|
18
|
+
-- left open (20260901000110's own header records the three it did not examine).
|
|
19
|
+
-- Nothing here acquires a lock, changes a body, or drops anything: this migration
|
|
20
|
+
-- contains one `comment on function` and no other statement, so no ACL and no other
|
|
21
|
+
-- COMMENT is disturbed.
|
|
22
|
+
--
|
|
23
|
+
-- WHY NOT SERIALIZE THE REMAINDER INSTEAD, which would have made the old prose
|
|
24
|
+
-- true. Each acquisition is a NEW edge in a lock graph that has already produced a
|
|
25
|
+
-- reproduced ABBA deadlock (#269 F4) and needed a fifth round of order analysis in
|
|
26
|
+
-- #276. Every reader would need its own total-order analysis and interleaving
|
|
27
|
+
-- proof; that is not a comment fix wearing a code hat, and a lock added to make
|
|
28
|
+
-- prose true is how deadlocks ship. Recorded here so the option is not re-litigated
|
|
29
|
+
-- as an oversight: it was considered and deliberately deferred per reader.
|
|
30
|
+
--
|
|
31
|
+
-- WHAT THE SET ACTUALLY IS, derived from pg_proc on a fresh replay of this bundle
|
|
32
|
+
-- and not from #277's issue text (which predates #276) or from memory:
|
|
33
|
+
--
|
|
34
|
+
-- readers of the ceiling for authorization (they call _grant_within_ceiling)
|
|
35
|
+
-- agent_board.grant_board_role(uuid,uuid,text,uuid) serialized
|
|
36
|
+
-- agent_board.onboard_federated_fleet_agent(...) serialized
|
|
37
|
+
-- agent_board._redeem_unbounded_invite_for_agent(text,uuid) UNSERIALIZED
|
|
38
|
+
-- agent_board.create_invite_link(...) UNSERIALIZED
|
|
39
|
+
-- writers of board_authority_ceilings
|
|
40
|
+
-- agent_board.set_board_authority_ceiling(uuid,text,uuid) serialized
|
|
41
|
+
--
|
|
42
|
+
-- grant_board_role joined the serialized side in #276 (20260909000100), which is
|
|
43
|
+
-- exactly why the issue's own list is already one entry stale: it says three of
|
|
44
|
+
-- four are UNSERIALIZED and two of four are.
|
|
45
|
+
--
|
|
46
|
+
-- HOW THIS COMMENT IS PREVENTED FROM ROTTING AGAIN -- the point of the exercise.
|
|
47
|
+
-- The two lists are written in a fixed machine-readable form,
|
|
48
|
+
--
|
|
49
|
+
-- WRITERS[serialized: <names>][UNSERIALIZED: <names>]
|
|
50
|
+
-- READERS[serialized: <names>][UNSERIALIZED: <names>]
|
|
51
|
+
--
|
|
52
|
+
-- and agent_board_atomic_fleet_onboarding.test.sql parses those blocks out of
|
|
53
|
+
-- obj_description() and compares them to the SAME pg_proc-derived classes its
|
|
54
|
+
-- existing registry assertion uses. A reader or writer added, removed, or moved
|
|
55
|
+
-- between serialized and UNSERIALIZED now fails an assertion that NAMES the
|
|
56
|
+
-- disagreement, and so does deleting or mangling the block (the parse yields NULL,
|
|
57
|
+
-- which cannot equal a catalog list). The prose around the blocks still has to be
|
|
58
|
+
-- read by a human, so it is kept to claims that have their own named pin:
|
|
59
|
+
-- * the setter takes the lock before its owner gate and its upsert
|
|
60
|
+
-- (agent_board_atomic_fleet_onboarding.test.sql + isolation spec
|
|
61
|
+
-- ceiling_setter_lock_before_upsert.spec);
|
|
62
|
+
-- * onboard_federated_fleet_agent takes it before its boards row lock
|
|
63
|
+
-- (same file + isolation spec onboarding_ceiling_lock_order.spec);
|
|
64
|
+
-- * grant_board_role takes no boards row lock at all, so it makes no ordering
|
|
65
|
+
-- claim -- checked against its live body, not assumed.
|
|
66
|
+
-- The old "before any boards row lock" was applied to every reader at once and had
|
|
67
|
+
-- a pin for exactly one of them. A single-word `serialized` that a test compares to
|
|
68
|
+
-- the catalog is worth more than a paragraph nothing checks.
|
|
69
|
+
--
|
|
70
|
+
-- NAMES, NOT SIGNATURES, in the blocks: onboard_federated_fleet_agent has 23
|
|
71
|
+
-- parameters and a signature list would be unreadable. Bare names are only an
|
|
72
|
+
-- identity while no two class members share a name, so the test asserts that
|
|
73
|
+
-- separately (as it already does for this helper's overload count).
|
|
74
|
+
--
|
|
75
|
+
-- KNOWN SIBLING, NOT FIXED HERE. set_board_authority_ceiling's live body still
|
|
76
|
+
-- carries the same overclaim as an inline comment: "Same board-keyed advisory lock
|
|
77
|
+
-- every ceiling READER for authorization takes." Correcting it means recreating a
|
|
78
|
+
-- SECURITY DEFINER function -- a far wider blast radius than this migration's --
|
|
79
|
+
-- to change prose, so it is left to a deliberate decision instead of being smuggled
|
|
80
|
+
-- in here. It is recorded POSITIVELY as a failing-when-fixed assertion in
|
|
81
|
+
-- agent_board_atomic_fleet_onboarding.test.sql rather than as a note in a PR body,
|
|
82
|
+
-- so it cannot be quietly lost.
|
|
83
|
+
|
|
84
|
+
comment on function agent_board._lock_board_authority_ceiling(uuid) is
|
|
85
|
+
'Board-keyed transaction advisory lock serializing the ABSENCE of a board_authority_ceilings row. Held to commit. '
|
|
86
|
+
'WRITERS[serialized: set_board_authority_ceiling][UNSERIALIZED: (none)] -- the RPCs that write board_authority_ceilings. set_board_authority_ceiling takes this lock before its owner gate and its upsert, and therefore before the boards row that upsert reaches through board_authority_ceilings'' FK on INSERT. A direct table write by service_role bypasses every function-level lock (deliberate, 20260717000400). '
|
|
87
|
+
'READERS[serialized: grant_board_role, onboard_federated_fleet_agent][UNSERIALIZED: _redeem_unbounded_invite_for_agent, create_invite_link] -- the RPCs that read the ceiling for authorization through _grant_within_ceiling. onboard_federated_fleet_agent takes this lock BEFORE its boards row lock, because the writer reaches boards through that FK and the two orders must agree (inverted, they deadlocked). grant_board_role takes no boards row lock, so it makes no ordering claim. The UNSERIALIZED readers evaluate the ceiling WITHOUT serializing against its absence: a concurrent set_board_authority_ceiling can commit a first ceiling row after such a reader observed none, and it passes as default-owner. That race is real, known and accepted (#277); it is deliberately NOT closed by adding acquisitions, since each new edge in this lock graph has already produced a reproduced ABBA deadlock once (#269 F4). '
|
|
88
|
+
'Both lists are machine-compared against pg_proc by agent_board_atomic_fleet_onboarding.test.sql, which fails when this COMMENT and the catalog disagree -- do not edit a list by hand without the catalog. '
|
|
89
|
+
'Internal: it takes a lock with NO authorization check on an arbitrary board id, so it must never be grantable to authenticated.';
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
-- Consiliency/message-board#280 — revoked means revoked: a fleet-revoked agent cannot create a
|
|
2
|
+
-- board either.
|
|
3
|
+
--
|
|
4
|
+
-- THE RATIFIED DECISION, and what it overturns. #276 (20260909000100) gated the three board
|
|
5
|
+
-- authority chokepoints and both invite halves, and DELIBERATELY left `create_board` open. Its
|
|
6
|
+
-- argument was narrow and sound: creating a fresh board confers nothing on anyone else, takes
|
|
7
|
+
-- nothing from an existing board, and the ratified criterion for #276 spoke about GRANTING board
|
|
8
|
+
-- roles — and here there is no grantee, because the board did not exist a moment earlier. The
|
|
9
|
+
-- owner has since decided that the simpler operator-facing rule is worth more than that
|
|
10
|
+
-- distinction. A fleet-revoked principal creates nothing. This migration implements the
|
|
11
|
+
-- decision, not the debate.
|
|
12
|
+
--
|
|
13
|
+
-- THIS SUPERSEDES the "NAMED RESIDUAL (#280)" paragraph in 20260909000100's header. That file's
|
|
14
|
+
-- BYTES ARE NOT EDITED HERE, and that is a rule rather than a preference: #264's release gate
|
|
15
|
+
-- keys on the manifest's `aggregateSha256`, which moves for a byte edit to a shipped forward
|
|
16
|
+
-- migration — so amending 20260909000100's prose would make the published 1.28.0 artifact stop
|
|
17
|
+
-- being the bytes at 1.28.0, which is the exact failure #264 exists to catch. The record is
|
|
18
|
+
-- corrected forward, here, and the pgTAP file's RESIDUAL assertions are flipped in the same
|
|
19
|
+
-- commit so the change is behaviour rather than commentary.
|
|
20
|
+
--
|
|
21
|
+
-- ONE MECHANISM, NOT A SECOND. The gate is #276's `_assert_actor_not_fleet_revoked` — same
|
|
22
|
+
-- helper, same predicate (the existence of an agent-subject `fleet_revocation_events` row), same
|
|
23
|
+
-- 42501 with the `fleet_revoked:` prefix, same share lock. Every judgement recorded in
|
|
24
|
+
-- 20260909000100 carries over unchanged and is deliberately NOT re-litigated here: agent-wide
|
|
25
|
+
-- and binding-blind (JUDGEMENT 1), no supersession clause, and a principal that was never in a
|
|
26
|
+
-- fleet has no row and is unaffected by construction (JUDGEMENT 2). That last one is the
|
|
27
|
+
-- capability-regression risk on THIS function and it is the property most worth pinning, because
|
|
28
|
+
-- `create_board` is the one gated site that every ordinary user reaches: the new pgTAP section
|
|
29
|
+
-- asserts a NON-FLEET agent still creates a board and is still seeded its owner, POSITIVELY, on
|
|
30
|
+
-- both the authenticated and the service_role route.
|
|
31
|
+
--
|
|
32
|
+
-- ============================================================================
|
|
33
|
+
-- THE CLASS — create_board was ALREADY IN IT, listed as an exemption.
|
|
34
|
+
-- ============================================================================
|
|
35
|
+
-- #276's authority-write class is catalog-derived and matches ANY insert into
|
|
36
|
+
-- `board_memberships` (its memberships arm was deliberately made shape-blind BECAUSE
|
|
37
|
+
-- `create_board` seeds `role = 'owner'` with an INSERT that an UPDATE-shaped regex could not
|
|
38
|
+
-- see). So this function has always been a member of the class; it was carried as one of four
|
|
39
|
+
-- EXEMPTIONS with a positive disposition.
|
|
40
|
+
--
|
|
41
|
+
-- This migration moves it from exempt to gated, and the exemption's positive disposition is
|
|
42
|
+
-- REMOVED rather than left behind claiming something that is no longer true. The old EXEMPTION B
|
|
43
|
+
-- assertions (B1: it declares no role parameter; B2: the seat is the literal 'owner' for the
|
|
44
|
+
-- board's own created_by_agent_id; B3: the slug-collision branch cannot elevate a membership on
|
|
45
|
+
-- a pre-existing board) all remain TRUE — but they exist to license an exemption that no longer
|
|
46
|
+
-- exists, and a live assertion that licenses nothing is the shape this repo has been bitten by.
|
|
47
|
+
-- They are deleted, the exemption list drops from four to three, and the ledger-consult set
|
|
48
|
+
-- grows from five to six. Both sets are derived from pg_catalog on each run, so the two edits
|
|
49
|
+
-- must agree with the shipped schema or the difference NAMES create_board.
|
|
50
|
+
--
|
|
51
|
+
-- ============================================================================
|
|
52
|
+
-- LOCK ORDER — stated in full, and checked against #276 and #269 by name.
|
|
53
|
+
-- ============================================================================
|
|
54
|
+
-- The gate is placed at the point where BOTH branches converge, immediately before
|
|
55
|
+
-- `return query`, i.e. AFTER every write. That is the same placement, for the same reason, as
|
|
56
|
+
-- `set_board_authority_ceiling`, `set_board_role_gate` and `create_invite_link`.
|
|
57
|
+
--
|
|
58
|
+
-- create_board, FRESH-BOARD branch
|
|
59
|
+
-- boards(INSERT, a row this transaction created)
|
|
60
|
+
-- -> board_memberships(INSERT; FK KEY SHARE on agents(actor) and on the new boards row)
|
|
61
|
+
-- -> agents(SHARE)
|
|
62
|
+
-- create_board, SLUG-COLLISION branch
|
|
63
|
+
-- boards(FOR UPDATE, a pre-existing row)
|
|
64
|
+
-- -> agents(SHARE)
|
|
65
|
+
--
|
|
66
|
+
-- Both are boards-before-agents, which is the canonical order (#194 established boards before
|
|
67
|
+
-- auth.users before agents after a reproduced `deadlock detected`, and
|
|
68
|
+
-- onboard_federated_fleet_agent takes advisory -> boards -> auth.users -> agents ->
|
|
69
|
+
-- board_memberships -> agents -> agent_signing_keys). Every order above is a subsequence of that.
|
|
70
|
+
--
|
|
71
|
+
-- HOISTING THE GATE IS THE ABBA, and it is #269 F4's exact pair. Placed after actor resolution
|
|
72
|
+
-- and before the slug read, the collision branch would become agents(SHARE) -> boards(FOR
|
|
73
|
+
-- UPDATE): one session inside onboarding holds a `boards` row and wants `agents`, while a
|
|
74
|
+
-- concurrent `create_board` holds that `agents` row at SHARE and wants that `boards` row at FOR
|
|
75
|
+
-- UPDATE. SHARE and FOR UPDATE conflict, so both sides genuinely wait. That is the cycle #269
|
|
76
|
+
-- reproduced as `deadlock detected` and #276 refused to re-create; the placement here is
|
|
77
|
+
-- therefore load-bearing and must not be "tidied" to read more naturally.
|
|
78
|
+
--
|
|
79
|
+
-- Correctness is unaffected by the position: the function is one transaction, so the raise rolls
|
|
80
|
+
-- back the boards INSERT (or UPDATE), the membership seat and the emitted membership event. A
|
|
81
|
+
-- refused caller never sees a board id and no `owner_seed` event survives. This is #269's and
|
|
82
|
+
-- #276's ratified argument for the identical placement, not a new one.
|
|
83
|
+
--
|
|
84
|
+
-- WHAT THE FOR SHARE ACTUALLY BUYS, per branch, measured rather than claimed — the same honesty
|
|
85
|
+
-- 20260909000100 applied to the mint path:
|
|
86
|
+
--
|
|
87
|
+
-- * FRESH-BOARD branch: nothing. `board_memberships_agent_id_fkey` (and
|
|
88
|
+
-- `board_memberships_created_by_agent_id_fkey`) make the seat INSERT take FOR KEY SHARE on
|
|
89
|
+
-- the actor's `agents` row, and that insert PRECEDES the gate. KEY SHARE already conflicts
|
|
90
|
+
-- with `revoke_fleet_subject`'s FOR UPDATE, so the serialization on this path is supplied by
|
|
91
|
+
-- the FK; by the time the gate runs, this transaction already holds the row and upgrading
|
|
92
|
+
-- KEY SHARE to SHARE adds no new wait. The isolation permutations for this branch pin the
|
|
93
|
+
-- OUTCOME (it blocks, then fails closed, leaving no board and no seat) and CANNOT pin this
|
|
94
|
+
-- lock, because the FK acquisition always precedes the gate.
|
|
95
|
+
-- * SLUG-COLLISION branch: everything. The `boards` UPDATE sets name/description/status/
|
|
96
|
+
-- visibility/metadata/updated_at and does NOT touch `created_by_agent_id`, so no FK trigger
|
|
97
|
+
-- fires and no `agents` lock is taken anywhere else in that branch. The gate's FOR SHARE is
|
|
98
|
+
-- the FIRST and ONLY `agents` lock on that path, which makes it the genuine serialization
|
|
99
|
+
-- point — and gives this migration the lock discriminator the mint path could not provide.
|
|
100
|
+
-- `ledger_authority_revoke.spec` permutation 11 is that discriminator: with the FOR SHARE
|
|
101
|
+
-- removed, the reopen reads its own snapshot and SUCCEEDS instead of blocking.
|
|
102
|
+
--
|
|
103
|
+
-- CHECKED BY NAME against the functions #269 F4 and #276 named:
|
|
104
|
+
-- * revoke_fleet_subject — fleet_machines -> agents -> fleet_machine_agents ->
|
|
105
|
+
-- fleet_revocation_events. It touches neither `boards` nor `board_memberships`, so it shares
|
|
106
|
+
-- no second operand with either branch above: it can only be WAITED ON, which is the point.
|
|
107
|
+
-- * _revoke_agent_principal — agents -> agent_signing_keys. Same: no boards, no memberships.
|
|
108
|
+
-- * onboard_federated_fleet_agent — advisory -> boards -> auth.users -> agents ->
|
|
109
|
+
-- board_memberships -> agents -> agent_signing_keys. Both branches above are subsequences.
|
|
110
|
+
-- * enroll_federated_identity — takes `boards FOR KEY SHARE` (20260903000100:112-114, hoisted
|
|
111
|
+
-- there by #194 to fix a reproduced deadlock) before it reaches any membership work. Again
|
|
112
|
+
-- boards-before-agents; create_board does not invert against it.
|
|
113
|
+
--
|
|
114
|
+
-- ============================================================================
|
|
115
|
+
-- THE agents/board_memberships CYCLE CLASS (signer_lifecycle section 9) — WHY create_board
|
|
116
|
+
-- DOES NOT JOIN IT, ASSERTED RATHER THAN ASSUMED.
|
|
117
|
+
-- ============================================================================
|
|
118
|
+
-- #276 discovered, the hard way, that adding this same gate to `grant_board_role` put it in the
|
|
119
|
+
-- class that section 9 pins from the call graph — every function row-locking BOTH `agents` and
|
|
120
|
+
-- `board_memberships` must take the board advisory lock — and the assertion FAILED and named it.
|
|
121
|
+
-- The obvious question here is whether create_board now joins that class too. It does not, and
|
|
122
|
+
-- the reason is substantive rather than a regex accident:
|
|
123
|
+
--
|
|
124
|
+
-- * create_board's ONLY `board_memberships` write is the owner seat, and it happens ONLY on
|
|
125
|
+
-- the fresh-board branch — against a `boards` row this same transaction inserted moments
|
|
126
|
+
-- earlier. No other session can hold, see or contend for a membership row on a board that
|
|
127
|
+
-- does not exist outside this transaction, so create_board cannot be one end of the
|
|
128
|
+
-- (agents, board_memberships) cycle F4 reproduced. The `on conflict` clause is defensive and
|
|
129
|
+
-- structurally unreachable on a board with no memberships.
|
|
130
|
+
-- * And it COULD NOT take the advisory lock even if the class demanded it. That lock is keyed
|
|
131
|
+
-- on the board id, which on the fresh path does not exist until after the `boards` INSERT —
|
|
132
|
+
-- so taking it would give boards -> advisory, inverting against
|
|
133
|
+
-- onboard_federated_fleet_agent's advisory -> boards. The fix would BE the cycle, which is
|
|
134
|
+
-- the shape #276 rejected for the redeem path.
|
|
135
|
+
--
|
|
136
|
+
-- Section 9's assertion is left unchanged and re-run: it must still report exactly three members
|
|
137
|
+
-- (grant_board_role, onboard_federated_fleet_agent, signed_grant_board_role). That it stays
|
|
138
|
+
-- green is CHECKED after this migration is applied, not inferred from this paragraph.
|
|
139
|
+
--
|
|
140
|
+
-- ============================================================================
|
|
141
|
+
-- SCOPE — THE COLLISION BRANCH IS GATED TOO, and that is more than "board creation".
|
|
142
|
+
-- ============================================================================
|
|
143
|
+
-- Recorded explicitly rather than shipped quietly. `create_board` is two operations behind one
|
|
144
|
+
-- name: it creates a board, and — when the slug already exists AND the actor holds an ACTIVE
|
|
145
|
+
-- owner membership on it — it renames/reopens/re-visibilities that existing board. The second
|
|
146
|
+
-- branch writes NO authority relation and is therefore not in the class this issue is about.
|
|
147
|
+
--
|
|
148
|
+
-- The gate at the convergence point covers both, deliberately:
|
|
149
|
+
-- * The ratified rule is a rule about a PRINCIPAL, not about a syntax. "A fleet-revoked agent
|
|
150
|
+
-- cannot create a board, but can still flip an existing board to `visibility = 'open'` and
|
|
151
|
+
-- `status = 'active'`" is precisely the conditional, hard-to-state behaviour the owner's
|
|
152
|
+
-- decision replaced.
|
|
153
|
+
-- * Reopening an archived board and making it publicly visible is an operator act on a
|
|
154
|
+
-- resource that DOES already exist and DOES already have other members — materially closer
|
|
155
|
+
-- to the invite half #276 closed than to the "confers nothing on anyone else" argument that
|
|
156
|
+
-- licensed the original exemption.
|
|
157
|
+
-- A revoked NON-owner colliding on a slug still gets the pre-existing `board slug already in
|
|
158
|
+
-- use` 42501 from the owner check, which runs first; the fleet_revoked refusal is reachable only
|
|
159
|
+
-- by the actor who would otherwise have been allowed through.
|
|
160
|
+
--
|
|
161
|
+
-- ============================================================================
|
|
162
|
+
-- BLAST RADIUS — create_board is `authenticated`-callable and on a live path.
|
|
163
|
+
-- ============================================================================
|
|
164
|
+
-- IN-DATABASE CALLERS: NONE. Swept from pg_catalog on the shipped schema, over every namespace
|
|
165
|
+
-- and all three carriers of SQL — `pg_proc.prosrc ~ '\mcreate_board\s*\('` (excluding the
|
|
166
|
+
-- function itself), `pg_views.definition`, and `pg_trigger` — exactly zero referrers. So unlike
|
|
167
|
+
-- grant_board_role (whose signed wrapper inherits the gate by construction) there is no second
|
|
168
|
+
-- in-schema route to keep in step, and no in-DB caller can be broken by this change.
|
|
169
|
+
--
|
|
170
|
+
-- OUT-OF-DATABASE CALLERS, enumerated and unchanged by this migration — the signature, the
|
|
171
|
+
-- return shape and the ACL are all identical, so nothing needs a client release:
|
|
172
|
+
-- * SDK AgentBoardClient.createBoard (packages/agent-board-client/src/client.ts) — posts
|
|
173
|
+
-- to the `create_board` RPC; `create_board` is in the client's actor-argument map
|
|
174
|
+
-- and its M-posture table, neither of which changes.
|
|
175
|
+
-- * MCP the `agent_board.create_board` tool (packages/agent-board-mcp/src/tools.ts).
|
|
176
|
+
-- * CLI the standalone board command, AND the enroll flow's DEFAULT post-enroll intent
|
|
177
|
+
-- (`kind: "create_board"` with the agent's own slug —
|
|
178
|
+
-- packages/agent-board-client/src/enroll_command.ts).
|
|
179
|
+
--
|
|
180
|
+
-- THE ONE LIVE CONSEQUENCE WORTH NAMING, because a subscriber should read it here and not
|
|
181
|
+
-- discover it: a FLEET-REVOKED agent that re-runs enroll now gets a durable enrollment and then
|
|
182
|
+
-- a `fleet_revoked:` failure at the default board-create step. Enrollment itself is untouched.
|
|
183
|
+
-- That is the decision working exactly as ratified — a revoked principal ends up enrolled and
|
|
184
|
+
-- boardless rather than enrolled and owning a fresh namespace — but it is a behaviour change on
|
|
185
|
+
-- a path every CLI user hits, and it is the item for an adopter's migration checklist. A
|
|
186
|
+
-- never-revoked agent, and any principal that was never in a fleet at all, is unaffected: there
|
|
187
|
+
-- is no ledger row to find (JUDGEMENT 2), and the pgTAP file asserts that positively rather than
|
|
188
|
+
-- by the absence of a refusal.
|
|
189
|
+
--
|
|
190
|
+
-- CREATE OR REPLACE preserves the ACL and the COMMENT, but both are restated below anyway — the
|
|
191
|
+
-- behaviour changed, so the comment must, and this repo has been bitten by a recreation that
|
|
192
|
+
-- silently re-granted PUBLIC.
|
|
193
|
+
|
|
194
|
+
-- ---------------------------------------------------------------------------
|
|
195
|
+
-- create_board — verbatim from 20260807000100:272 plus ONE line: the #276 gate at the point
|
|
196
|
+
-- where both branches converge. Nothing else in the body changes.
|
|
197
|
+
-- ---------------------------------------------------------------------------
|
|
198
|
+
create or replace function agent_board.create_board(p_created_by_agent_id uuid, p_slug text, p_name text, p_description text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb, p_visibility text DEFAULT 'private'::text)
|
|
199
|
+
RETURNS TABLE(board_id uuid, slug text, name text, status text, visibility text, created_at timestamp with time zone, updated_at timestamp with time zone)
|
|
200
|
+
LANGUAGE plpgsql
|
|
201
|
+
SECURITY DEFINER
|
|
202
|
+
SET search_path TO 'agent_board', 'public'
|
|
203
|
+
as $$
|
|
204
|
+
#variable_conflict use_column
|
|
205
|
+
declare
|
|
206
|
+
v_seed_state text;
|
|
207
|
+
v_actor uuid;
|
|
208
|
+
v_existing agent_board.boards%rowtype;
|
|
209
|
+
v_board agent_board.boards%rowtype;
|
|
210
|
+
begin
|
|
211
|
+
if nullif(trim(p_slug), '') is null or nullif(trim(p_name), '') is null then
|
|
212
|
+
raise exception 'board slug and name are required' using errcode = '22023';
|
|
213
|
+
end if;
|
|
214
|
+
if coalesce(p_visibility, 'private') not in ('private', 'invite_only', 'open', 'directory_hidden') then
|
|
215
|
+
raise exception 'invalid board visibility' using errcode = '22023';
|
|
216
|
+
end if;
|
|
217
|
+
|
|
218
|
+
-- Effective actor: service_role -> supplied arg (create-on-behalf-of);
|
|
219
|
+
-- authenticated -> current_agent_id() and rejects arg <> current (42501).
|
|
220
|
+
v_actor := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
|
|
221
|
+
|
|
222
|
+
if not exists (select 1 from agent_board.agents where id = v_actor) then
|
|
223
|
+
raise exception 'agent not found' using errcode = 'P0002';
|
|
224
|
+
end if;
|
|
225
|
+
|
|
226
|
+
select * into v_existing
|
|
227
|
+
from agent_board.boards b
|
|
228
|
+
where b.slug = trim(p_slug);
|
|
229
|
+
|
|
230
|
+
if found then
|
|
231
|
+
-- Slug-collision path is OWNER-ONLY, keyed on CURRENT authority: the actor must
|
|
232
|
+
-- hold an ACTIVE owner-role membership on the existing board — NOT merely be its
|
|
233
|
+
-- original `created_by_agent_id`. A demoted or departed original creator no longer
|
|
234
|
+
-- owns the board and must NOT be able to re-open/rename/flip-visibility it.
|
|
235
|
+
if not exists (
|
|
236
|
+
select 1 from agent_board.board_memberships m
|
|
237
|
+
where m.board_id = v_existing.id
|
|
238
|
+
and m.agent_id = v_actor
|
|
239
|
+
and m.role = 'owner'
|
|
240
|
+
and m.membership_state in ('joined', 'subscribed')
|
|
241
|
+
) then
|
|
242
|
+
raise exception 'board slug already in use' using errcode = '42501';
|
|
243
|
+
end if;
|
|
244
|
+
|
|
245
|
+
update agent_board.boards as b
|
|
246
|
+
set name = trim(p_name),
|
|
247
|
+
description = nullif(trim(p_description), ''),
|
|
248
|
+
status = 'active',
|
|
249
|
+
visibility = coalesce(p_visibility, 'private'),
|
|
250
|
+
metadata = coalesce(b.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
|
|
251
|
+
updated_at = now()
|
|
252
|
+
where b.id = v_existing.id
|
|
253
|
+
returning * into v_board;
|
|
254
|
+
-- No owner re-seed: the owner was seeded at this board's original creation, and
|
|
255
|
+
-- re-promoting on every create_board would resurrect a deliberately demoted owner.
|
|
256
|
+
else
|
|
257
|
+
insert into agent_board.boards as b(slug, name, description, created_by_agent_id, visibility, metadata)
|
|
258
|
+
values (
|
|
259
|
+
trim(p_slug),
|
|
260
|
+
trim(p_name),
|
|
261
|
+
nullif(trim(p_description), ''),
|
|
262
|
+
v_actor,
|
|
263
|
+
coalesce(p_visibility, 'private'),
|
|
264
|
+
coalesce(p_metadata, '{}'::jsonb)
|
|
265
|
+
)
|
|
266
|
+
returning * into v_board;
|
|
267
|
+
|
|
268
|
+
-- Seed the creator's owner membership, bound to the board's ACTUAL
|
|
269
|
+
-- created_by_agent_id (never the raw param). Idempotent against the active
|
|
270
|
+
-- partial-unique index so at most one active owner row exists.
|
|
271
|
+
insert into agent_board.board_memberships as bm(board_id, agent_id, membership_state, role, created_by_agent_id)
|
|
272
|
+
values (v_board.id, v_board.created_by_agent_id, 'joined', 'owner', v_board.created_by_agent_id)
|
|
273
|
+
on conflict (board_id, agent_id) where (membership_state in ('joined', 'subscribed'))
|
|
274
|
+
do update set role = 'owner', updated_at = now()
|
|
275
|
+
returning bm.membership_state into v_seed_state;
|
|
276
|
+
-- The creator seeds their OWN owner seat, so the session is the subject and this emits
|
|
277
|
+
-- target-authored. Under service_role it emits operator-designated instead -- correct
|
|
278
|
+
-- either way, because the helper reads the session rather than being told.
|
|
279
|
+
--
|
|
280
|
+
-- v_seed_state, NOT a literal 'joined'. The ON CONFLICT above lifts only the ROLE, so a
|
|
281
|
+
-- board whose creator already holds a `subscribed` row stays subscribed while a literal
|
|
282
|
+
-- would record `joined` -- a ledger contradicting the table. Same defect the redemption
|
|
283
|
+
-- path had; fixing one instance without sweeping the class is how it survived here.
|
|
284
|
+
perform agent_board._emit_membership_event(v_board.id, v_board.created_by_agent_id, v_seed_state, 'owner_seed');
|
|
285
|
+
end if;
|
|
286
|
+
|
|
287
|
+
-- #280. Revoked means revoked: a fleet-revoked agent creates nothing. #276 gated the three
|
|
288
|
+
-- authority chokepoints and both invite halves and left this function open on a stated
|
|
289
|
+
-- argument (a fresh board confers nothing on anyone else); the owner overturned that in favour
|
|
290
|
+
-- of the simpler rule, and this line is the whole of the change.
|
|
291
|
+
--
|
|
292
|
+
-- SITED HERE, at the convergence of both branches and AFTER every write, for the lock order.
|
|
293
|
+
-- Fresh branch: boards(insert) -> board_memberships(insert, FK KEY SHARE on agents) ->
|
|
294
|
+
-- agents(SHARE). Collision branch: boards(FOR UPDATE) -> agents(SHARE). Both are
|
|
295
|
+
-- boards-before-agents, the canonical order. HOISTED above the slug read it would become
|
|
296
|
+
-- agents(SHARE) -> boards(FOR UPDATE) on the collision branch, which is #269 F4's exact ABBA
|
|
297
|
+
-- against onboard_federated_fleet_agent's boards -> agents. One transaction, so this raise
|
|
298
|
+
-- rolls back the board, the owner seat and the owner_seed event alike -- position costs
|
|
299
|
+
-- nothing in correctness and everything in deadlock safety.
|
|
300
|
+
--
|
|
301
|
+
-- On the fresh branch the FK on the seat insert has ALREADY taken KEY SHARE on this row, so
|
|
302
|
+
-- the FOR SHARE is a free upgrade and is NOT what serializes that path (see the header). On
|
|
303
|
+
-- the collision branch it is the only agents lock there is, and it is the serialization point.
|
|
304
|
+
perform agent_board._assert_actor_not_fleet_revoked(v_actor, 'create_board');
|
|
305
|
+
|
|
306
|
+
return query
|
|
307
|
+
select v_board.id, v_board.slug, v_board.name, v_board.status, v_board.visibility, v_board.created_at, v_board.updated_at;
|
|
308
|
+
end;
|
|
309
|
+
$$;
|
|
310
|
+
|
|
311
|
+
comment on function agent_board.create_board(uuid, text, text, text, jsonb, text) is
|
|
312
|
+
'Creates a board and seeds the resolved actor''s owner membership; on a slug COLLISION it '
|
|
313
|
+
'instead renames/reopens/re-visibilities the existing board, but only for an actor holding an '
|
|
314
|
+
'ACTIVE owner-role membership on it (else 42501 ''board slug already in use''), and it never '
|
|
315
|
+
're-seeds an owner. Actor is _resolved_actor_agent_id: service_role uses the supplied arg '
|
|
316
|
+
'(create-on-behalf-of), authenticated uses current_agent_id() and fails closed on mismatch. '
|
|
317
|
+
'#280: the resolved actor is then checked against the fleet revocation ledger by '
|
|
318
|
+
'_assert_actor_not_fleet_revoked -- an agent carrying an agent-subject fleet_revocation_events '
|
|
319
|
+
'row cannot create a board at all (42501, ''fleet_revoked:''), the same answer '
|
|
320
|
+
'evaluate_fleet_sender_policy already gives it on the send path and #276 gave it on the five '
|
|
321
|
+
'authority routes. This CLOSES the residual #276 named and left open. The gate covers BOTH '
|
|
322
|
+
'branches by design, so a revoked owner also cannot reopen or re-visibility an existing board. '
|
|
323
|
+
'It is placed AFTER the writes so the lock order stays boards -> agents (fresh branch: '
|
|
324
|
+
'boards -> board_memberships -> agents); hoisting it would give agents -> boards on the '
|
|
325
|
+
'collision branch and invert against onboard_federated_fleet_agent, the ABBA #269 reproduced. '
|
|
326
|
+
'The function is one transaction, so the raise rolls back the board, the owner seat and the '
|
|
327
|
+
'owner_seed event. A principal with no fleet binding has no ledger row and is unaffected.';
|
|
328
|
+
|
|
329
|
+
revoke all on function agent_board.create_board(uuid, text, text, text, jsonb, text) from public, anon;
|
|
330
|
+
grant execute on function agent_board.create_board(uuid, text, text, text, jsonb, text) to authenticated, service_role;
|