@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,206 @@
1
+ create or replace view agent_board.v_thread_feed
2
+ with (security_invoker = true)
3
+ as
4
+ select
5
+ m.thread_id,
6
+ m.id as message_id,
7
+ m.message_sequence,
8
+ m.created_at as message_created_at,
9
+ m.author_agent_id,
10
+ a.handle as author_handle,
11
+ a.display_name as author_display_name,
12
+ m.message_type,
13
+ m.body,
14
+ m.task_id
15
+ from agent_board.messages m
16
+ join agent_board.agents a on a.id = m.author_agent_id;
17
+
18
+ create or replace view agent_board.v_task_tree
19
+ with (security_invoker = true)
20
+ as
21
+ select
22
+ coalesce(t.root_task_id, t.id) as root_task_id,
23
+ t.id as task_id,
24
+ t.parent_task_id,
25
+ t.thread_id,
26
+ t.created_by_agent_id,
27
+ t.assigned_agent_id,
28
+ assigned.handle as assigned_agent_handle,
29
+ t.status,
30
+ t.priority,
31
+ t.orchestration_mode,
32
+ t.title,
33
+ t.details,
34
+ t.created_at,
35
+ t.updated_at,
36
+ t.completed_at,
37
+ latest.event_sequence as latest_event_sequence,
38
+ latest.event_type as latest_event_type,
39
+ latest.payload->>'kind' as latest_event_kind
40
+ from agent_board.tasks t
41
+ left join agent_board.agents assigned on assigned.id = t.assigned_agent_id
42
+ left join lateral (
43
+ select te.event_sequence, te.event_type, te.payload
44
+ from agent_board.task_events te
45
+ where te.task_id = t.id
46
+ order by te.event_sequence desc
47
+ limit 1
48
+ ) latest on true;
49
+
50
+ create or replace view agent_board.v_active_claims
51
+ with (security_invoker = true)
52
+ as
53
+ select
54
+ c.id as claim_id,
55
+ c.task_id,
56
+ c.thread_id,
57
+ c.agent_id,
58
+ a.handle as agent_handle,
59
+ c.resource_kind,
60
+ c.resource_key,
61
+ c.lease_token,
62
+ c.lease_expires_at,
63
+ c.created_at,
64
+ c.updated_at,
65
+ t.status as task_status
66
+ from agent_board.claims c
67
+ join agent_board.agents a on a.id = c.agent_id
68
+ left join agent_board.tasks t on t.id = c.task_id
69
+ where c.status = 'active'
70
+ and c.released_at is null
71
+ and c.lease_expires_at > now();
72
+
73
+ create or replace view agent_board.v_open_escalations
74
+ with (security_invoker = true)
75
+ as
76
+ select
77
+ e.id as escalation_id,
78
+ e.thread_id,
79
+ e.task_id,
80
+ e.raised_by_agent_id,
81
+ raised.handle as raised_by_handle,
82
+ e.assigned_agent_id,
83
+ assigned.handle as assigned_agent_handle,
84
+ e.escalation_type,
85
+ e.severity,
86
+ e.status,
87
+ e.summary,
88
+ e.details,
89
+ e.created_at,
90
+ e.updated_at,
91
+ e.acknowledged_at,
92
+ t.status as task_status
93
+ from agent_board.escalations e
94
+ join agent_board.agents raised on raised.id = e.raised_by_agent_id
95
+ left join agent_board.agents assigned on assigned.id = e.assigned_agent_id
96
+ left join agent_board.tasks t on t.id = e.task_id
97
+ where e.status in ('open', 'acknowledged');
98
+
99
+ create or replace view agent_board.v_agent_dashboard
100
+ with (security_invoker = true)
101
+ as
102
+ select
103
+ a.id as agent_id,
104
+ a.handle,
105
+ a.display_name,
106
+ a.status as participant_status,
107
+ p.availability,
108
+ p.thread_id as presence_thread_id,
109
+ p.last_heartbeat_at,
110
+ p.expires_at as presence_expires_at,
111
+ coalesce(open_tasks.open_task_count, 0)::bigint as open_task_count,
112
+ coalesce(active_claims.active_claim_count, 0)::bigint as active_claim_count,
113
+ coalesce(open_escalations.assigned_open_escalation_count, 0)::bigint as assigned_open_escalation_count,
114
+ latest.task_id as latest_task_id,
115
+ latest.event_sequence as latest_task_event_sequence,
116
+ latest.event_type as latest_task_event_type,
117
+ latest.payload->>'kind' as latest_task_event_kind,
118
+ latest.updated_at as latest_task_updated_at
119
+ from agent_board.agents a
120
+ left join agent_board.presence p on p.agent_id = a.id
121
+ left join lateral (
122
+ select count(*) as open_task_count
123
+ from agent_board.tasks t
124
+ where t.assigned_agent_id = a.id
125
+ and t.status in ('pending', 'in_progress', 'blocked')
126
+ ) open_tasks on true
127
+ left join lateral (
128
+ select count(*) as active_claim_count
129
+ from agent_board.claims c
130
+ where c.agent_id = a.id
131
+ and c.status = 'active'
132
+ and c.released_at is null
133
+ and c.lease_expires_at > now()
134
+ ) active_claims on true
135
+ left join lateral (
136
+ select count(*) as assigned_open_escalation_count
137
+ from agent_board.escalations e
138
+ where e.assigned_agent_id = a.id
139
+ and e.status in ('open', 'acknowledged')
140
+ ) open_escalations on true
141
+ left join lateral (
142
+ select t.id as task_id, te.event_sequence, te.event_type, te.payload, t.updated_at
143
+ from agent_board.tasks t
144
+ left join agent_board.task_events te on te.task_id = t.id
145
+ where t.assigned_agent_id = a.id or t.created_by_agent_id = a.id
146
+ order by t.updated_at desc, te.event_sequence desc nulls last
147
+ limit 1
148
+ ) latest on true;
149
+
150
+ grant usage on schema agent_board to service_role;
151
+ grant select on all tables in schema agent_board to service_role;
152
+ grant select on agent_board.v_thread_feed, agent_board.v_task_tree, agent_board.v_active_claims, agent_board.v_open_escalations, agent_board.v_agent_dashboard to authenticated;
153
+
154
+ alter table agent_board.agents enable row level security;
155
+ alter table agent_board.threads enable row level security;
156
+ alter table agent_board.messages enable row level security;
157
+ alter table agent_board.tasks enable row level security;
158
+ alter table agent_board.task_events enable row level security;
159
+ alter table agent_board.claims enable row level security;
160
+ alter table agent_board.delegations enable row level security;
161
+ alter table agent_board.artifacts enable row level security;
162
+ alter table agent_board.conflicts enable row level security;
163
+ alter table agent_board.escalations enable row level security;
164
+ alter table agent_board.read_receipts enable row level security;
165
+ alter table agent_board.presence enable row level security;
166
+
167
+ do $$
168
+ declare
169
+ v_table text;
170
+ begin
171
+ foreach v_table in array array[
172
+ 'agents',
173
+ 'threads',
174
+ 'messages',
175
+ 'tasks',
176
+ 'task_events',
177
+ 'claims',
178
+ 'delegations',
179
+ 'artifacts',
180
+ 'conflicts',
181
+ 'escalations',
182
+ 'read_receipts',
183
+ 'presence'
184
+ ]
185
+ loop
186
+ execute format('drop policy if exists %I on agent_board.%I', 'agent_board_service_all_' || v_table, v_table);
187
+ execute format('create policy %I on agent_board.%I as permissive for all to service_role using (true) with check (true)', 'agent_board_service_all_' || v_table, v_table);
188
+ end loop;
189
+ end;
190
+ $$;
191
+
192
+ do $$
193
+ declare
194
+ v_table text;
195
+ begin
196
+ foreach v_table in array array['messages', 'task_events', 'tasks', 'claims', 'delegations', 'conflicts', 'escalations', 'presence']
197
+ loop
198
+ begin
199
+ execute format('alter publication supabase_realtime add table agent_board.%I', v_table);
200
+ exception
201
+ when duplicate_object then null;
202
+ when undefined_object then null;
203
+ end;
204
+ end loop;
205
+ end;
206
+ $$;
@@ -0,0 +1,341 @@
1
+ create table if not exists agent_board.boards (
2
+ id uuid primary key default gen_random_uuid(),
3
+ slug text not null unique,
4
+ name text not null,
5
+ description text,
6
+ created_by_agent_id uuid not null references agent_board.agents(id),
7
+ status text not null default 'active' check (status in ('active', 'archived')),
8
+ metadata jsonb not null default '{}'::jsonb,
9
+ created_at timestamptz not null default now(),
10
+ updated_at timestamptz not null default now()
11
+ );
12
+
13
+ comment on table agent_board.boards is 'Named opt-in coordination surfaces for shared board delivery and board feeds.';
14
+
15
+ create table if not exists agent_board.board_memberships (
16
+ id uuid primary key default gen_random_uuid(),
17
+ board_id uuid not null references agent_board.boards(id) on delete cascade,
18
+ agent_id uuid not null references agent_board.agents(id) on delete cascade,
19
+ membership_state text not null default 'joined' check (membership_state in ('joined', 'subscribed', 'left', 'removed')),
20
+ created_by_agent_id uuid references agent_board.agents(id),
21
+ left_at timestamptz,
22
+ metadata jsonb not null default '{}'::jsonb,
23
+ created_at timestamptz not null default now(),
24
+ updated_at timestamptz not null default now(),
25
+ constraint board_memberships_state_left_at check (
26
+ (membership_state in ('joined', 'subscribed') and left_at is null)
27
+ or (membership_state in ('left', 'removed') and left_at is not null)
28
+ ),
29
+ constraint board_memberships_identity_unique unique (id, board_id, agent_id)
30
+ );
31
+
32
+ comment on table agent_board.board_memberships is 'One agent membership state per board; joined and subscribed are active delivery states.';
33
+
34
+ create unique index if not exists idx_agent_board_board_memberships_active_unique
35
+ on agent_board.board_memberships(board_id, agent_id)
36
+ where membership_state in ('joined', 'subscribed');
37
+
38
+ create index if not exists idx_agent_board_board_memberships_board_state
39
+ on agent_board.board_memberships(board_id, membership_state, created_at desc);
40
+
41
+ create table if not exists agent_board.delivery_recipients (
42
+ id uuid primary key default gen_random_uuid(),
43
+ delivery_mode text not null check (delivery_mode in ('direct', 'multicast', 'board', 'tasking')),
44
+ recipient_agent_id uuid not null references agent_board.agents(id) on delete cascade,
45
+ source_kind text not null check (source_kind in ('message', 'task', 'delegation')),
46
+ source_message_id uuid references agent_board.messages(id) on delete cascade,
47
+ source_task_id uuid references agent_board.tasks(id) on delete cascade,
48
+ source_delegation_id uuid references agent_board.delegations(id) on delete cascade,
49
+ board_id uuid references agent_board.boards(id) on delete cascade,
50
+ board_membership_id uuid,
51
+ canonical_source_key text generated always as (
52
+ case source_kind
53
+ when 'message' then 'message:' || source_message_id::text
54
+ when 'task' then 'task:' || source_task_id::text
55
+ when 'delegation' then 'delegation:' || source_delegation_id::text
56
+ end
57
+ ) stored,
58
+ metadata jsonb not null default '{}'::jsonb,
59
+ created_at timestamptz not null default now(),
60
+ constraint delivery_recipients_source_shape check (
61
+ (source_kind = 'message' and source_message_id is not null and source_task_id is null and source_delegation_id is null)
62
+ or (source_kind = 'task' and source_message_id is null and source_task_id is not null and source_delegation_id is null)
63
+ or (source_kind = 'delegation' and source_message_id is null and source_task_id is null and source_delegation_id is not null)
64
+ ),
65
+ constraint delivery_recipients_board_scope check (
66
+ (
67
+ delivery_mode in ('direct', 'multicast')
68
+ and board_id is null
69
+ and board_membership_id is null
70
+ )
71
+ or (
72
+ delivery_mode in ('board', 'tasking')
73
+ and (
74
+ (board_id is null and board_membership_id is null)
75
+ or (board_id is not null and board_membership_id is not null)
76
+ )
77
+ )
78
+ ),
79
+ constraint delivery_recipients_board_membership_fkey
80
+ foreign key (board_membership_id, board_id, recipient_agent_id)
81
+ references agent_board.board_memberships(id, board_id, agent_id)
82
+ );
83
+
84
+ comment on table agent_board.delivery_recipients is 'Append-only canonical routing rows for direct, multicast, board, and tasking recipients.';
85
+
86
+ create unique index if not exists idx_agent_board_delivery_recipients_unique_source_recipient
87
+ on agent_board.delivery_recipients(recipient_agent_id, canonical_source_key);
88
+
89
+ create index if not exists idx_agent_board_delivery_recipients_board_feed
90
+ on agent_board.delivery_recipients(board_id, created_at desc)
91
+ where board_id is not null;
92
+
93
+ create table if not exists agent_board.inbox_items (
94
+ id uuid primary key default gen_random_uuid(),
95
+ delivery_recipient_id uuid not null unique references agent_board.delivery_recipients(id) on delete cascade,
96
+ recipient_agent_id uuid not null references agent_board.agents(id) on delete cascade,
97
+ board_id uuid references agent_board.boards(id) on delete cascade,
98
+ delivery_mode text not null check (delivery_mode in ('direct', 'multicast', 'board', 'tasking')),
99
+ source_kind text not null check (source_kind in ('message', 'task', 'delegation')),
100
+ source_message_id uuid references agent_board.messages(id) on delete cascade,
101
+ source_task_id uuid references agent_board.tasks(id) on delete cascade,
102
+ source_delegation_id uuid references agent_board.delegations(id) on delete cascade,
103
+ subject text not null,
104
+ preview text,
105
+ priority text not null default 'medium' check (priority in ('low', 'medium', 'high', 'urgent')),
106
+ action_state text not null default 'new' check (action_state in ('new', 'read', 'acknowledged', 'deferred', 'dismissed', 'restored')),
107
+ visible_in_inbox boolean not null default true,
108
+ visible_in_board_feed boolean not null default false,
109
+ fetch_descriptor jsonb not null default '{}'::jsonb,
110
+ metadata jsonb not null default '{}'::jsonb,
111
+ created_at timestamptz not null default now(),
112
+ updated_at timestamptz not null default now(),
113
+ constraint inbox_items_source_shape check (
114
+ (source_kind = 'message' and source_message_id is not null and source_task_id is null and source_delegation_id is null)
115
+ or (source_kind = 'task' and source_message_id is null and source_task_id is not null and source_delegation_id is null)
116
+ or (source_kind = 'delegation' and source_message_id is null and source_task_id is null and source_delegation_id is not null)
117
+ ),
118
+ constraint inbox_items_visibility_shape check (
119
+ (visible_in_inbox or visible_in_board_feed)
120
+ and (not visible_in_board_feed or board_id is not null)
121
+ ),
122
+ constraint inbox_items_board_scope check (
123
+ (
124
+ delivery_mode in ('direct', 'multicast')
125
+ and board_id is null
126
+ and visible_in_board_feed = false
127
+ )
128
+ or delivery_mode in ('board', 'tasking')
129
+ ),
130
+ constraint inbox_items_fetch_descriptor_object check (jsonb_typeof(fetch_descriptor) = 'object')
131
+ );
132
+
133
+ comment on table agent_board.inbox_items is 'Compact per-recipient triage rows with bounded metadata and canonical fetch descriptors.';
134
+
135
+ create index if not exists idx_agent_board_inbox_items_agent_triage
136
+ on agent_board.inbox_items(recipient_agent_id, updated_at desc, created_at desc);
137
+
138
+ create index if not exists idx_agent_board_inbox_items_agent_pending
139
+ on agent_board.inbox_items(recipient_agent_id, updated_at desc)
140
+ where action_state in ('new', 'restored', 'deferred');
141
+
142
+ create index if not exists idx_agent_board_inbox_items_board_feed
143
+ on agent_board.inbox_items(board_id, created_at desc)
144
+ where visible_in_board_feed = true;
145
+
146
+ create table if not exists agent_board.callback_endpoints (
147
+ id uuid primary key default gen_random_uuid(),
148
+ owner_agent_id uuid not null references agent_board.agents(id) on delete cascade,
149
+ endpoint_url text not null,
150
+ endpoint_status text not null default 'pending_verification' check (endpoint_status in ('pending_verification', 'verified', 'disabled')),
151
+ signing_key_version integer not null default 1 check (signing_key_version > 0),
152
+ verification_requested_at timestamptz,
153
+ verified_at timestamptz,
154
+ disabled_at timestamptz,
155
+ disabled_reason text,
156
+ last_delivery_at timestamptz,
157
+ last_failure_at timestamptz,
158
+ metadata jsonb not null default '{}'::jsonb,
159
+ created_at timestamptz not null default now(),
160
+ updated_at timestamptz not null default now(),
161
+ constraint callback_endpoints_status_disabled_shape check (
162
+ (endpoint_status = 'disabled' and disabled_at is not null)
163
+ or (endpoint_status <> 'disabled' and disabled_at is null)
164
+ ),
165
+ constraint callback_endpoints_url_unique unique (owner_agent_id, endpoint_url)
166
+ );
167
+
168
+ comment on table agent_board.callback_endpoints is 'Durable callback endpoint registration with verification and disablement metadata.';
169
+
170
+ create table if not exists agent_board.callback_subscriptions (
171
+ id uuid primary key default gen_random_uuid(),
172
+ endpoint_id uuid not null references agent_board.callback_endpoints(id) on delete cascade,
173
+ agent_id uuid not null references agent_board.agents(id) on delete cascade,
174
+ subscription_scope text not null check (subscription_scope in ('own_inbox', 'board')),
175
+ event_category text not null check (event_category in ('inbox.item.delivered', 'inbox.item.updated', 'board.post.delivered', 'board.post.updated')),
176
+ board_id uuid references agent_board.boards(id) on delete cascade,
177
+ board_membership_id uuid,
178
+ subscription_status text not null default 'active' check (subscription_status in ('active', 'disabled')),
179
+ disabled_at timestamptz,
180
+ metadata jsonb not null default '{}'::jsonb,
181
+ created_at timestamptz not null default now(),
182
+ updated_at timestamptz not null default now(),
183
+ constraint callback_subscriptions_scope_shape check (
184
+ (
185
+ subscription_scope = 'own_inbox'
186
+ and event_category in ('inbox.item.delivered', 'inbox.item.updated')
187
+ and board_id is null
188
+ and board_membership_id is null
189
+ )
190
+ or (
191
+ subscription_scope = 'board'
192
+ and event_category in ('board.post.delivered', 'board.post.updated')
193
+ and board_id is not null
194
+ and board_membership_id is not null
195
+ )
196
+ ),
197
+ constraint callback_subscriptions_status_disabled_shape check (
198
+ (subscription_status = 'disabled' and disabled_at is not null)
199
+ or (subscription_status <> 'disabled' and disabled_at is null)
200
+ ),
201
+ constraint callback_subscriptions_board_membership_fkey
202
+ foreign key (board_membership_id, board_id, agent_id)
203
+ references agent_board.board_memberships(id, board_id, agent_id)
204
+ );
205
+
206
+ comment on table agent_board.callback_subscriptions is 'Own-inbox and board-scoped callback intent for one endpoint.';
207
+
208
+ create unique index if not exists idx_agent_board_callback_subscriptions_active_unique
209
+ on agent_board.callback_subscriptions(endpoint_id, agent_id, event_category, coalesce(board_id, '00000000-0000-0000-0000-000000000000'::uuid))
210
+ where subscription_status = 'active';
211
+
212
+ create index if not exists idx_agent_board_callback_subscriptions_board
213
+ on agent_board.callback_subscriptions(board_id, subscription_status, created_at desc)
214
+ where board_id is not null;
215
+
216
+ create table if not exists agent_board.callback_outbox_events (
217
+ id uuid primary key default gen_random_uuid(),
218
+ endpoint_id uuid not null references agent_board.callback_endpoints(id) on delete cascade,
219
+ subscription_id uuid not null references agent_board.callback_subscriptions(id) on delete cascade,
220
+ recipient_agent_id uuid not null references agent_board.agents(id) on delete cascade,
221
+ inbox_item_id uuid references agent_board.inbox_items(id) on delete set null,
222
+ board_id uuid references agent_board.boards(id) on delete cascade,
223
+ event_category text not null check (event_category in ('inbox.item.delivered', 'inbox.item.updated', 'board.post.delivered', 'board.post.updated')),
224
+ delivery_mode text not null check (delivery_mode in ('direct', 'multicast', 'board', 'tasking')),
225
+ source_kind text not null check (source_kind in ('message', 'task', 'delegation')),
226
+ source_message_id uuid references agent_board.messages(id) on delete cascade,
227
+ source_task_id uuid references agent_board.tasks(id) on delete cascade,
228
+ source_delegation_id uuid references agent_board.delegations(id) on delete cascade,
229
+ idempotency_key text not null unique,
230
+ subject text,
231
+ preview text,
232
+ priority text not null default 'medium' check (priority in ('low', 'medium', 'high', 'urgent')),
233
+ fetch_descriptor jsonb not null default '{}'::jsonb,
234
+ outbox_status text not null default 'pending' check (outbox_status in ('pending', 'delivering', 'retryable', 'delivered', 'failed', 'disabled')),
235
+ available_at timestamptz not null default now(),
236
+ locked_at timestamptz,
237
+ delivered_at timestamptz,
238
+ last_error_at timestamptz,
239
+ metadata jsonb not null default '{}'::jsonb,
240
+ created_at timestamptz not null default now(),
241
+ updated_at timestamptz not null default now(),
242
+ constraint callback_outbox_events_source_shape check (
243
+ (source_kind = 'message' and source_message_id is not null and source_task_id is null and source_delegation_id is null)
244
+ or (source_kind = 'task' and source_message_id is null and source_task_id is not null and source_delegation_id is null)
245
+ or (source_kind = 'delegation' and source_message_id is null and source_task_id is null and source_delegation_id is not null)
246
+ ),
247
+ constraint callback_outbox_events_scope_shape check (
248
+ (
249
+ event_category in ('inbox.item.delivered', 'inbox.item.updated')
250
+ and board_id is null
251
+ )
252
+ or (
253
+ event_category in ('board.post.delivered', 'board.post.updated')
254
+ and board_id is not null
255
+ )
256
+ ),
257
+ constraint callback_outbox_events_fetch_descriptor_object check (jsonb_typeof(fetch_descriptor) = 'object')
258
+ );
259
+
260
+ comment on table agent_board.callback_outbox_events is 'Durable callback notification intent written before any external delivery attempt.';
261
+
262
+ create index if not exists idx_agent_board_callback_outbox_events_poll
263
+ on agent_board.callback_outbox_events(outbox_status, available_at, created_at)
264
+ where outbox_status in ('pending', 'retryable');
265
+
266
+ create index if not exists idx_agent_board_callback_outbox_events_retry
267
+ on agent_board.callback_outbox_events(available_at, updated_at desc)
268
+ where outbox_status = 'retryable';
269
+
270
+ create table if not exists agent_board.callback_delivery_attempts (
271
+ id uuid primary key default gen_random_uuid(),
272
+ outbox_event_id uuid not null references agent_board.callback_outbox_events(id) on delete cascade,
273
+ endpoint_id uuid not null references agent_board.callback_endpoints(id) on delete cascade,
274
+ attempt_number integer not null check (attempt_number > 0),
275
+ attempt_outcome text not null check (attempt_outcome in ('delivered', 'retryable_failure', 'terminal_failure', 'endpoint_disabled')),
276
+ http_status integer check (http_status is null or (http_status >= 100 and http_status <= 599)),
277
+ error_summary text,
278
+ started_at timestamptz not null default now(),
279
+ finished_at timestamptz,
280
+ next_retry_at timestamptz,
281
+ metadata jsonb not null default '{}'::jsonb,
282
+ created_at timestamptz not null default now(),
283
+ constraint callback_delivery_attempts_retry_shape check (
284
+ (attempt_outcome = 'retryable_failure' and next_retry_at is not null)
285
+ or (attempt_outcome <> 'retryable_failure' and next_retry_at is null)
286
+ ),
287
+ constraint callback_delivery_attempts_outbox_attempt_unique unique (outbox_event_id, attempt_number)
288
+ );
289
+
290
+ comment on table agent_board.callback_delivery_attempts is 'Append-only callback delivery attempts for retries, successes, and terminal failures.';
291
+
292
+ create index if not exists idx_agent_board_callback_delivery_attempts_retry_schedule
293
+ on agent_board.callback_delivery_attempts(next_retry_at, created_at)
294
+ where attempt_outcome = 'retryable_failure';
295
+
296
+ create index if not exists idx_agent_board_callback_delivery_attempts_outbox
297
+ on agent_board.callback_delivery_attempts(outbox_event_id, attempt_number desc);
298
+
299
+ grant select on agent_board.boards,
300
+ agent_board.board_memberships,
301
+ agent_board.delivery_recipients,
302
+ agent_board.inbox_items,
303
+ agent_board.callback_endpoints,
304
+ agent_board.callback_subscriptions,
305
+ agent_board.callback_outbox_events,
306
+ agent_board.callback_delivery_attempts
307
+ to service_role;
308
+
309
+ alter table agent_board.boards enable row level security;
310
+ alter table agent_board.board_memberships enable row level security;
311
+ alter table agent_board.delivery_recipients enable row level security;
312
+ alter table agent_board.inbox_items enable row level security;
313
+ alter table agent_board.callback_endpoints enable row level security;
314
+ alter table agent_board.callback_subscriptions enable row level security;
315
+ alter table agent_board.callback_outbox_events enable row level security;
316
+ alter table agent_board.callback_delivery_attempts enable row level security;
317
+
318
+ do $$
319
+ declare
320
+ v_table text;
321
+ begin
322
+ foreach v_table in array array[
323
+ 'boards',
324
+ 'board_memberships',
325
+ 'delivery_recipients',
326
+ 'inbox_items',
327
+ 'callback_endpoints',
328
+ 'callback_subscriptions',
329
+ 'callback_outbox_events',
330
+ 'callback_delivery_attempts'
331
+ ]
332
+ loop
333
+ execute format('drop policy if exists %I on agent_board.%I', 'agent_board_service_all_' || v_table, v_table);
334
+ execute format(
335
+ 'create policy %I on agent_board.%I as permissive for all to service_role using (true) with check (true)',
336
+ 'agent_board_service_all_' || v_table,
337
+ v_table
338
+ );
339
+ end loop;
340
+ end;
341
+ $$;