@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,227 @@
1
+ insert into public.app_settings(key, value)
2
+ values
3
+ ('agent_board_monitoring_stale_claim_hours', to_jsonb(24)),
4
+ ('agent_board_monitoring_presence_lag_minutes', to_jsonb(15)),
5
+ ('agent_board_monitoring_callback_retry_threshold', to_jsonb(25)),
6
+ ('agent_board_monitoring_inbox_pending_threshold', to_jsonb(50)),
7
+ ('agent_board_monitoring_table_growth_bytes', to_jsonb(52428800))
8
+ on conflict (key) do nothing;
9
+
10
+ create or replace view agent_board.v_ops_stale_claims
11
+ with (security_invoker = true)
12
+ as
13
+ select
14
+ c.id as claim_id,
15
+ c.task_id,
16
+ c.thread_id,
17
+ c.agent_id,
18
+ a.handle as agent_handle,
19
+ c.resource_kind,
20
+ c.resource_key,
21
+ round(extract(epoch from (now() - c.created_at)) / 3600.0, 2) as stale_hours,
22
+ c.lease_expires_at,
23
+ c.created_at
24
+ from agent_board.claims c
25
+ join agent_board.agents a on a.id = c.agent_id
26
+ where c.status = 'active'
27
+ and c.released_at is null
28
+ and c.lease_expires_at < now();
29
+
30
+ create or replace view agent_board.v_ops_presence_heartbeats
31
+ with (security_invoker = true)
32
+ as
33
+ select
34
+ p.agent_id,
35
+ a.handle as agent_handle,
36
+ p.availability,
37
+ p.last_heartbeat_at,
38
+ p.expires_at,
39
+ round(extract(epoch from (now() - p.last_heartbeat_at)) / 60.0, 2) as heartbeat_lag_minutes,
40
+ (p.expires_at < now()) as is_expired
41
+ from agent_board.presence p
42
+ join agent_board.agents a on a.id = p.agent_id
43
+ where p.expires_at < now();
44
+
45
+ create or replace view agent_board.v_ops_open_escalations
46
+ with (security_invoker = true)
47
+ as
48
+ select
49
+ e.id as escalation_id,
50
+ e.task_id,
51
+ e.thread_id,
52
+ e.severity,
53
+ e.status,
54
+ e.summary,
55
+ e.assigned_agent_id,
56
+ assignee.handle as assigned_agent_handle,
57
+ round(extract(epoch from (now() - e.created_at)) / 3600.0, 2) as age_hours,
58
+ e.created_at
59
+ from agent_board.escalations e
60
+ left join agent_board.agents assignee on assignee.id = e.assigned_agent_id
61
+ where e.status in ('open', 'acknowledged');
62
+
63
+ create or replace view agent_board.v_ops_callback_retry_pressure
64
+ with (security_invoker = true)
65
+ as
66
+ select
67
+ coalesce(vcdh.endpoint_id, '00000000-0000-0000-0000-000000000000'::uuid) as endpoint_id,
68
+ count(*) filter (where vcdh.latest_attempt_outcome = 'retryable_failure')::bigint as retryable_failure_count,
69
+ count(*) filter (where vcdh.latest_outbox_status = 'retryable')::bigint as queued_retry_count,
70
+ max(vcdh.latest_attempt_finished_at) as latest_retry_finished_at,
71
+ max(vcdh.last_failure_at) as endpoint_last_failure_at
72
+ from agent_board.v_callback_delivery_health vcdh
73
+ group by coalesce(vcdh.endpoint_id, '00000000-0000-0000-0000-000000000000'::uuid)
74
+ having count(*) filter (where vcdh.latest_attempt_outcome = 'retryable_failure') > 0
75
+ or count(*) filter (where vcdh.latest_outbox_status = 'retryable') > 0;
76
+
77
+ create or replace view agent_board.v_ops_endpoint_disablement
78
+ with (security_invoker = true)
79
+ as
80
+ select
81
+ ce.id as endpoint_id,
82
+ ce.owner_agent_id,
83
+ owner.handle as owner_agent_handle,
84
+ ce.disabled_reason,
85
+ ce.disabled_at,
86
+ ce.last_failure_at
87
+ from agent_board.callback_endpoints ce
88
+ join agent_board.agents owner on owner.id = ce.owner_agent_id
89
+ where ce.endpoint_status = 'disabled';
90
+
91
+ create or replace view agent_board.v_ops_inbox_backlog
92
+ with (security_invoker = true)
93
+ as
94
+ select
95
+ counts.recipient_agent_id as agent_id,
96
+ a.handle as recipient_handle,
97
+ counts.unread_count,
98
+ counts.pending_action_count,
99
+ counts.total_visible_count,
100
+ counts.latest_inbox_updated_at
101
+ from agent_board.v_agent_inbox_counts counts
102
+ join agent_board.agents a on a.id = counts.recipient_agent_id
103
+ where counts.pending_action_count > 0
104
+ or counts.unread_count > 0;
105
+
106
+ create or replace view agent_board.v_ops_table_growth
107
+ with (security_invoker = true)
108
+ as
109
+ select
110
+ table_name,
111
+ pg_total_relation_size(format('agent_board.%I', table_name)::regclass) as total_bytes
112
+ from (
113
+ values
114
+ ('messages'),
115
+ ('task_events'),
116
+ ('presence'),
117
+ ('callback_outbox_events'),
118
+ ('callback_delivery_attempts'),
119
+ ('inbox_items'),
120
+ ('delivery_recipients'),
121
+ ('board_memberships')
122
+ ) as monitored(table_name);
123
+
124
+ create or replace view agent_board.v_callback_health
125
+ with (security_invoker = true)
126
+ as
127
+ with subscription_counts as (
128
+ select
129
+ cs.endpoint_id,
130
+ count(*) filter (where cs.subscription_status = 'active')::bigint as active_subscription_count,
131
+ count(*) filter (where cs.subscription_status = 'disabled')::bigint as disabled_subscription_count
132
+ from agent_board.callback_subscriptions cs
133
+ group by cs.endpoint_id
134
+ ),
135
+ attempt_counts as (
136
+ select
137
+ cda.endpoint_id,
138
+ count(*) filter (where cda.attempt_outcome = 'delivered')::bigint as delivered_count,
139
+ count(*) filter (where cda.attempt_outcome = 'retryable_failure')::bigint as retryable_failure_count,
140
+ count(*) filter (where cda.attempt_outcome = 'terminal_failure')::bigint as terminal_failure_count,
141
+ count(*) filter (where cda.attempt_outcome = 'endpoint_disabled')::bigint as endpoint_disabled_count,
142
+ max(cda.finished_at) as latest_attempt_finished_at
143
+ from agent_board.callback_delivery_attempts cda
144
+ group by cda.endpoint_id
145
+ ),
146
+ outbox_counts as (
147
+ select
148
+ coe.endpoint_id,
149
+ count(*) filter (where coe.outbox_status = 'retryable')::bigint as queued_retry_count,
150
+ count(*) filter (where coe.outbox_status = 'disabled')::bigint as disabled_outbox_count,
151
+ min(coe.available_at) filter (where coe.outbox_status = 'retryable') as oldest_retry_available_at
152
+ from agent_board.callback_outbox_events coe
153
+ group by coe.endpoint_id
154
+ ),
155
+ latest_outbox as (
156
+ select distinct on (coe.endpoint_id)
157
+ coe.endpoint_id,
158
+ coe.id as latest_outbox_event_id,
159
+ coe.outbox_status as latest_outbox_status,
160
+ coe.delivery_mode as latest_delivery_mode,
161
+ coe.source_kind as latest_source_kind,
162
+ coe.updated_at as latest_outbox_updated_at
163
+ from agent_board.callback_outbox_events coe
164
+ order by coe.endpoint_id, coe.updated_at desc, coe.created_at desc, coe.id desc
165
+ )
166
+ select
167
+ ce.id as endpoint_id,
168
+ ce.owner_agent_id,
169
+ owner.handle as owner_agent_handle,
170
+ ce.endpoint_status,
171
+ ce.signing_key_version,
172
+ ce.disabled_at,
173
+ ce.disabled_reason,
174
+ coalesce(subscription_counts.active_subscription_count, 0::bigint) as active_subscription_count,
175
+ coalesce(subscription_counts.disabled_subscription_count, 0::bigint) as disabled_subscription_count,
176
+ coalesce(attempt_counts.delivered_count, 0::bigint) as delivered_count,
177
+ coalesce(attempt_counts.retryable_failure_count, 0::bigint) as retryable_failure_count,
178
+ coalesce(attempt_counts.terminal_failure_count, 0::bigint) as terminal_failure_count,
179
+ coalesce(attempt_counts.endpoint_disabled_count, 0::bigint) as endpoint_disabled_count,
180
+ coalesce(outbox_counts.queued_retry_count, 0::bigint) as queued_retry_count,
181
+ coalesce(outbox_counts.disabled_outbox_count, 0::bigint) as disabled_outbox_count,
182
+ latest_outbox.latest_outbox_event_id,
183
+ latest_outbox.latest_outbox_status,
184
+ latest_outbox.latest_delivery_mode,
185
+ latest_outbox.latest_source_kind,
186
+ latest_attempt.attempt_number as latest_attempt_number,
187
+ latest_attempt.attempt_outcome as latest_attempt_outcome,
188
+ latest_attempt.http_status as latest_http_status,
189
+ latest_attempt.error_summary as latest_error_summary,
190
+ latest_attempt.next_retry_at as latest_next_retry_at,
191
+ attempt_counts.latest_attempt_finished_at,
192
+ latest_outbox.latest_outbox_updated_at,
193
+ outbox_counts.oldest_retry_available_at,
194
+ ce.last_delivery_at,
195
+ ce.last_failure_at,
196
+ round(extract(epoch from (now() - ce.last_delivery_at)))::bigint as last_delivery_age_seconds,
197
+ round(extract(epoch from (now() - ce.last_failure_at)))::bigint as last_failure_age_seconds,
198
+ round(extract(epoch from (now() - attempt_counts.latest_attempt_finished_at)))::bigint as latest_attempt_age_seconds,
199
+ round(extract(epoch from (now() - outbox_counts.oldest_retry_available_at)))::bigint as oldest_retry_age_seconds
200
+ from agent_board.callback_endpoints ce
201
+ join agent_board.agents owner on owner.id = ce.owner_agent_id
202
+ left join subscription_counts on subscription_counts.endpoint_id = ce.id
203
+ left join attempt_counts on attempt_counts.endpoint_id = ce.id
204
+ left join outbox_counts on outbox_counts.endpoint_id = ce.id
205
+ left join latest_outbox on latest_outbox.endpoint_id = ce.id
206
+ left join lateral (
207
+ select
208
+ cda.attempt_number,
209
+ cda.attempt_outcome,
210
+ cda.http_status,
211
+ cda.error_summary,
212
+ cda.next_retry_at
213
+ from agent_board.callback_delivery_attempts cda
214
+ where cda.outbox_event_id = latest_outbox.latest_outbox_event_id
215
+ order by cda.attempt_number desc
216
+ limit 1
217
+ ) latest_attempt on true;
218
+
219
+ grant select on agent_board.v_ops_stale_claims,
220
+ agent_board.v_ops_presence_heartbeats,
221
+ agent_board.v_ops_open_escalations,
222
+ agent_board.v_ops_callback_retry_pressure,
223
+ agent_board.v_ops_endpoint_disablement,
224
+ agent_board.v_ops_inbox_backlog,
225
+ agent_board.v_ops_table_growth,
226
+ agent_board.v_callback_health
227
+ to authenticated, service_role;
@@ -0,0 +1,150 @@
1
+ -- Fix ambiguous column resolution in agent_board RLS-policy EXISTS subqueries
2
+ -- introduced by 20260512000003_agent_board_auth_rls.sql.
3
+ --
4
+ -- Bug class (deny): a subquery's FROM alias introduces a column (`id`) that
5
+ -- shadows the outer table's `id`. PostgreSQL resolves the unqualified column
6
+ -- to the inner alias, collapsing the join predicate to a tautological
7
+ -- comparison that is essentially never true. Effect: legitimate readers
8
+ -- (joined board members, board-delivery recipients) are denied access.
9
+ -- - agent_board_authenticated_select_messages (line 136):
10
+ -- dr.source_message_id = id -> dr.source_message_id = dr.id
11
+ -- - agent_board_authenticated_select_tasks (line 165):
12
+ -- dr.source_task_id = id -> dr.source_task_id = dr.id
13
+ -- - agent_board_authenticated_select_boards (line 354):
14
+ -- bm.board_id = id -> bm.board_id = bm.id
15
+ --
16
+ -- Bug class (leak): a subquery's FROM alias introduces a column whose name
17
+ -- matches the outer table's column (`board_id` on board_memberships and on
18
+ -- delivery_recipients/inbox_items). The unqualified outer reference resolves
19
+ -- to the inner alias instead, collapsing the predicate to bm.board_id =
20
+ -- bm.board_id (always true for any non-null membership row). Effect: an
21
+ -- authenticated agent who is a joined/subscribed member of ANY board can
22
+ -- read delivery_recipients and inbox_items rows scoped to ANY OTHER board.
23
+ -- - agent_board_authenticated_select_delivery_recipients (line 383):
24
+ -- bm.board_id = board_id -> bm.board_id = bm.board_id
25
+ -- - agent_board_authenticated_select_inbox_items (line 404):
26
+ -- bm.board_id = board_id -> bm.board_id = bm.board_id
27
+ --
28
+ -- Fix: drop and recreate the affected policies, qualifying every outer-table
29
+ -- reference (messages.id, tasks.id, boards.id, delivery_recipients.board_id,
30
+ -- inbox_items.board_id). No schema-shape change; this is a behavior fix to
31
+ -- the policy expressions.
32
+
33
+ drop policy if exists agent_board_authenticated_select_messages on agent_board.messages;
34
+ create policy agent_board_authenticated_select_messages
35
+ on agent_board.messages
36
+ as permissive
37
+ for select
38
+ to authenticated
39
+ using (
40
+ author_agent_id = agent_board.current_agent_id()
41
+ or exists (
42
+ select 1
43
+ from agent_board.delivery_recipients dr
44
+ where dr.source_message_id = messages.id
45
+ and (
46
+ dr.recipient_agent_id = agent_board.current_agent_id()
47
+ or (
48
+ dr.board_id is not null
49
+ and exists (
50
+ select 1
51
+ from agent_board.board_memberships bm
52
+ where bm.board_id = dr.board_id
53
+ and bm.agent_id = agent_board.current_agent_id()
54
+ and bm.membership_state in ('joined', 'subscribed')
55
+ )
56
+ )
57
+ )
58
+ )
59
+ );
60
+
61
+ drop policy if exists agent_board_authenticated_select_tasks on agent_board.tasks;
62
+ create policy agent_board_authenticated_select_tasks
63
+ on agent_board.tasks
64
+ as permissive
65
+ for select
66
+ to authenticated
67
+ using (
68
+ created_by_agent_id = agent_board.current_agent_id()
69
+ or assigned_agent_id = agent_board.current_agent_id()
70
+ or exists (
71
+ select 1
72
+ from agent_board.delivery_recipients dr
73
+ where dr.source_task_id = tasks.id
74
+ and (
75
+ dr.recipient_agent_id = agent_board.current_agent_id()
76
+ or (
77
+ dr.board_id is not null
78
+ and exists (
79
+ select 1
80
+ from agent_board.board_memberships bm
81
+ where bm.board_id = dr.board_id
82
+ and bm.agent_id = agent_board.current_agent_id()
83
+ and bm.membership_state in ('joined', 'subscribed')
84
+ )
85
+ )
86
+ )
87
+ )
88
+ );
89
+
90
+ drop policy if exists agent_board_authenticated_select_boards on agent_board.boards;
91
+ create policy agent_board_authenticated_select_boards
92
+ on agent_board.boards
93
+ as permissive
94
+ for select
95
+ to authenticated
96
+ using (
97
+ created_by_agent_id = agent_board.current_agent_id()
98
+ or exists (
99
+ select 1
100
+ from agent_board.board_memberships bm
101
+ where bm.board_id = boards.id
102
+ and bm.agent_id = agent_board.current_agent_id()
103
+ and bm.membership_state in ('joined', 'subscribed')
104
+ )
105
+ );
106
+
107
+ drop policy if exists agent_board_authenticated_select_delivery_recipients on agent_board.delivery_recipients;
108
+ create policy agent_board_authenticated_select_delivery_recipients
109
+ on agent_board.delivery_recipients
110
+ as permissive
111
+ for select
112
+ to authenticated
113
+ using (
114
+ recipient_agent_id = agent_board.current_agent_id()
115
+ or (
116
+ board_id is not null
117
+ and exists (
118
+ select 1
119
+ from agent_board.board_memberships bm
120
+ where bm.board_id = delivery_recipients.board_id
121
+ and bm.agent_id = agent_board.current_agent_id()
122
+ and bm.membership_state in ('joined', 'subscribed')
123
+ )
124
+ )
125
+ );
126
+
127
+ drop policy if exists agent_board_authenticated_select_inbox_items on agent_board.inbox_items;
128
+ create policy agent_board_authenticated_select_inbox_items
129
+ on agent_board.inbox_items
130
+ as permissive
131
+ for select
132
+ to authenticated
133
+ using (
134
+ recipient_agent_id = agent_board.current_agent_id()
135
+ or (
136
+ board_id is not null
137
+ and visible_in_board_feed = true
138
+ and exists (
139
+ select 1
140
+ from agent_board.board_memberships bm
141
+ where bm.board_id = inbox_items.board_id
142
+ and bm.agent_id = agent_board.current_agent_id()
143
+ and bm.membership_state in ('joined', 'subscribed')
144
+ )
145
+ )
146
+ );
147
+
148
+ insert into agent_board.schema_version (version)
149
+ values (4)
150
+ on conflict ((true)) do update set version = excluded.version;