@consiliency/agent-board-schema 1.58.0 → 1.59.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consiliency/agent-board-schema",
3
- "version": "1.58.0",
3
+ "version": "1.59.0",
4
4
  "description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
5
5
  "repository": "https://github.com/Consiliency/message-board.git",
6
6
  "type": "module",
@@ -2,7 +2,7 @@
2
2
  "schema": "agent_board_schema_manifest.v1",
3
3
  "interface": "IF-0-MBPLANE-BUNDLE-1",
4
4
  "package": "@consiliency/agent-board-schema",
5
- "packageVersion": "1.58.0",
5
+ "packageVersion": "1.59.0",
6
6
  "schemaVersion": 8,
7
7
  "minClientVersion": "1.1.0",
8
8
  "source": {
@@ -46,8 +46,8 @@
46
46
  "usage": "upsert (retention/monitoring defaults; on conflict do nothing)"
47
47
  }
48
48
  ],
49
- "migrationCount": 156,
50
- "aggregateSha256": "2ad57215f17554d6ceb6ec1b7987f0e705cbc1c24e0411214be93dc2a2018315",
49
+ "migrationCount": 157,
50
+ "aggregateSha256": "481b368359a456111c6a2f901ceba486f32085ee8d48c36aefcfb592c0f1070f",
51
51
  "migrations": [
52
52
  {
53
53
  "order": 0,
@@ -1452,6 +1452,15 @@
1452
1452
  "sourceRef": "message-board",
1453
1453
  "bytes": 166072,
1454
1454
  "sha256": "ef57647e29b8afc788dc8c481a0165b10a3c448aae12b8901901b653216bc3a8"
1455
+ },
1456
+ {
1457
+ "order": 156,
1458
+ "id": "20261009000100_agent_board_hosted_followups",
1459
+ "filename": "20261009000100_agent_board_hosted_followups.sql",
1460
+ "origin": "message-board-forward",
1461
+ "sourceRef": "message-board",
1462
+ "bytes": 37430,
1463
+ "sha256": "4c2c4041bb41126b3e1bce940bc8a52ac78b69389a0c866556ba0c0e4adc3564"
1455
1464
  }
1456
1465
  ],
1457
1466
  "excludedBridge": {
@@ -38,9 +38,9 @@ describe('manifest integrity', () => {
38
38
  expect(manifest.schemaVersion).toBe(8);
39
39
  expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
40
40
  });
41
- it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 156)', () => {
41
+ it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 157)', () => {
42
42
  expect(manifest.migrationCount).toBe(manifest.migrations.length);
43
- expect(manifest.migrationCount).toBe(156);
43
+ expect(manifest.migrationCount).toBe(157);
44
44
  // MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
45
45
  // schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
46
46
  // enrollment-only additive surface (identity_providers registry, enrollment audit,
@@ -349,6 +349,10 @@ describe('manifest integrity', () => {
349
349
  // HOSTED (v11): the agent_board_delegate role, delegate instances, the exchange and
350
350
  // tombstone RPCs, add_delegate_to_board, the D14 read predicates and the D16 refusals.
351
351
  '20261008000100_agent_board_hosted_delegates.sql',
352
+ // HOSTED follow-ups (#591): the owner's verified (iss, sub) on each delegate instance and the
353
+ // revocation marker revoke_human_subtree derives from it; the tombstone's and the exchange's
354
+ // lock order against the subtree revokers (AB15).
355
+ '20261009000100_agent_board_hosted_followups.sql',
352
356
  ]) {
353
357
  expect(names.has(f)).toBe(true);
354
358
  }
@@ -21,7 +21,7 @@ set search_path = extensions, public, agent_board;
21
21
 
22
22
  begin;
23
23
 
24
- select plan(129);
24
+ select plan(143);
25
25
 
26
26
  -- The test role switches to agent_board_delegate. On PostgreSQL 16+ the role's creator holds
