@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.
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +55 -0
- package/bin/agent-board-schema.js +97 -0
- package/fixtures/already-applied.json +181 -0
- package/fixtures/divergent-history.json +173 -0
- package/fixtures/production-history.json +173 -0
- package/migrations/20260501000001_agent_board_schema_version.sql +12 -0
- package/migrations/20260502000000_agent_board_schema.sql +180 -0
- package/migrations/20260503000000_agent_board_rpcs.sql +422 -0
- package/migrations/20260503000001_agent_board_coordination_rpcs.sql +229 -0
- package/migrations/20260504000000_agent_board_read_models.sql +206 -0
- package/migrations/20260505000000_agent_board_inbox_schema.sql +341 -0
- package/migrations/20260506000000_agent_board_inbox_delivery_rpcs.sql +1200 -0
- package/migrations/20260506000001_agent_board_inbox_callback_rpcs.sql +330 -0
- package/migrations/20260507000000_agent_board_inbox_reads.sql +334 -0
- package/migrations/20260508000000_agent_board_callback_runtime.sql +453 -0
- package/migrations/20260508000001_agent_board_inbox_service_role_reads.sql +10 -0
- package/migrations/20260510000000_agent_board_dblint_repairs.sql +413 -0
- package/migrations/20260511000000_agent_board_auth_schema.sql +146 -0
- package/migrations/20260511000001_agent_board_auth_credentials.sql +39 -0
- package/migrations/20260512000000_agent_board_auth_provisioning_rpcs.sql +622 -0
- package/migrations/20260512000001_agent_board_auth_v1_rpcs.sql +798 -0
- package/migrations/20260512000002_agent_board_auth_v2_rpcs.sql +1243 -0
- package/migrations/20260512000003_agent_board_auth_rls.sql +484 -0
- package/migrations/20260513000000_agent_board_backup_ops.sql +34 -0
- package/migrations/20260513000001_agent_board_retention.sql +160 -0
- package/migrations/20260513000002_agent_board_monitoring_views.sql +227 -0
- package/migrations/20260514000000_agent_board_auth_rls_id_qualification.sql +150 -0
- package/migrations/20260515000000_agent_board_boardopt.sql +493 -0
- package/migrations/20260516000000_agent_board_callbackcap.sql +113 -0
- package/migrations/20260517000000_agent_board_addressing.sql +542 -0
- package/migrations/20260628000000_agent_board_signing_store.sql +174 -0
- package/migrations/20260628000001_agent_board_signing_proof_rpcs.sql +215 -0
- package/migrations/20260628000002_agent_board_signature_rls_reads.sql +300 -0
- package/migrations/20260628000003_agent_board_signed_write_gateway.sql +288 -0
- package/migrations/20260628000004_agent_board_fleet_registry.sql +411 -0
- package/migrations/20260628000005_agent_board_fleet_policy.sql +509 -0
- package/migrations/20260628000006_agent_board_fleet_read_models.sql +170 -0
- package/migrations/20260629000100_agent_board_fleet_registry_repair.sql +411 -0
- package/migrations/20260629000101_agent_board_fleet_policy_refresh.sql +509 -0
- package/migrations/20260629000102_agent_board_fleet_read_models_refresh.sql +170 -0
- package/migrations/20260630000100_agent_board_harness_kind_open_vocab.sql +15 -0
- package/migrations/20260710000000_agent_board_authbound_boundary.sql +155 -0
- package/migrations/20260710000100_agent_board_schema_version_rpc.sql +14 -0
- package/migrations/20260710000400_agent_board_nonfleet_identity.sql +87 -0
- package/migrations/20260710000401_agent_board_workload_providers.sql +39 -0
- package/migrations/20260711000000_agent_board_provision_system_fail_closed.sql +242 -0
- package/migrations/20260711000100_canary_signed_fleet_enforcement.sql +234 -0
- package/migrations/20260712000000_agent_board_bundle_endpoint_compat.sql +97 -0
- package/migrations/20260713000000_agent_board_rls_reassert_board_scoping.sql +90 -0
- package/package.json +42 -0
- package/provenance.json +580 -0
- package/schema-manifest.json +458 -0
- package/src/check.js +23 -0
- package/src/index.js +50 -0
- package/src/materialize.js +73 -0
- package/src/plan.js +44 -0
- package/src/tooling.test.mjs +97 -0
- package/tests/agent_board_addressing.test.sql +166 -0
- package/tests/agent_board_auth_rls.test.sql +243 -0
- package/tests/agent_board_auth_rls_id_qualification.test.sql +225 -0
- package/tests/agent_board_auth_rpcs.test.sql +311 -0
- package/tests/agent_board_auth_schema.test.sql +411 -0
- package/tests/agent_board_authbound_boundary.test.sql +148 -0
- package/tests/agent_board_boardopt.test.sql +170 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +49 -0
- package/tests/agent_board_callback_runtime.test.sql +425 -0
- package/tests/agent_board_callbackcap.test.sql +222 -0
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +60 -0
- package/tests/agent_board_dblint_rpcs.test.sql +247 -0
- package/tests/agent_board_fleet_policy.test.sql +370 -0
- package/tests/agent_board_fleet_read_models.test.sql +158 -0
- package/tests/agent_board_fleet_registry.test.sql +149 -0
- package/tests/agent_board_inbox_reads.test.sql +471 -0
- package/tests/agent_board_inbox_rpcs.test.sql +452 -0
- package/tests/agent_board_inbox_schema.test.sql +355 -0
- package/tests/agent_board_nonfleet_identity.test.sql +26 -0
- package/tests/agent_board_rls_reassert_board_scoping.test.sql +39 -0
- package/tests/agent_board_schema_version.test.sql +24 -0
- package/tests/agent_board_schema_version_rpc.test.sql +40 -0
- package/tests/agent_board_signature_rls_reads.test.sql +133 -0
- package/tests/agent_board_signed_write_gateway.test.sql +219 -0
- package/tests/agent_board_signing_proof_rpcs.test.sql +143 -0
- package/tests/agent_board_signing_store.test.sql +183 -0
- package/tests/agent_board_smoke.test.sql +12 -0
- package/tests/agent_board_v1_invariants.test.sql +298 -0
- package/tests/agent_board_v2_invariants.test.sql +753 -0
- package/tests/agent_board_workload_providers.test.sql +15 -0
- package/tests/fixtures/adopter-base.sql +19 -0
- package/tests/fixtures/operator-hardening.sql +27 -0
- 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;
|