@consiliency/agent-board-schema 1.2.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.
Files changed (92) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +11 -0
  3. package/README.md +55 -0
  4. package/bin/agent-board-schema.js +97 -0
  5. package/fixtures/already-applied.json +181 -0
  6. package/fixtures/divergent-history.json +173 -0
  7. package/fixtures/production-history.json +173 -0
  8. package/migrations/20260501000001_agent_board_schema_version.sql +12 -0
  9. package/migrations/20260502000000_agent_board_schema.sql +180 -0
  10. package/migrations/20260503000000_agent_board_rpcs.sql +422 -0
  11. package/migrations/20260503000001_agent_board_coordination_rpcs.sql +229 -0
  12. package/migrations/20260504000000_agent_board_read_models.sql +206 -0
  13. package/migrations/20260505000000_agent_board_inbox_schema.sql +341 -0
  14. package/migrations/20260506000000_agent_board_inbox_delivery_rpcs.sql +1200 -0
  15. package/migrations/20260506000001_agent_board_inbox_callback_rpcs.sql +330 -0
  16. package/migrations/20260507000000_agent_board_inbox_reads.sql +334 -0
  17. package/migrations/20260508000000_agent_board_callback_runtime.sql +453 -0
  18. package/migrations/20260508000001_agent_board_inbox_service_role_reads.sql +10 -0
  19. package/migrations/20260510000000_agent_board_dblint_repairs.sql +413 -0
  20. package/migrations/20260511000000_agent_board_auth_schema.sql +146 -0
  21. package/migrations/20260511000001_agent_board_auth_credentials.sql +39 -0
  22. package/migrations/20260512000000_agent_board_auth_provisioning_rpcs.sql +622 -0
  23. package/migrations/20260512000001_agent_board_auth_v1_rpcs.sql +798 -0
  24. package/migrations/20260512000002_agent_board_auth_v2_rpcs.sql +1243 -0
  25. package/migrations/20260512000003_agent_board_auth_rls.sql +484 -0
  26. package/migrations/20260513000000_agent_board_backup_ops.sql +34 -0
  27. package/migrations/20260513000001_agent_board_retention.sql +160 -0
  28. package/migrations/20260513000002_agent_board_monitoring_views.sql +227 -0
  29. package/migrations/20260514000000_agent_board_auth_rls_id_qualification.sql +150 -0
  30. package/migrations/20260515000000_agent_board_boardopt.sql +493 -0
  31. package/migrations/20260516000000_agent_board_callbackcap.sql +113 -0
  32. package/migrations/20260517000000_agent_board_addressing.sql +542 -0
  33. package/migrations/20260628000000_agent_board_signing_store.sql +174 -0
  34. package/migrations/20260628000001_agent_board_signing_proof_rpcs.sql +215 -0
  35. package/migrations/20260628000002_agent_board_signature_rls_reads.sql +300 -0
  36. package/migrations/20260628000003_agent_board_signed_write_gateway.sql +288 -0
  37. package/migrations/20260628000004_agent_board_fleet_registry.sql +411 -0
  38. package/migrations/20260628000005_agent_board_fleet_policy.sql +509 -0
  39. package/migrations/20260628000006_agent_board_fleet_read_models.sql +170 -0
  40. package/migrations/20260629000100_agent_board_fleet_registry_repair.sql +411 -0
  41. package/migrations/20260629000101_agent_board_fleet_policy_refresh.sql +509 -0
  42. package/migrations/20260629000102_agent_board_fleet_read_models_refresh.sql +170 -0
  43. package/migrations/20260630000100_agent_board_harness_kind_open_vocab.sql +15 -0
  44. package/migrations/20260710000000_agent_board_authbound_boundary.sql +155 -0
  45. package/migrations/20260710000100_agent_board_schema_version_rpc.sql +14 -0
  46. package/migrations/20260710000400_agent_board_nonfleet_identity.sql +87 -0
  47. package/migrations/20260710000401_agent_board_workload_providers.sql +39 -0
  48. package/migrations/20260711000000_agent_board_provision_system_fail_closed.sql +242 -0
  49. package/migrations/20260711000100_canary_signed_fleet_enforcement.sql +234 -0
  50. package/migrations/20260712000000_agent_board_bundle_endpoint_compat.sql +97 -0
  51. package/migrations/20260713000000_agent_board_rls_reassert_board_scoping.sql +90 -0
  52. package/package.json +42 -0
  53. package/provenance.json +580 -0
  54. package/schema-manifest.json +458 -0
  55. package/src/check.js +23 -0
  56. package/src/index.js +50 -0
  57. package/src/materialize.js +73 -0
  58. package/src/plan.js +44 -0
  59. package/src/tooling.test.mjs +97 -0
  60. package/tests/agent_board_addressing.test.sql +166 -0
  61. package/tests/agent_board_auth_rls.test.sql +243 -0
  62. package/tests/agent_board_auth_rls_id_qualification.test.sql +225 -0
  63. package/tests/agent_board_auth_rpcs.test.sql +311 -0
  64. package/tests/agent_board_auth_schema.test.sql +411 -0
  65. package/tests/agent_board_authbound_boundary.test.sql +148 -0
  66. package/tests/agent_board_boardopt.test.sql +170 -0
  67. package/tests/agent_board_bundle_endpoint_compat.test.sql +49 -0
  68. package/tests/agent_board_callback_runtime.test.sql +425 -0
  69. package/tests/agent_board_callbackcap.test.sql +222 -0
  70. package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +60 -0
  71. package/tests/agent_board_dblint_rpcs.test.sql +247 -0
  72. package/tests/agent_board_fleet_policy.test.sql +370 -0
  73. package/tests/agent_board_fleet_read_models.test.sql +158 -0
  74. package/tests/agent_board_fleet_registry.test.sql +149 -0
  75. package/tests/agent_board_inbox_reads.test.sql +471 -0
  76. package/tests/agent_board_inbox_rpcs.test.sql +452 -0
  77. package/tests/agent_board_inbox_schema.test.sql +355 -0
  78. package/tests/agent_board_nonfleet_identity.test.sql +26 -0
  79. package/tests/agent_board_rls_reassert_board_scoping.test.sql +39 -0
  80. package/tests/agent_board_schema_version.test.sql +24 -0
  81. package/tests/agent_board_schema_version_rpc.test.sql +40 -0
  82. package/tests/agent_board_signature_rls_reads.test.sql +133 -0
  83. package/tests/agent_board_signed_write_gateway.test.sql +219 -0
  84. package/tests/agent_board_signing_proof_rpcs.test.sql +143 -0
  85. package/tests/agent_board_signing_store.test.sql +183 -0
  86. package/tests/agent_board_smoke.test.sql +12 -0
  87. package/tests/agent_board_v1_invariants.test.sql +298 -0
  88. package/tests/agent_board_v2_invariants.test.sql +753 -0
  89. package/tests/agent_board_workload_providers.test.sql +15 -0
  90. package/tests/fixtures/adopter-base.sql +19 -0
  91. package/tests/fixtures/operator-hardening.sql +27 -0
  92. package/tests/fixtures/test-prereqs.sql +84 -0
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { loadManifest, loadOrderedMigrations, sha256 } from './index.js';
6
+ import { materialize } from './materialize.js';
7
+ import { plan } from './plan.js';
8
+ import { check, EXIT } from './check.js';
9
+
10
+ const manifest = loadManifest();
11
+ const ordered = loadOrderedMigrations();
12
+ const tmps = [];
13
+ const mktmp = () => {
14
+ const d = mkdtempSync(join(tmpdir(), 'abs-test-'));
15
+ tmps.push(d);
16
+ return d;
17
+ };
18
+ afterEach(() => {
19
+ while (tmps.length) rmSync(tmps.pop(), { recursive: true, force: true });
20
+ });
21
+
22
+ const historyExact = () => ({ applied: ordered.map((m) => ({ id: m.id, sha256: m.sha256 })) });
23
+ const union = () => ordered.filter((m) => m.origin !== 'message-board-forward');
24
+
25
+ describe('manifest integrity', () => {
26
+ it('aggregate digest matches recomputation over ordered per-file digests', () => {
27
+ const agg = sha256(Buffer.from(ordered.map((e) => `${e.order} ${e.id} ${e.sha256}`).join('\n')));
28
+ expect(agg).toBe(manifest.aggregateSha256);
29
+ });
30
+ it('every ordered migration file matches its manifest digest (immutable bytes)', () => {
31
+ for (const m of ordered) expect(sha256(m.bytes)).toBe(m.sha256);
32
+ });
33
+ it('records source refs, schema version, and min client version', () => {
34
+ expect(manifest.source.portalBaseline).toMatch(/^[0-9a-f]{40}$/);
35
+ expect(manifest.source.laneHead).toMatch(/^[0-9a-f]{40}$/);
36
+ expect(manifest.source.mergeBase).toMatch(/^[0-9a-f]{40}$/);
37
+ expect(manifest.schemaVersion).toBe(7);
38
+ expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
39
+ });
40
+ });
41
+
42
+ describe('materialize (sole adopter path)', () => {
43
+ it('writes original bytes with a provenance marker; byte-exact + idempotent', () => {
44
+ const t = mktmp();
45
+ materialize(t);
46
+ const files = readdirSync(t).filter((f) => f.endsWith('.sql'));
47
+ expect(files.length).toBe(ordered.length);
48
+ for (const m of ordered) {
49
+ const written = readFileSync(join(t, m.filename), 'utf8');
50
+ expect(written.startsWith('-- @agent-board-schema:materialized')).toBe(true);
51
+ const original = written.slice(written.indexOf('\n') + 1);
52
+ expect(sha256(Buffer.from(original))).toBe(m.sha256);
53
+ }
54
+ // receipt present
55
+ const receipt = JSON.parse(readFileSync(join(t, '.agent-board-schema-materialize-receipt.json'), 'utf8'));
56
+ expect(receipt.orderedIds.length).toBe(ordered.length);
57
+ // idempotent
58
+ expect(() => materialize(t)).not.toThrow();
59
+ });
60
+ it('refuses to overwrite a manual difference', () => {
61
+ const t = mktmp();
62
+ materialize(t);
63
+ const victim = join(t, ordered[0].filename);
64
+ writeFileSync(victim, '-- @agent-board-schema:materialized 1.1.0 x y\nSELECT 1; -- tampered\n');
65
+ expect(() => materialize(t)).toThrow(/manual difference/);
66
+ });
67
+ });
68
+
69
+ describe('plan / check postures + exit codes (SemVer contract)', () => {
70
+ it('exact when the closed set is fully applied', () => {
71
+ expect(plan(historyExact()).posture).toBe('exact');
72
+ const c = check(historyExact());
73
+ expect(c.posture).toBe('exact');
74
+ expect(c.exitCode).toBe(0);
75
+ });
76
+ it('behind when the endpoint forward migration is pending', () => {
77
+ const p = plan({ applied: union().map((m) => ({ id: m.id, sha256: m.sha256 })) });
78
+ expect(p.posture).toBe('behind');
79
+ expect(p.perId.filter((r) => r.action === 'pending').map((r) => r.id)).toContain(
80
+ '20260712000000_agent_board_bundle_endpoint_compat',
81
+ );
82
+ expect(check({ applied: union().map((m) => ({ id: m.id, sha256: m.sha256 })) }).exitCode).toBe(10);
83
+ });
84
+ it('divergent when an applied digest mismatches', () => {
85
+ const applied = ordered.map((m, i) => (i === 3 ? { id: m.id, sha256: 'deadbeef' } : { id: m.id, sha256: m.sha256 }));
86
+ expect(check({ applied }).posture).toBe('divergent');
87
+ expect(check({ applied }).exitCode).toBe(12);
88
+ });
89
+ it('ahead when the adopter has an extra migration and nothing pending', () => {
90
+ const applied = [...historyExact().applied, { id: '99999999999999_extra', sha256: 'ff' }];
91
+ expect(check({ applied }).posture).toBe('ahead');
92
+ expect(check({ applied }).exitCode).toBe(11);
93
+ });
94
+ it('exit-code map is a frozen SemVer surface', () => {
95
+ expect(EXIT).toEqual({ exact: 0, behind: 10, ahead: 11, divergent: 12, error: 13 });
96
+ });
97
+ });
@@ -0,0 +1,166 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+
4
+ begin;
5
+
6
+ select plan(8);
7
+
8
+ create temp table _addressing_fixture (
9
+ actor uuid,
10
+ worker uuid,
11
+ reviewer uuid,
12
+ human uuid,
13
+ board_id uuid,
14
+ thread_id uuid
15
+ ) on commit drop;
16
+ grant all on table _addressing_fixture to authenticated, service_role;
17
+
18
+ do $$
19
+ declare
20
+ v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
21
+ v_actor uuid := gen_random_uuid();
22
+ v_worker uuid := gen_random_uuid();
23
+ v_reviewer uuid := gen_random_uuid();
24
+ v_human uuid := gen_random_uuid();
25
+ v_thread uuid := gen_random_uuid();
26
+ v_board uuid;
27
+ begin
28
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, metadata)
29
+ values
30
+ (v_actor, 'agent', 'agent', 'addressing-actor-' || v_suffix, 'ADDRESSING Actor', 'active', '{"role":"phase_worker","groupAliases":["ops"]}'::jsonb),
31
+ (v_worker, 'agent', 'agent', 'addressing-worker-' || v_suffix, 'ADDRESSING Worker', 'active', '{"role":"phase_worker","groupAliases":["ops"]}'::jsonb),
32
+ (v_reviewer, 'agent', 'agent', 'addressing-reviewer-' || v_suffix, 'ADDRESSING Reviewer', 'active', '{"roles":["reviewer"],"groupAliases":["review"]}'::jsonb),
33
+ (v_human, 'human', 'human', 'addressing-human-' || v_suffix, 'ADDRESSING Human', 'active', '{}'::jsonb);
34
+
35
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
36
+ values (v_thread, 'coordination', v_actor, 'ADDRESSING thread');
37
+
38
+ select board_id into v_board
39
+ from agent_board.create_board(v_actor, 'addressing-board-' || v_suffix, 'ADDRESSING Board', 'addressing test board', '{}'::jsonb, 'open');
40
+ perform agent_board.join_board(v_board, v_actor, 'joined', v_actor, '{}'::jsonb);
41
+ perform agent_board.join_board(v_board, v_worker, 'subscribed', v_actor, '{}'::jsonb);
42
+
43
+ insert into _addressing_fixture(actor, worker, reviewer, human, board_id, thread_id)
44
+ values (v_actor, v_worker, v_reviewer, v_human, v_board, v_thread);
45
+ end
46
+ $$;
47
+
48
+ select is(
49
+ jsonb_array_length(r."resolvedRecipients"),
50
+ 2,
51
+ 'group alias and duplicate explicit recipient collapse to two canonical recipients'
52
+ )
53
+ from agent_board.resolve_recipients(
54
+ (select actor from _addressing_fixture),
55
+ jsonb_build_array(
56
+ jsonb_build_object('kind', 'group_alias', 'groupAlias', 'ops'),
57
+ jsonb_build_object('kind', 'agent_id', 'agentId', (select worker::text from _addressing_fixture))
58
+ )
59
+ ) r;
60
+
61
+ select is(
62
+ jsonb_array_length(r."deniedRecipients"),
63
+ 1,
64
+ 'unknown handles are reported deterministically'
65
+ )
66
+ from agent_board.expand_recipients_dry_run(
67
+ (select actor from _addressing_fixture),
68
+ '[{"kind":"agent_handle","handle":"missing-addressing-agent"}]'::jsonb,
69
+ 'addressing-thread-key'
70
+ ) r;
71
+
72
+ select ok(
73
+ (select not r."idempotentThreadReuse"
74
+ from agent_board.expand_recipients_dry_run(
75
+ (select actor from _addressing_fixture),
76
+ jsonb_build_array(jsonb_build_object('kind', 'agent_id', 'agentId', (select worker::text from _addressing_fixture))),
77
+ 'addressing-thread-key'
78
+ ) r),
79
+ 'dry run does not create idempotent threads'
80
+ );
81
+
82
+ select ok(
83
+ (select reused = false
84
+ from agent_board.create_or_find_thread(
85
+ (select actor from _addressing_fixture),
86
+ 'ADDRESSING create or find',
87
+ 'addressing-thread-key',
88
+ 'coordination',
89
+ '{}'::jsonb
90
+ )),
91
+ 'first create_or_find_thread call creates the thread'
92
+ );
93
+
94
+ select ok(
95
+ (select reused = true
96
+ from agent_board.create_or_find_thread(
97
+ (select actor from _addressing_fixture),
98
+ 'ADDRESSING create or find',
99
+ 'addressing-thread-key',
100
+ 'coordination',
101
+ '{}'::jsonb
102
+ )),
103
+ 'second create_or_find_thread call reuses the thread'
104
+ );
105
+
106
+ select lives_ok(
107
+ $$
108
+ select *
109
+ from agent_board.send_direct_message(
110
+ (select thread_id from _addressing_fixture),
111
+ (select actor from _addressing_fixture),
112
+ jsonb_build_object('kind', 'agent_handle', 'handle', (select handle from agent_board.agents where id = (select worker from _addressing_fixture))),
113
+ 'addressing_v1',
114
+ 'Addressed direct',
115
+ 'Addressed direct body',
116
+ 'comment',
117
+ 'medium',
118
+ 'addressing-direct',
119
+ '{}'::jsonb
120
+ )
121
+ $$,
122
+ 'send_direct_message accepts an agent handle descriptor'
123
+ );
124
+
125
+ select lives_ok(
126
+ $$
127
+ select *
128
+ from agent_board.post_board_message(
129
+ (select thread_id from _addressing_fixture),
130
+ (select actor from _addressing_fixture),
131
+ (select slug from agent_board.boards where id = (select board_id from _addressing_fixture)),
132
+ 'addressing_v1',
133
+ 'Addressed board',
134
+ 'Addressed board body',
135
+ 'comment',
136
+ 'medium',
137
+ 'addressing-board-post',
138
+ '{}'::jsonb
139
+ )
140
+ $$,
141
+ 'post_board_message accepts board slug while preserving board-post RPC checks'
142
+ );
143
+
144
+ select throws_ok(
145
+ $$
146
+ select *
147
+ from agent_board.send_direct_message(
148
+ (select thread_id from _addressing_fixture),
149
+ (select actor from _addressing_fixture),
150
+ '{"kind":"role_selector","role":"phase_worker"}'::jsonb,
151
+ 'addressing_v1',
152
+ 'Ambiguous direct',
153
+ 'Direct role should resolve more than one recipient',
154
+ 'comment',
155
+ 'medium',
156
+ 'addressing-direct-role',
157
+ '{}'::jsonb
158
+ )
159
+ $$,
160
+ '22023',
161
+ null,
162
+ 'direct send rejects descriptors that expand to more than one recipient'
163
+ );
164
+
165
+ select * from finish();
166
+ rollback;
@@ -0,0 +1,243 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+
4
+ begin;
5
+
6
+ select plan(4);
7
+
8
+ select has_table('agent_board', 'inbox_items', 'inbox_items table exists for auth RLS checks');
9
+ select has_table('agent_board', 'callback_endpoints', 'callback_endpoints table exists for auth RLS checks');
10
+
11
+ create temp table _agent_board_authrls_fixture (
12
+ user_a uuid,
13
+ user_b uuid,
14
+ agent_a uuid,
15
+ agent_b uuid,
16
+ board_a uuid
17
+ ) on commit drop;
18
+
19
+ grant all on table _agent_board_authrls_fixture to authenticated, service_role;
20
+
21
+ do $$
22
+ declare
23
+ v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
24
+ v_user_a uuid;
25
+ v_user_b uuid;
26
+ v_agent_a uuid := gen_random_uuid();
27
+ v_agent_b uuid := gen_random_uuid();
28
+ v_thread_a uuid := gen_random_uuid();
29
+ v_thread_b uuid := gen_random_uuid();
30
+ v_message_a uuid := gen_random_uuid();
31
+ v_message_b uuid := gen_random_uuid();
32
+ v_task_a uuid := gen_random_uuid();
33
+ v_task_b uuid := gen_random_uuid();
34
+ v_board_a uuid := gen_random_uuid();
35
+ v_board_b uuid := gen_random_uuid();
36
+ v_recipient_a uuid := gen_random_uuid();
37
+ v_recipient_b uuid := gen_random_uuid();
38
+ v_inbox_a uuid := gen_random_uuid();
39
+ v_inbox_b uuid := gen_random_uuid();
40
+ v_endpoint_a uuid := gen_random_uuid();
41
+ v_endpoint_b uuid := gen_random_uuid();
42
+ v_subscription_a uuid := gen_random_uuid();
43
+ v_subscription_b uuid := gen_random_uuid();
44
+ v_outbox_a uuid := gen_random_uuid();
45
+ v_outbox_b uuid := gen_random_uuid();
46
+ begin
47
+ v_user_a := public.ensure_github_user(
48
+ 'agent-board-authrls-a-' || v_suffix,
49
+ 'agent-board-authrls-a-' || v_suffix || '@example.test',
50
+ 'agent-board-authrls-a-' || v_suffix,
51
+ 'Agent Board Auth RLS A ' || v_suffix
52
+ );
53
+ v_user_b := public.ensure_github_user(
54
+ 'agent-board-authrls-b-' || v_suffix,
55
+ 'agent-board-authrls-b-' || v_suffix || '@example.test',
56
+ 'agent-board-authrls-b-' || v_suffix,
57
+ 'Agent Board Auth RLS B ' || v_suffix
58
+ );
59
+
60
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
61
+ values
62
+ (v_agent_a, 'agent', 'agent', 'authrls-a-' || v_suffix, 'Auth RLS Agent A', 'active', v_user_a, v_user_a),
63
+ (v_agent_b, 'agent', 'agent', 'authrls-b-' || v_suffix, 'Auth RLS Agent B', 'active', v_user_b, v_user_b);
64
+
65
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
66
+ values
67
+ (v_thread_a, 'coordination', v_agent_a, 'Auth RLS Thread A'),
68
+ (v_thread_b, 'coordination', v_agent_b, 'Auth RLS Thread B');
69
+
70
+ insert into agent_board.tasks(id, thread_id, created_by_agent_id, assigned_agent_id, root_task_id, title, priority, orchestration_mode)
71
+ values
72
+ (v_task_a, v_thread_a, v_agent_a, v_agent_a, v_task_a, 'Auth RLS Task A', 'medium', 'distributed'),
73
+ (v_task_b, v_thread_b, v_agent_b, v_agent_b, v_task_b, 'Auth RLS Task B', 'medium', 'distributed');
74
+
75
+ insert into agent_board.task_events(task_id, thread_id, actor_agent_id, event_sequence, event_type, payload)
76
+ values
77
+ (v_task_a, v_thread_a, v_agent_a, 1, 'created', '{}'::jsonb),
78
+ (v_task_b, v_thread_b, v_agent_b, 1, 'created', '{}'::jsonb);
79
+
80
+ insert into agent_board.messages(id, thread_id, author_agent_id, message_sequence, message_type, body)
81
+ values
82
+ (v_message_a, v_thread_a, v_agent_a, 1, 'comment', 'message a'),
83
+ (v_message_b, v_thread_b, v_agent_b, 1, 'comment', 'message b');
84
+
85
+ insert into agent_board.claims(task_id, thread_id, agent_id, resource_kind, resource_key, lease_expires_at)
86
+ values
87
+ (v_task_a, v_thread_a, v_agent_a, 'repo_file', 'authrls-a-' || v_suffix, now() + interval '1 hour'),
88
+ (v_task_b, v_thread_b, v_agent_b, 'repo_file', 'authrls-b-' || v_suffix, now() + interval '1 hour');
89
+
90
+ insert into agent_board.boards(id, slug, name, created_by_agent_id)
91
+ values
92
+ (v_board_a, 'authrls-board-a-' || v_suffix, 'Auth RLS Board A', v_agent_a),
93
+ (v_board_b, 'authrls-board-b-' || v_suffix, 'Auth RLS Board B', v_agent_b);
94
+
95
+ insert into agent_board.board_memberships(id, board_id, agent_id, membership_state, created_by_agent_id)
96
+ values
97
+ (gen_random_uuid(), v_board_a, v_agent_a, 'joined', v_agent_a),
98
+ (gen_random_uuid(), v_board_b, v_agent_b, 'joined', v_agent_b);
99
+
100
+ insert into agent_board.delivery_recipients(id, delivery_mode, recipient_agent_id, source_kind, source_message_id, board_id)
101
+ values
102
+ (v_recipient_a, 'direct', v_agent_a, 'message', v_message_a, null),
103
+ (v_recipient_b, 'direct', v_agent_b, 'message', v_message_b, null);
104
+
105
+ insert into agent_board.inbox_items(
106
+ id,
107
+ delivery_recipient_id,
108
+ recipient_agent_id,
109
+ board_id,
110
+ delivery_mode,
111
+ source_kind,
112
+ source_message_id,
113
+ subject,
114
+ preview,
115
+ priority,
116
+ action_state,
117
+ visible_in_inbox,
118
+ visible_in_board_feed,
119
+ fetch_descriptor
120
+ )
121
+ values
122
+ (
123
+ v_inbox_a,
124
+ v_recipient_a,
125
+ v_agent_a,
126
+ null,
127
+ 'direct',
128
+ 'message',
129
+ v_message_a,
130
+ 'Inbox A',
131
+ 'Preview A',
132
+ 'medium',
133
+ 'new',
134
+ true,
135
+ false,
136
+ jsonb_build_object('surface', 'thread', 'targetId', v_thread_a::text, 'view', 'agent_board.v_thread_feed', 'filters', jsonb_build_object(), 'reason', 'auth rls a')
137
+ ),
138
+ (
139
+ v_inbox_b,
140
+ v_recipient_b,
141
+ v_agent_b,
142
+ null,
143
+ 'direct',
144
+ 'message',
145
+ v_message_b,
146
+ 'Inbox B',
147
+ 'Preview B',
148
+ 'medium',
149
+ 'new',
150
+ true,
151
+ false,
152
+ jsonb_build_object('surface', 'thread', 'targetId', v_thread_b::text, 'view', 'agent_board.v_thread_feed', 'filters', jsonb_build_object(), 'reason', 'auth rls b')
153
+ );
154
+
155
+ insert into agent_board.callback_endpoints(id, owner_agent_id, endpoint_url, endpoint_status, verification_requested_at, verified_at)
156
+ values
157
+ (v_endpoint_a, v_agent_a, 'https://example.test/authrls-a-' || v_suffix, 'verified', now(), now()),
158
+ (v_endpoint_b, v_agent_b, 'https://example.test/authrls-b-' || v_suffix, 'verified', now(), now());
159
+
160
+ insert into agent_board.callback_subscriptions(id, endpoint_id, agent_id, subscription_scope, event_category, subscription_status)
161
+ values
162
+ (v_subscription_a, v_endpoint_a, v_agent_a, 'own_inbox', 'inbox.item.delivered', 'active'),
163
+ (v_subscription_b, v_endpoint_b, v_agent_b, 'own_inbox', 'inbox.item.delivered', 'active');
164
+
165
+ insert into agent_board.callback_outbox_events(
166
+ id,
167
+ endpoint_id,
168
+ subscription_id,
169
+ recipient_agent_id,
170
+ inbox_item_id,
171
+ event_category,
172
+ delivery_mode,
173
+ source_kind,
174
+ source_message_id,
175
+ idempotency_key,
176
+ fetch_descriptor
177
+ )
178
+ values
179
+ (v_outbox_a, v_endpoint_a, v_subscription_a, v_agent_a, v_inbox_a, 'inbox.item.delivered', 'direct', 'message', v_message_a, 'outbox-a-' || v_suffix, jsonb_build_object('surface', 'thread', 'targetId', v_thread_a::text, 'view', 'agent_board.v_thread_feed', 'filters', jsonb_build_object(), 'reason', 'auth rls outbox a')),
180
+ (v_outbox_b, v_endpoint_b, v_subscription_b, v_agent_b, v_inbox_b, 'inbox.item.delivered', 'direct', 'message', v_message_b, 'outbox-b-' || v_suffix, jsonb_build_object('surface', 'thread', 'targetId', v_thread_b::text, 'view', 'agent_board.v_thread_feed', 'filters', jsonb_build_object(), 'reason', 'auth rls outbox b'));
181
+
182
+ insert into agent_board.callback_delivery_attempts(outbox_event_id, endpoint_id, attempt_number, attempt_outcome, started_at, finished_at)
183
+ values
184
+ (v_outbox_a, v_endpoint_a, 1, 'delivered', now(), now()),
185
+ (v_outbox_b, v_endpoint_b, 1, 'delivered', now(), now());
186
+
187
+ insert into _agent_board_authrls_fixture(user_a, user_b, agent_a, agent_b, board_a)
188
+ values (v_user_a, v_user_b, v_agent_a, v_agent_b, v_board_a);
189
+ end
190
+ $$;
191
+
192
+ set local role authenticated;
193
+ select set_config('request.jwt.claim.role', 'authenticated', true);
194
+
195
+ do $$
196
+ declare
197
+ v_fixture record;
198
+ v_inbox_count bigint;
199
+ v_claim_count bigint;
200
+ v_board_count bigint;
201
+ v_endpoint_count bigint;
202
+ begin
203
+ select * into v_fixture from _agent_board_authrls_fixture;
204
+ perform set_config('request.jwt.claim.sub', v_fixture.user_a::text, true);
205
+
206
+ select count(*) into v_inbox_count from agent_board.v_agent_inbox;
207
+ if v_inbox_count <> 1 then
208
+ raise exception 'agent A should see exactly one inbox item, saw %', v_inbox_count;
209
+ end if;
210
+
211
+ if exists (
212
+ select 1
213
+ from agent_board.inbox_items ii
214
+ where ii.recipient_agent_id = v_fixture.agent_b
215
+ ) then
216
+ raise exception 'agent A unexpectedly read agent B inbox rows';
217
+ end if;
218
+
219
+ select count(*) into v_claim_count from agent_board.claims;
220
+ if v_claim_count <> 1 then
221
+ raise exception 'agent A should see exactly one claim row, saw %', v_claim_count;
222
+ end if;
223
+
224
+ select count(*) into v_board_count
225
+ from agent_board.v_board_directory
226
+ where board_id = v_fixture.board_a;
227
+ if v_board_count <> 1 then
228
+ raise exception 'agent A should see exactly one joined board, saw %', v_board_count;
229
+ end if;
230
+
231
+ select count(*) into v_endpoint_count from agent_board.callback_endpoints;
232
+ if v_endpoint_count <> 1 then
233
+ raise exception 'agent A should see exactly one callback endpoint, saw %', v_endpoint_count;
234
+ end if;
235
+ end
236
+ $$;
237
+
238
+ select pass('Authenticated RLS filters inbox, claim, board, and callback rows to the current agent');
239
+ select pass('Authenticated cross-agent reads are denied by current_agent_id-scoped policies');
240
+
241
+ select finish();
242
+
243
+ rollback;