27
27
  -- ADMIN OPTION but not SET, so grant SET to the current role for this transaction only (rolled
@@ -1105,9 +1105,11 @@ select is(
1105
1105
  'N2: a suspended owner cannot bring up a new delegate instance, and nothing is written');
1106
1106
 
1107
1107
  -- Owner E: never enrolled as a human on this board (no human row), then durably revoked. The
1108
- -- marker is SEEDED: revoke_human_subtree writes it only from a federated enrollment's audit row
1109
- -- of an agent the human owns (a keyed agent via enroll_federated_identity), which this file does
1110
- -- not build. The row is the shape revoke_human_subtree writes, keyed by the verified (iss, sub).
1108
+ -- marker is SEEDED here, by hand, so that E's delegate row stays active and 128 isolates the
1109
+ -- marker arm alone (revoke_human_subtree would also revoke the delegate). The row is the shape
1110
+ -- revoke_human_subtree writes, keyed by the verified (iss, sub). The real revocation of a
1111
+ -- delegate-only owner, which now writes the marker from the instance's recorded (iss, sub)
1112
+ -- (#591 item 4, AB15), is section 24.
1111
1113
  -- 127 -- control: an owner with no human row is allowed, as before
1112
1114
  select is(
1113
1115
  pg_temp.exch(pg_temp.claim('inst_eeeeeeeeeeeeeeee_01', 'grant_eeeeeeeeeeeeee_01_1', 1, 'hosted-owner-e', 'client-e'), 'E1'),
@@ -1130,5 +1132,147 @@ select is(
1130
1132
  '42501:owner_inactive 0 0 0',
1131
1133
  'N2: the revocation marker alone refuses a new instance, and nothing is written');
1132
1134
 
1135
+ -- ===========================================================================
1136
+ -- 24. #591 item 4 -- the owner's (iss, sub) on each instance, and the marker it gives
1137
+ -- revoke_human_subtree (AB15); #591 item 9 -- owner_inactive before instance_revoked
1138
+ -- ===========================================================================
1139
+ -- An owner whose only principals are hosted delegates has no enrollment audit row, so before
1140
+ -- 1.59.0 revoke_human_subtree wrote it no marker, and a fresh first exchange passed. Both RPCs now
1141
+ -- record the verified pair, and the revocation derives a marker from it.
1142
+ -- plpgsql, not sql: each read runs as its own query, so it sees what an action earlier in the
1143
+ -- same statement wrote (the reason pg_temp.q exists).
1144
+ create function pg_temp.pair(p_iid text) returns text language plpgsql as $$
1145
+ begin
1146
+ return coalesce((select coalesce(owner_issuer, '(null)') || ' ' || coalesce(owner_subject, '(null)')
1147
+ from agent_board.delegate_instances where instance_id = p_iid), '(no row)');
1148
+ end $$;
1149
+ create function pg_temp.owner_of(p_iid text) returns uuid language plpgsql as $$
1150
+ begin
1151
+ return (select owner_user_id from agent_board.delegate_instances where instance_id = p_iid);
1152
+ end $$;
1153
+ create function pg_temp.markers(p_sub text) returns text language plpgsql as $$
1154
+ begin
1155
+ return (select count(*)::text || coalesce(' ' || string_agg(r.owner_user_id::text, ','), '')
1156
+ from agent_board.fleet_human_revocations r
1157
+ where r.issuer = 'https://portal.hosted.test' and r.subject = p_sub);
1158
+ end $$;
1159
+ -- 130 -- the exchange records the pair (D1 was exchanged in 123)
1160
+ select is(pg_temp.pair('inst_dddddddddddddddd_01'), 'https://portal.hosted.test hosted-owner-d',
1161
+ 'AB15: the exchange records the owner''s verified (iss, sub) on the instance');
1162
+ -- 131 -- a revocation before the first exchange records it too (owner H has nothing else)
1163
+ select is(
1164
+ pg_temp.tomb('inst_hhhhhhhhhhhhhhhh_01', 'hosted-owner-h') || ' ' || pg_temp.pair('inst_hhhhhhhhhhhhhhhh_01'),
1165
+ 'first=true https://portal.hosted.test hosted-owner-h',
1166
+ 'AB15: a pre-exchange tombstone records the owner''s verified (iss, sub) on the row it creates');
1167
+ -- 132 -- once recorded, the pair never changes
1168
+ select is(
1169
+ pg_temp.try($q$update agent_board.delegate_instances set owner_subject = 'someone-else' where instance_id = 'inst_dddddddddddddddd_01'$q$)
1170
+ || ' ' || pg_temp.pair('inst_dddddddddddddddd_01'),
1171
+ '23514:delegate_instances owner identity never changes once recorded https://portal.hosted.test hosted-owner-d',
1172
+ 'AB15: the recorded owner pair is immutable');
1173
+
1174
+ -- Owner G: never enrolled, live; the control owner, and the legacy-row fills.
1175
+ select pg_temp.svc();
1176
+ -- 133 -- a LEGACY row (pre-1.59.0 shape: no pair) is filled by its first exchange
1177
+ select is(
1178
+ pg_temp.exch(pg_temp.claim('inst_gggggggggggggggg_01', 'grant_gggggggggggggg_01_1', 1, 'hosted-owner-g', 'client-g'), 'G1')
1179
+ || ' ' || pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id) values ('inst_gggggggggggggggg_l1', %L), ('inst_gggggggggggggggg_l2', %L)$q$,
1180
+ pg_temp.owner_of('inst_gggggggggggggggg_01'), pg_temp.owner_of('inst_gggggggggggggggg_01')))
1181
+ || ' ' || pg_temp.pair('inst_gggggggggggggggg_l1')
1182
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_gggggggggggggggg_l1', 'grant_gggggggggggggg_l1_1', 1, 'hosted-owner-g', 'client-g'), 'GL1')
1183
+ || ' ' || pg_temp.pair('inst_gggggggggggggggg_l1'),
1184
+ 'ok ok (null) (null) ok https://portal.hosted.test hosted-owner-g',
1185
+ 'AB15: a legacy instance with no recorded pair is filled by its next exchange');
1186
+ -- 134 -- ...and by a tombstone
1187
+ select is(
1188
+ pg_temp.tomb('inst_gggggggggggggggg_l2', 'hosted-owner-g') || ' ' || pg_temp.pair('inst_gggggggggggggggg_l2'),
1189
+ 'first=true https://portal.hosted.test hosted-owner-g',
1190
+ 'AB15: a legacy instance with no recorded pair is filled by its tombstone');
1191
+
1192
+ -- Owner F: never enrolled as a human; its ONLY principal is a hosted delegate.
1193
+ -- 135 -- revoke_human_subtree for real, as service_role
1194
+ select is(
1195
+ pg_temp.exch(pg_temp.claim('inst_ffffffffffffffff_01', 'grant_ffffffffffffff_01_1', 1, 'hosted-owner-f', 'client-f'), 'F1')
1196
+ || ' ' || pg_temp.try(format($q$select agent_board.revoke_human_subtree(%L, %L, 'hosted 591 test')$q$,
1197
+ pg_temp.owner_of('inst_ffffffffffffffff_01'), pg_temp.v('K')))
1198
+ || ' ' || pg_temp.q(format($q$select status from agent_board.agents where id = %L$q$, pg_temp.v('F1.agent'))),
1199
+ 'ok ok revoked',
1200
+ '#591 item 4: an owner with only a hosted delegate is revoked, and its delegate with it');
1201
+ -- 136 -- the marker exists, keyed by the verified pair, naming the owner
1202
+ select is(pg_temp.markers('hosted-owner-f'), '1 ' || pg_temp.owner_of('inst_ffffffffffffffff_01')::text,
1203
+ '#591 item 4: revoking a delegate-only owner writes the durable (iss, sub) marker from the instance''s recorded pair');
1204
+ -- 137 -- so a fresh first exchange is refused, and writes nothing
1205
+ select is(
1206
+ pg_temp.exch(pg_temp.claim('inst_ffffffffffffffff_02', 'grant_ffffffffffffff_02_1', 1, 'hosted-owner-f', 'client-f'), 'x')
1207
+ || ' ' || pg_temp.n2_rows('inst_ffffffffffffffff_02'),
1208
+ '42501:owner_inactive 0 0 0',
1209
+ '#591 item 4: after the revocation a new first exchange for that owner is refused owner_inactive, and nothing is written');
1210
+ -- 138 -- control: another live owner, never enrolled, still brings up a new instance
1211
+ select is(
1212
+ pg_temp.exch(pg_temp.claim('inst_gggggggggggggggg_02', 'grant_gggggggggggggg_02_1', 1, 'hosted-owner-g', 'client-g'), 'G2')
1213
+ || ' ' || pg_temp.markers('hosted-owner-g'),
1214
+ 'ok 0',
1215
+ '#591 item 4 control: a different, unrevoked owner''s first exchange succeeds and has no marker');
1216
+ -- 139 -- #591 item 9: a revoked owner AND a tombstoned instance -> owner_inactive, not instance_revoked
1217
+ select is(
1218
+ pg_temp.tomb('inst_ffffffffffffffff_01', 'hosted-owner-f')
1219
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_ffffffffffffffff_01', 'grant_ffffffffffffff_01_2', 2, 'hosted-owner-f', 'client-f'), 'x'),
1220
+ 'first=true 42501:owner_inactive',
1221
+ 'AB14 precedence: the owner check runs before the instance''s own refusals -- a tombstoned instance of a revoked owner is refused owner_inactive, not instance_revoked');
1222
+ -- 140 -- owner H, whose only row is a pre-exchange tombstone (131): its revocation marks it too
1223
+ select is(
1224
+ pg_temp.try(format($q$select agent_board.revoke_human_subtree(%L, %L, 'hosted 591 test')$q$,
1225
+ pg_temp.owner_of('inst_hhhhhhhhhhhhhhhh_01'), pg_temp.v('K')))
1226
+ || ' ' || pg_temp.markers('hosted-owner-h')
1227
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_hhhhhhhhhhhhhhhh_02', 'grant_hhhhhhhhhhhhhh_02_1', 1, 'hosted-owner-h', 'client-h'), 'x')
1228
+ || ' ' || pg_temp.n2_rows('inst_hhhhhhhhhhhhhhhh_02'),
1229
+ 'ok 1 ' || pg_temp.owner_of('inst_hhhhhhhhhhhhhhhh_01')::text || ' 42501:owner_inactive 0 0 0',
1230
+ '#591 item 4: an owner whose only row is a pre-exchange tombstone is marked by its revocation, and a new instance is refused');
1231
+ -- 141 -- the class: every function that WRITES delegate_instances (any INSERT, UPDATE, MERGE or
1232
+ -- DELETE, from the catalog, through the shared matcher family) is one of the two RPCs that record
1233
+ -- the pair, and every row this file created carries one except the two hand-inserted legacy rows,
1234
+ -- which their exchange and tombstone filled (133, 134). A new writer fails this and names itself.
1235
+ -- Every non-system schema, not only agent_board: a writer in public or extensions bypasses the pair
1236
+ -- just as well (mutation-checked with one in public). agent_board_test is excluded because it holds
1237
+ -- this suite's matcher functions, which carry relation names as data and write nothing.
1238
+ select is(
1239
+ (select string_agg(p.proname, ',' order by p.proname)
1240
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
1241
+ where n.nspname not in ('pg_catalog', 'information_schema', 'agent_board_test')
1242
+ and n.nspname !~ '^pg_'
1243
+ and p.prokind in ('f', 'p')
1244
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.write_re('delegate_instances'))
1245
+ || ' ' || (select count(*) from agent_board.delegate_instances where owner_issuer is null)::text,
1246
+ 'exchange_delegate_credential,tombstone_delegate_instance 0',
1247
+ 'AB15: exactly the exchange and the tombstone write delegate_instances (catalog-derived), and no row is left without the owner pair');
1248
+
1249
+ -- 142 -- the pair is both-or-neither and never empty (delegate_instances_owner_identity_shape)
1250
+ select is(
1251
+ pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id, owner_issuer) values ('inst_gggggggggggggggg_s1', %L, 'https://portal.hosted.test')$q$, pg_temp.owner_of('inst_gggggggggggggggg_01')))
1252
+ || ' ' || pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id, owner_issuer, owner_subject) values ('inst_gggggggggggggggg_s2', %L, 'https://portal.hosted.test', '')$q$, pg_temp.owner_of('inst_gggggggggggggggg_01'))),
1253
+ '23514:new row for relation "delegate_instances" violates check constraint "delegate_instances_owner_identity_shape" 23514:new row for relation "delegate_instances" violates check constraint "delegate_instances_owner_identity_shape"',
1254
+ 'AB15: an owner pair is recorded whole or not at all, and never empty');
1255
+
1256
+ -- 143 -- a LEGACY TOMBSTONED row (pre-1.59.0: tombstoned, no pair) is filled by a repeated
1257
+ -- revocation. Nothing else ever writes such a row again: the exchange refuses it
1258
+ -- (instance_revoked) before its UPDATE, and a tombstone used to return early. Owner J has only this
1259
+ -- row (never enrolled, never exchanged), so the pair is the only evidence revoke_human_subtree can
1260
+ -- mark it from. Then a new first exchange of J is refused owner_inactive and writes nothing.
1261
+ select pg_temp.put('J.owner', agent_board._ensure_agent_auth_user(
1262
+ agent_board._federated_identity_slug('https://portal.hosted.test', 'hosted-owner-j'))::text);
1263
+ select is(
1264
+ pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id, tombstoned_at, tombstone_reason) values ('inst_jjjjjjjjjjjjjjjj_l1', %L, now(), 'owner_revoked')$q$, pg_temp.v('J.owner')))
1265
+ || ' ' || pg_temp.pair('inst_jjjjjjjjjjjjjjjj_l1')
1266
+ || ' ' || pg_temp.tomb('inst_jjjjjjjjjjjjjjjj_l1', 'hosted-owner-j')
1267
+ || ' ' || pg_temp.pair('inst_jjjjjjjjjjjjjjjj_l1')
1268
+ || ' ' || pg_temp.try(format($q$select agent_board.revoke_human_subtree(%L, %L, 'hosted 591 test')$q$,
1269
+ pg_temp.v('J.owner'), pg_temp.v('K')))
1270
+ || ' ' || pg_temp.markers('hosted-owner-j')
1271
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_jjjjjjjjjjjjjjjj_02', 'grant_jjjjjjjjjjjjjj_02_1', 1, 'hosted-owner-j', 'client-j'), 'x')
1272
+ || ' ' || pg_temp.n2_rows('inst_jjjjjjjjjjjjjjjj_02'),
1273
+ 'ok (null) (null) first=false https://portal.hosted.test hosted-owner-j ok 1 ' || pg_temp.v('J.owner')
1274
+ || ' 42501:owner_inactive 0 0 0',
1275
+ 'AB15: a legacy tombstoned row is filled by a repeated revocation, so revoking its delegate-only owner marks it and a new instance is refused');
1276
+
1133
1277
  select * from finish();
1134
1278
  rollback;
@@ -343,6 +343,15 @@
343
343
  "authenticated": false
344
344
  }
345
345
  },
346
+ {
347
+ "name": "_delegate_instances_owner_identity_immutable",
348
+ "signature": "_delegate_instances_owner_identity_immutable()",
349
+ "executable_by": {
350
+ "anon": false,
351
+ "service_role": false,
352
+ "authenticated": false
353
+ }
354
+ },
346
355
  {
347
356
  "name": "_delegate_principal_agent_id",
348
357
  "signature": "_delegate_principal_agent_id(p_agent_id uuid)",
@@ -153,6 +153,10 @@
153
153
  "signature": "_delegate_instance_slug(p_issuer text, p_instance_id text)",
154
154
  "declares_p_metadata": false
155
155
  },
156
+ {
157
+ "signature": "_delegate_instances_owner_identity_immutable()",
158
+ "declares_p_metadata": false
159
+ },
156
160
  {
157
161
  "signature": "_delegate_principal_agent_id(p_agent_id uuid)",
158
162
  "declares_p_metadata": false
@@ -0,0 +1,105 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: o_begin o_add t_begin t_tomb o_commit t_commit check
4
+ step o_begin:
5
+ BEGIN;
6
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
7
+ (select auth_user_id from agent_board.agents
8
+ where handle = agent_board._federated_identity_slug('https://iso-at.test', 'iso-at-owner')))::text, true) is not null as owner_session;
9
+
10
+ owner_session
11
+ -------------
12
+ t
13
+ (1 row)
14
+
15
+ step o_add:
16
+ select membership_state from agent_board.add_delegate_to_board(
17
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_at_instance_00001'),
18
+ '16591300-0000-4000-8000-000000000001');
19
+
20
+ membership_state
21
+ ----------------
22
+ joined
23
+ (1 row)
24
+
25
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
26
+ step t_tomb:
27
+ select first from agent_board.tombstone_delegate_instance(
28
+ 'iso_at_instance_00001', 'https://iso-at.test', 'iso-at-owner', 'owner_revoked');
29
+ <waiting ...>
30
+ step o_commit: COMMIT;
31
+ step t_tomb: <... completed>
32
+ first
33
+ -----
34
+ t
35
+ (1 row)
36
+
37
+ step t_commit: COMMIT;
38
+ step check:
39
+ select (select (di.tombstoned_at is not null)::text from agent_board.delegate_instances di
40
+ where di.instance_id = 'iso_at_instance_00001') as tombstoned,
41
+ (select a.status from agent_board.agents a
42
+ join agent_board.delegate_instances di on di.agent_id = a.id
43
+ where di.instance_id = 'iso_at_instance_00001') as delegate_status,
44
+ (select string_agg(bm.membership_state, ',' order by bm.created_at, bm.id) from agent_board.board_memberships bm
45
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
46
+ where di.instance_id = 'iso_at_instance_00001') as delegate_memberships,
47
+ (select count(*) from agent_board.board_memberships bm
48
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
49
+ where di.tombstoned_at is not null and bm.membership_state in ('joined', 'subscribed')) as tombstoned_and_active;
50
+
51
+ tombstoned|delegate_status|delegate_memberships|tombstoned_and_active
52
+ ----------+---------------+--------------------+---------------------
53
+ true |revoked |left | 0
54
+ (1 row)
55
+
56
+
57
+ starting permutation: t_begin t_tomb o_begin o_add t_commit o_commit check
58
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
59
+ step t_tomb:
60
+ select first from agent_board.tombstone_delegate_instance(
61
+ 'iso_at_instance_00001', 'https://iso-at.test', 'iso-at-owner', 'owner_revoked');
62
+
63
+ first
64
+ -----
65
+ t
66
+ (1 row)
67
+
68
+ step o_begin:
69
+ BEGIN;
70
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
71
+ (select auth_user_id from agent_board.agents
72
+ where handle = agent_board._federated_identity_slug('https://iso-at.test', 'iso-at-owner')))::text, true) is not null as owner_session;
73
+
74
+ owner_session
75
+ -------------
76
+ t
77
+ (1 row)
78
+
79
+ step o_add:
80
+ select membership_state from agent_board.add_delegate_to_board(
81
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_at_instance_00001'),
82
+ '16591300-0000-4000-8000-000000000001');
83
+ <waiting ...>
84
+ step t_commit: COMMIT;
85
+ step o_add: <... completed>
86
+ ERROR: the hosted delegate instance is not live
87
+ step o_commit: COMMIT;
88
+ step check:
89
+ select (select (di.tombstoned_at is not null)::text from agent_board.delegate_instances di
90
+ where di.instance_id = 'iso_at_instance_00001') as tombstoned,
91
+ (select a.status from agent_board.agents a
92
+ join agent_board.delegate_instances di on di.agent_id = a.id
93
+ where di.instance_id = 'iso_at_instance_00001') as delegate_status,
94
+ (select string_agg(bm.membership_state, ',' order by bm.created_at, bm.id) from agent_board.board_memberships bm
95
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
96
+ where di.instance_id = 'iso_at_instance_00001') as delegate_memberships,
97
+ (select count(*) from agent_board.board_memberships bm
98
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
99
+ where di.tombstoned_at is not null and bm.membership_state in ('joined', 'subscribed')) as tombstoned_and_active;
100
+
101
+ tombstoned|delegate_status|delegate_memberships|tombstoned_and_active
102
+ ----------+---------------+--------------------+---------------------
103
+ true |revoked | | 0
104
+ (1 row)
105
+
@@ -0,0 +1,66 @@
1
+ Parsed test spec with 4 sessions
2
+
3
+ starting permutation: h_begin x_begin x_gen2 r_begin r_agent h_commit x_commit r_commit check
4
+ count
5
+ -----
6
+ 1
7
+ (1 row)
8
+
9
+ step h_begin:
10
+ BEGIN;
11
+ select (ac.id is not null) as held
12
+ from agent_board.agent_credentials ac
13
+ join agent_board.delegate_instances di on di.agent_id = ac.agent_id
14
+ where di.instance_id = 'iso_xa_instance_00001'
15
+ order by ac.created_at desc, ac.id desc
16
+ limit 1
17
+ for update of ac;
18
+
19
+ held
20
+ ----
21
+ t
22
+ (1 row)
23
+
24
+ step x_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
25
+ step x_gen2:
26
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
27
+ 'iss', 'https://iso-xa.test', 'sub', 'iso-xa-owner', 'slot_resource', null,
28
+ 'agent_board_delegate_instance', jsonb_build_object(
29
+ 'instance_id', 'iso_xa_instance_00001', 'grant_id', 'iso_grant_0000000002', 'generation', 2,
30
+ 'owner_sub', 'iso-xa-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
31
+ <waiting ...>
32
+ step r_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
33
+ step r_agent:
34
+ select revoked_agent_count from agent_board.revoke_agent_subtree(
35
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_xa_instance_00001'),
36
+ '16591400-0000-4000-8000-000000000001', 'iso exchange revoke race');
37
+ <waiting ...>
38
+ step h_commit: COMMIT;
39
+ step x_gen2: <... completed>
40
+ issued
41
+ ------
42
+ 1
43
+ (1 row)
44
+
45
+ step x_commit: COMMIT;
46
+ step r_agent: <... completed>
47
+ revoked_agent_count
48
+ -------------------
49
+ 1
50
+ (1 row)
51
+
52
+ step r_commit: COMMIT;
53
+ step check:
54
+ select a.status as delegate_status,
55
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = a.id) as credentials,
56
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = a.id and ac.generation = 2) as generation_2,
57
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = a.id and ac.revoked_at is null) as live
58
+ from agent_board.agents a
59
+ join agent_board.delegate_instances di on di.agent_id = a.id
60
+ where di.instance_id = 'iso_xa_instance_00001';
61
+
62
+ delegate_status|credentials|generation_2|live
63
+ ---------------+-----------+------------+----
64
+ revoked | 3| 1| 0
65
+ (1 row)
66
+
@@ -0,0 +1,170 @@
1
+ Parsed test spec with 4 sessions
2
+
3
+ starting permutation: h_begin t_begin t_tomb r_begin r_human h_commit t_commit r_commit check
4
+ bare_owner
5
+ ----------
6
+ t
7
+ (1 row)
8
+
9
+ step h_begin:
10
+ BEGIN;
11
+ select (bm.id is not null) as held from agent_board.board_memberships bm
12
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
13
+ where di.instance_id = 'iso_tr_instance_00001' and bm.membership_state = 'joined'
14
+ for update of bm;
15
+
16
+ held
17
+ ----
18
+ t
19
+ (1 row)
20
+
21
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
22
+ step t_tomb:
23
+ select first from agent_board.tombstone_delegate_instance(
24
+ 'iso_tr_instance_00001', 'https://iso-tr.test', 'iso-tr-owner', 'owner_revoked');
25
+ <waiting ...>
26
+ step r_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
27
+ step r_human:
28
+ select revoked_agent_count from agent_board.revoke_human_subtree(
29
+ (select auth_user_id from agent_board.agents
30
+ where handle = agent_board._federated_identity_slug('https://iso-tr.test', 'iso-tr-owner')),
31
+ '16591000-0000-4000-8000-000000000001', 'iso tombstone revoke race');
32
+ <waiting ...>
33
+ step h_commit: COMMIT;
34
+ step t_tomb: <... completed>
35
+ first
36
+ -----
37
+ t
38
+ (1 row)
39
+
40
+ step t_commit: COMMIT;
41
+ step r_human: <... completed>
42
+ revoked_agent_count
43
+ -------------------
44
+ 2
45
+ (1 row)
46
+
47
+ step r_commit: COMMIT;
48
+ step check:
49
+ select (select (di.tombstoned_at is not null)::text from agent_board.delegate_instances di
50
+ where di.instance_id = 'iso_tr_instance_00001') as tombstoned,
51
+ (select a.status from agent_board.agents a
52
+ join agent_board.delegate_instances di on di.agent_id = a.id
53
+ where di.instance_id = 'iso_tr_instance_00001') as delegate_status,
54
+ (select count(*) from agent_board.agent_credentials ac
55
+ join agent_board.delegate_instances di on di.agent_id = ac.agent_id
56
+ where di.instance_id = 'iso_tr_instance_00001' and ac.revoked_at is null) as live_credentials,
57
+ (select count(*) from agent_board.board_memberships bm
58
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
59
+ where di.instance_id = 'iso_tr_instance_00001' and bm.membership_state in ('joined', 'subscribed')) as active_memberships;
60
+
61
+ tombstoned|delegate_status|live_credentials|active_memberships
62
+ ----------+---------------+----------------+------------------
63
+ true |revoked | 0| 0
64
+ (1 row)
65
+
66
+
67
+ starting permutation: h_begin t_begin t_tomb r_begin r_agent h_commit t_commit r_commit check
68
+ bare_owner
69
+ ----------
70
+ t
71
+ (1 row)
72
+
73
+ step h_begin:
74
+ BEGIN;
75
+ select (bm.id is not null) as held from agent_board.board_memberships bm
76
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
77
+ where di.instance_id = 'iso_tr_instance_00001' and bm.membership_state = 'joined'
78
+ for update of bm;
79
+
80
+ held
81
+ ----
82
+ t
83
+ (1 row)
84
+
85
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
86
+ step t_tomb:
87
+ select first from agent_board.tombstone_delegate_instance(
88
+ 'iso_tr_instance_00001', 'https://iso-tr.test', 'iso-tr-owner', 'owner_revoked');
89
+ <waiting ...>
90
+ step r_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
91
+ step r_agent:
92
+ select revoked_agent_count from agent_board.revoke_agent_subtree(
93
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_tr_instance_00001'),
94
+ '16591000-0000-4000-8000-000000000001', 'iso tombstone revoke race');
95
+ <waiting ...>
96
+ step h_commit: COMMIT;
97
+ step t_tomb: <... completed>
98
+ first
99
+ -----
100
+ t
101
+ (1 row)
102
+
103
+ step t_commit: COMMIT;
104
+ step r_agent: <... completed>
105
+ revoked_agent_count
106
+ -------------------
107
+ 1
108
+ (1 row)
109
+
110
+ step r_commit: COMMIT;
111
+ step check:
112
+ select (select (di.tombstoned_at is not null)::text from agent_board.delegate_instances di
113
+ where di.instance_id = 'iso_tr_instance_00001') as tombstoned,
114
+ (select a.status from agent_board.agents a
115
+ join agent_board.delegate_instances di on di.agent_id = a.id
116
+ where di.instance_id = 'iso_tr_instance_00001') as delegate_status,
117
+ (select count(*) from agent_board.agent_credentials ac
118
+ join agent_board.delegate_instances di on di.agent_id = ac.agent_id
119
+ where di.instance_id = 'iso_tr_instance_00001' and ac.revoked_at is null) as live_credentials,
120
+ (select count(*) from agent_board.board_memberships bm
121
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
122
+ where di.instance_id = 'iso_tr_instance_00001' and bm.membership_state in ('joined', 'subscribed')) as active_memberships;
123
+
124
+ tombstoned|delegate_status|live_credentials|active_memberships
125
+ ----------+---------------+----------------+------------------
126
+ true |revoked | 0| 0
127
+ (1 row)
128
+
129
+
130
+ starting permutation: t_begin t_tomb_bare r_begin r_human_bare t_commit r_commit check_bare
131
+ bare_owner
132
+ ----------
133
+ t
134
+ (1 row)
135
+
136
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
137
+ step t_tomb_bare:
138
+ select first from agent_board.tombstone_delegate_instance(
139
+ 'iso_tr_bare_instance_01', 'https://iso-tr.test', 'iso-tr-bare', 'owner_revoked');
140
+
141
+ first
142
+ -----
143
+ t
144
+ (1 row)
145
+
146
+ step r_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
147
+ step r_human_bare:
148
+ select revoked_agent_count from agent_board.revoke_human_subtree(
149
+ (select id from auth.users
150
+ where email = 'agent+' || agent_board._federated_identity_slug('https://iso-tr.test', 'iso-tr-bare') || '@agent-board.local'),
151
+ '16591000-0000-4000-8000-000000000001', 'iso tombstone revoke race');
152
+
153
+ revoked_agent_count
154
+ -------------------
155
+ 0
156
+ (1 row)
157
+
158
+ step t_commit: COMMIT;
159
+ step r_commit: COMMIT;
160
+ step check_bare:
161
+ select (select count(*) from agent_board.delegate_instances where instance_id = 'iso_tr_bare_instance_01'
162
+ and tombstoned_at is not null and owner_subject = 'iso-tr-bare') as tombstoned_recorded,
163
+ (select count(*) from agent_board.fleet_human_revocations
164
+ where issuer = 'https://iso-tr.test' and subject = 'iso-tr-bare') as markers;
165
+
166
+ tombstoned_recorded|markers
167
+ -------------------+-------
168
+ 1| 0
169
+ (1 row)
170
+