@agentsdock/server 1.0.7-beta.5

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 (113) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +19 -0
  3. package/README.md +60 -0
  4. package/npm/cli.cjs +289 -0
  5. package/package.json +32 -0
  6. package/server/LICENSE +202 -0
  7. package/server/NOTICE +19 -0
  8. package/server/VERSION +1 -0
  9. package/server/activation_transaction.py +2458 -0
  10. package/server/agent_server.py +93226 -0
  11. package/server/agentsdock_chats.py +1237 -0
  12. package/server/agentsdock_emergency.py +338 -0
  13. package/server/agentsdock_jobs.py +640 -0
  14. package/server/agentsdock_mail.py +325 -0
  15. package/server/agentsdock_publish.py +404 -0
  16. package/server/agentsdock_team.py +793 -0
  17. package/server/agentsdock_team_hub/__init__.py +33 -0
  18. package/server/agentsdock_team_hub/auth.py +710 -0
  19. package/server/agentsdock_team_hub/cli.py +647 -0
  20. package/server/agentsdock_team_hub/database.py +310 -0
  21. package/server/agentsdock_team_hub/mail_hint_streams.py +176 -0
  22. package/server/agentsdock_team_hub/mail_hints.py +201 -0
  23. package/server/agentsdock_team_hub/migrations/0001_identity_auth.sql +690 -0
  24. package/server/agentsdock_team_hub/migrations/0002_teamspace_ledger.sql +1336 -0
  25. package/server/agentsdock_team_hub/migrations/0003_service_runtime.sql +361 -0
  26. package/server/agentsdock_team_hub/migrations/0004_managed_host_binding.sql +22 -0
  27. package/server/agentsdock_team_hub/migrations/0005_tailnet_bootstrap_delegations.sql +63 -0
  28. package/server/agentsdock_team_hub/migrations/0006_team_network_mailbox.sql +423 -0
  29. package/server/agentsdock_team_hub/migrations/0007_local_agent_mail.sql +63 -0
  30. package/server/agentsdock_team_hub/migrations/0008_managed_server_session.sql +73 -0
  31. package/server/agentsdock_team_hub/migrations/0009_team_messages.sql +324 -0
  32. package/server/agentsdock_team_hub/migrations/0010_team_attachment_orphan_reclamation.sql +52 -0
  33. package/server/agentsdock_team_hub/migrations/0011_human_admin_paging.sql +65 -0
  34. package/server/agentsdock_team_hub/migrations/0012_network_content_deletions.sql +69 -0
  35. package/server/agentsdock_team_hub/migrations/0013_team_message_revisions.sql +48 -0
  36. package/server/agentsdock_team_hub/migrations/0014_managed_network_owner.sql +50 -0
  37. package/server/agentsdock_team_hub/migrations/0015_team_message_inbox_dismissals.sql +3 -0
  38. package/server/agentsdock_team_hub/migrations/0016_team_message_all_servers.sql +4 -0
  39. package/server/agentsdock_team_hub/migrations/0017_skill_announcement_deletions.sql +16 -0
  40. package/server/agentsdock_team_hub/migrations/0018_team_mail_subjects.sql +8 -0
  41. package/server/agentsdock_team_hub/migrations/0019_team_mailbox_state.sql +5 -0
  42. package/server/agentsdock_team_hub/migrations/0020_team_mail_arrivals.sql +44 -0
  43. package/server/agentsdock_team_hub/migrations/0021_team_mail_threads.sql +4 -0
  44. package/server/agentsdock_team_hub/migrations/0022_team_bulletin_changes.sql +49 -0
  45. package/server/agentsdock_team_hub/migrations/0023_team_message_search.sql +93 -0
  46. package/server/agentsdock_team_hub/migrations/__init__.py +1 -0
  47. package/server/agentsdock_team_hub/notification_hints.py +306 -0
  48. package/server/agentsdock_team_hub/secure_peer.py +11215 -0
  49. package/server/agentsdock_team_hub/secure_peer_hub.py +1372 -0
  50. package/server/agentsdock_team_hub/security.py +318 -0
  51. package/server/agentsdock_team_hub/service.py +1737 -0
  52. package/server/agentsdock_team_hub/store.py +16276 -0
  53. package/server/chat_mailbox.py +448 -0
  54. package/server/claude_background_reconciliation.py +92 -0
  55. package/server/claude_goals.py +245 -0
  56. package/server/claude_history_provenance.py +137 -0
  57. package/server/claude_history_repair.py +1601 -0
  58. package/server/claude_sdk_client.py +3731 -0
  59. package/server/claude_side_question.py +212 -0
  60. package/server/codex_app_server.py +3030 -0
  61. package/server/codex_auth.py +93 -0
  62. package/server/codex_history_repair.py +1247 -0
  63. package/server/codex_provider.py +1120 -0
  64. package/server/codex_side_question.py +301 -0
  65. package/server/cursor_agent_client.py +654 -0
  66. package/server/cursor_process_guard.py +347 -0
  67. package/server/execution_activation.py +610 -0
  68. package/server/execution_control.py +233 -0
  69. package/server/execution_durability.py +184 -0
  70. package/server/execution_http.py +256 -0
  71. package/server/execution_install.py +792 -0
  72. package/server/execution_legacy_runner.py +406 -0
  73. package/server/execution_maintenance.py +137 -0
  74. package/server/execution_manage.py +615 -0
  75. package/server/execution_ownership.py +83 -0
  76. package/server/execution_preparation.py +362 -0
  77. package/server/execution_recovery.py +845 -0
  78. package/server/execution_recovery_status.py +195 -0
  79. package/server/execution_service.py +387 -0
  80. package/server/execution_transport.py +622 -0
  81. package/server/execution_uninstall.py +406 -0
  82. package/server/execution_update_status.py +49 -0
  83. package/server/install.sh +7074 -0
  84. package/server/interactive_chat_controls.py +153 -0
  85. package/server/interactive_chat_native.py +66 -0
  86. package/server/interactive_chat_projection.py +152 -0
  87. package/server/interactive_chat_runtime.py +132 -0
  88. package/server/interactive_chat_share_routes.py +534 -0
  89. package/server/interactive_chat_share_web.py +12 -0
  90. package/server/interactive_chat_shares.py +238 -0
  91. package/server/provider_commands.py +425 -0
  92. package/server/public_chat_share_routes.py +426 -0
  93. package/server/public_chat_shares.py +759 -0
  94. package/server/public_chat_transcript.py +357 -0
  95. package/server/pyproject.toml +29 -0
  96. package/server/release-public-key.pem +3 -0
  97. package/server/secure_peer_delivery.py +1236 -0
  98. package/server/secure_peer_runtime.py +7694 -0
  99. package/server/shared_chat_video_stream.py +262 -0
  100. package/server/shared_chat_videos.py +155 -0
  101. package/server/side_questions.py +594 -0
  102. package/server/team_hub_host.py +1354 -0
  103. package/server/team_mail_grants.py +136 -0
  104. package/server/team_mail_runtime.py +302 -0
  105. package/server/team_mail_websocket.py +211 -0
  106. package/server/title_generation.py +333 -0
  107. package/server/uninstall.sh +359 -0
  108. package/server/update_handoff.py +237 -0
  109. package/server/update_preparation.py +264 -0
  110. package/server/update_recovery.py +140 -0
  111. package/server/update_runner.py +1819 -0
  112. package/server/uv.lock +1101 -0
  113. package/server/workspace_git.py +545 -0
@@ -0,0 +1,1336 @@
1
+ -- Teamspace collaboration ledger. Messages are passive records. Explicit
2
+ -- dispatch requests live in a separate table and are the only future source
3
+ -- of agent execution admission.
4
+
5
+ CREATE TABLE channels (
6
+ id TEXT PRIMARY KEY,
7
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
8
+ kind TEXT NOT NULL CHECK(kind IN ('direct', 'board', 'announcements')),
9
+ visibility TEXT NOT NULL DEFAULT 'private' CHECK(visibility IN ('private', 'team')),
10
+ slug TEXT CHECK(slug IS NULL OR (
11
+ slug = lower(trim(slug)) AND length(slug) BETWEEN 1 AND 80
12
+ )),
13
+ display_name TEXT CHECK(display_name IS NULL OR length(trim(display_name)) BETWEEN 1 AND 160),
14
+ direct_pair_key BLOB CHECK(direct_pair_key IS NULL OR (
15
+ typeof(direct_pair_key) = 'blob' AND length(direct_pair_key) = 32
16
+ )),
17
+ created_by_principal_id TEXT NOT NULL,
18
+ next_message_sequence INTEGER NOT NULL DEFAULT 1 CHECK(next_message_sequence >= 1),
19
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
20
+ updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
21
+ archived_at INTEGER CHECK(archived_at IS NULL OR archived_at >= created_at),
22
+ FOREIGN KEY(team_id, created_by_principal_id)
23
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
24
+ UNIQUE(team_id, id),
25
+ UNIQUE(team_id, slug),
26
+ UNIQUE(team_id, direct_pair_key),
27
+ CHECK(
28
+ (kind = 'direct' AND direct_pair_key IS NOT NULL AND slug IS NULL)
29
+ OR (kind <> 'direct' AND direct_pair_key IS NULL AND slug IS NOT NULL)
30
+ )
31
+ );
32
+
33
+ CREATE TABLE channel_acl_entries (
34
+ id TEXT PRIMARY KEY,
35
+ team_id TEXT NOT NULL,
36
+ channel_id TEXT NOT NULL,
37
+ subject_kind TEXT NOT NULL CHECK(subject_kind IN ('principal', 'role')),
38
+ subject_principal_id TEXT REFERENCES principals(id) ON DELETE CASCADE,
39
+ subject_role TEXT CHECK(subject_role IN ('owner', 'admin', 'member', 'guest', 'automation')),
40
+ can_read INTEGER NOT NULL CHECK(can_read IN (0, 1)),
41
+ can_post INTEGER NOT NULL CHECK(can_post IN (0, 1)),
42
+ can_manage INTEGER NOT NULL CHECK(can_manage IN (0, 1)),
43
+ can_dispatch INTEGER NOT NULL CHECK(can_dispatch IN (0, 1)),
44
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
45
+ FOREIGN KEY(team_id, channel_id) REFERENCES channels(team_id, id) ON DELETE CASCADE,
46
+ CHECK(
47
+ (subject_kind = 'principal' AND subject_principal_id IS NOT NULL AND subject_role IS NULL)
48
+ OR (subject_kind = 'role' AND subject_principal_id IS NULL AND subject_role IS NOT NULL)
49
+ )
50
+ );
51
+
52
+ CREATE UNIQUE INDEX channel_acl_one_principal_entry
53
+ ON channel_acl_entries(channel_id, subject_principal_id)
54
+ WHERE subject_kind = 'principal';
55
+
56
+ CREATE UNIQUE INDEX channel_acl_one_role_entry
57
+ ON channel_acl_entries(channel_id, subject_role)
58
+ WHERE subject_kind = 'role';
59
+
60
+ CREATE TRIGGER channel_acl_require_team_principal
61
+ BEFORE INSERT ON channel_acl_entries
62
+ FOR EACH ROW WHEN NEW.subject_kind = 'principal' AND NOT EXISTS (
63
+ SELECT 1 FROM principals AS p
64
+ WHERE p.id = NEW.subject_principal_id
65
+ AND p.status = 'active'
66
+ AND (
67
+ p.scope_team_id = NEW.team_id
68
+ OR EXISTS (
69
+ SELECT 1 FROM memberships AS m
70
+ WHERE m.team_id = NEW.team_id
71
+ AND m.principal_id = p.id
72
+ AND m.status = 'active'
73
+ )
74
+ )
75
+ )
76
+ BEGIN
77
+ SELECT RAISE(ABORT, 'channel ACL principal does not belong to this team');
78
+ END;
79
+
80
+ CREATE TRIGGER channel_acl_subject_is_immutable
81
+ BEFORE UPDATE OF id, team_id, channel_id, subject_kind,
82
+ subject_principal_id, subject_role, created_at
83
+ ON channel_acl_entries
84
+ BEGIN
85
+ SELECT RAISE(ABORT, 'channel ACL subject is immutable');
86
+ END;
87
+
88
+ CREATE TABLE channel_participants (
89
+ team_id TEXT NOT NULL,
90
+ channel_id TEXT NOT NULL,
91
+ principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
92
+ participant_role TEXT NOT NULL DEFAULT 'member' CHECK(participant_role IN ('member', 'manager')),
93
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'left', 'removed')),
94
+ last_read_sequence INTEGER NOT NULL DEFAULT 0 CHECK(last_read_sequence >= 0),
95
+ joined_at INTEGER NOT NULL CHECK(joined_at >= 0),
96
+ updated_at INTEGER NOT NULL CHECK(updated_at >= joined_at),
97
+ PRIMARY KEY(channel_id, principal_id),
98
+ FOREIGN KEY(team_id, channel_id) REFERENCES channels(team_id, id) ON DELETE CASCADE,
99
+ UNIQUE(team_id, channel_id, principal_id)
100
+ );
101
+
102
+ CREATE TRIGGER channel_participants_require_team_principal
103
+ BEFORE INSERT ON channel_participants
104
+ FOR EACH ROW WHEN NOT EXISTS (
105
+ SELECT 1 FROM principals AS p
106
+ WHERE p.id = NEW.principal_id
107
+ AND p.status = 'active'
108
+ AND (
109
+ p.scope_team_id = NEW.team_id
110
+ OR (
111
+ p.scope_team_id IS NULL
112
+ AND EXISTS (
113
+ SELECT 1 FROM memberships AS m
114
+ WHERE m.team_id = NEW.team_id
115
+ AND m.principal_id = p.id
116
+ AND m.status = 'active'
117
+ )
118
+ )
119
+ )
120
+ )
121
+ BEGIN
122
+ SELECT RAISE(ABORT, 'channel participant does not belong to this team');
123
+ END;
124
+
125
+ CREATE TRIGGER channel_participant_identity_is_immutable
126
+ BEFORE UPDATE OF team_id, channel_id, principal_id, joined_at
127
+ ON channel_participants
128
+ BEGIN
129
+ SELECT RAISE(ABORT, 'channel participant identity is immutable');
130
+ END;
131
+
132
+ CREATE TABLE messages (
133
+ id TEXT PRIMARY KEY,
134
+ team_id TEXT NOT NULL,
135
+ channel_id TEXT NOT NULL,
136
+ channel_sequence INTEGER NOT NULL CHECK(channel_sequence >= 1),
137
+ kind TEXT NOT NULL CHECK(kind IN ('post', 'announcement', 'system')),
138
+ thread_root_message_id TEXT,
139
+ parent_message_id TEXT,
140
+ author_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
141
+ acting_human_principal_id TEXT,
142
+ provenance_node_id TEXT,
143
+ provenance_agent_id TEXT,
144
+ provenance_chat_id TEXT,
145
+ provenance_run_id TEXT,
146
+ body_format TEXT NOT NULL DEFAULT 'markdown' CHECK(body_format IN ('plain', 'markdown')),
147
+ body TEXT NOT NULL CHECK(length(body) BETWEEN 1 AND 65536),
148
+ idempotency_key BLOB NOT NULL
149
+ CHECK(typeof(idempotency_key) = 'blob' AND length(idempotency_key) = 32),
150
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
151
+ edited_at INTEGER CHECK(edited_at IS NULL OR edited_at >= created_at),
152
+ deleted_at INTEGER CHECK(deleted_at IS NULL OR deleted_at >= created_at),
153
+ expires_at INTEGER CHECK(expires_at IS NULL OR expires_at > created_at),
154
+ FOREIGN KEY(team_id, channel_id) REFERENCES channels(team_id, id) ON DELETE CASCADE,
155
+ FOREIGN KEY(team_id, channel_id, thread_root_message_id)
156
+ REFERENCES messages(team_id, channel_id, id) ON DELETE RESTRICT,
157
+ FOREIGN KEY(team_id, channel_id, parent_message_id)
158
+ REFERENCES messages(team_id, channel_id, id) ON DELETE RESTRICT,
159
+ FOREIGN KEY(team_id, acting_human_principal_id)
160
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
161
+ FOREIGN KEY(team_id, provenance_node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
162
+ FOREIGN KEY(team_id, provenance_agent_id) REFERENCES agents(team_id, id) ON DELETE RESTRICT,
163
+ FOREIGN KEY(team_id, provenance_chat_id) REFERENCES chats(team_id, id) ON DELETE RESTRICT,
164
+ FOREIGN KEY(team_id, provenance_run_id) REFERENCES runs(team_id, id) ON DELETE RESTRICT,
165
+ UNIQUE(channel_id, channel_sequence),
166
+ UNIQUE(team_id, idempotency_key),
167
+ UNIQUE(team_id, id),
168
+ UNIQUE(team_id, channel_id, id),
169
+ CHECK(parent_message_id IS NULL OR thread_root_message_id IS NOT NULL),
170
+ CHECK(parent_message_id IS NULL OR parent_message_id <> id),
171
+ CHECK(thread_root_message_id IS NULL OR thread_root_message_id <> id),
172
+ CHECK(
173
+ (
174
+ acting_human_principal_id IS NULL
175
+ AND provenance_agent_id IS NULL
176
+ AND provenance_node_id IS NULL
177
+ AND provenance_chat_id IS NULL
178
+ AND provenance_run_id IS NULL
179
+ )
180
+ OR (
181
+ acting_human_principal_id IS NOT NULL
182
+ AND provenance_agent_id IS NOT NULL
183
+ AND provenance_node_id IS NOT NULL
184
+ AND provenance_chat_id IS NOT NULL
185
+ AND provenance_run_id IS NOT NULL
186
+ )
187
+ )
188
+ );
189
+
190
+ CREATE TRIGGER messages_require_team_author
191
+ BEFORE INSERT ON messages
192
+ FOR EACH ROW WHEN NOT EXISTS (
193
+ SELECT 1 FROM principals AS p
194
+ WHERE p.id = NEW.author_principal_id
195
+ AND p.kind IN ('human', 'service', 'agent')
196
+ AND p.status = 'active'
197
+ AND (
198
+ p.scope_team_id = NEW.team_id
199
+ OR (
200
+ p.scope_team_id IS NULL
201
+ AND EXISTS (
202
+ SELECT 1 FROM memberships AS m
203
+ WHERE m.team_id = NEW.team_id
204
+ AND m.principal_id = p.id
205
+ AND m.status = 'active'
206
+ )
207
+ )
208
+ )
209
+ )
210
+ BEGIN
211
+ SELECT RAISE(ABORT, 'message author does not belong to this team');
212
+ END;
213
+
214
+ CREATE TRIGGER messages_reject_false_agent_provenance
215
+ BEFORE INSERT ON messages
216
+ FOR EACH ROW WHEN NEW.provenance_agent_id IS NOT NULL AND COALESCE(
217
+ (SELECT kind FROM principals WHERE id = NEW.author_principal_id), ''
218
+ ) <> 'agent'
219
+ BEGIN
220
+ SELECT RAISE(ABORT, 'only an agent author may claim agent provenance');
221
+ END;
222
+
223
+ CREATE TRIGGER messages_require_agent_provenance
224
+ BEFORE INSERT ON messages
225
+ FOR EACH ROW WHEN COALESCE(
226
+ (SELECT kind FROM principals WHERE id = NEW.author_principal_id), ''
227
+ ) = 'agent' AND NOT EXISTS (
228
+ SELECT 1
229
+ FROM agents AS a
230
+ JOIN chats AS c ON c.team_id = a.team_id AND c.id = NEW.provenance_chat_id
231
+ JOIN runs AS r ON r.team_id = a.team_id AND r.id = NEW.provenance_run_id
232
+ WHERE a.team_id = NEW.team_id
233
+ AND a.id = NEW.provenance_agent_id
234
+ AND a.principal_id = NEW.author_principal_id
235
+ AND a.node_id = NEW.provenance_node_id
236
+ AND c.node_id = a.node_id
237
+ AND c.agent_id = a.id
238
+ AND r.node_id = a.node_id
239
+ AND r.agent_id = a.id
240
+ AND r.chat_id = c.id
241
+ AND r.acting_human_principal_id = NEW.acting_human_principal_id
242
+ )
243
+ BEGIN
244
+ SELECT RAISE(ABORT, 'agent-authored message requires exact node, agent, chat, and run provenance');
245
+ END;
246
+
247
+ CREATE TRIGGER message_authority_is_immutable
248
+ BEFORE UPDATE OF id, team_id, channel_id, channel_sequence, kind,
249
+ thread_root_message_id, parent_message_id, author_principal_id,
250
+ acting_human_principal_id, provenance_node_id, provenance_agent_id,
251
+ provenance_chat_id, provenance_run_id, idempotency_key, created_at
252
+ ON messages
253
+ BEGIN
254
+ SELECT RAISE(ABORT, 'message routing and provenance are immutable');
255
+ END;
256
+
257
+ CREATE TABLE message_recipients (
258
+ id TEXT PRIMARY KEY,
259
+ team_id TEXT NOT NULL,
260
+ message_id TEXT NOT NULL,
261
+ recipient_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
262
+ reason TEXT NOT NULL CHECK(reason IN ('direct', 'mention', 'subscription', 'assignment')),
263
+ delivery_key BLOB NOT NULL UNIQUE
264
+ CHECK(typeof(delivery_key) = 'blob' AND length(delivery_key) = 32),
265
+ state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending', 'available', 'delivered', 'expired')),
266
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
267
+ expires_at INTEGER CHECK(expires_at IS NULL OR expires_at > created_at),
268
+ FOREIGN KEY(team_id, message_id) REFERENCES messages(team_id, id) ON DELETE CASCADE,
269
+ UNIQUE(message_id, recipient_principal_id, reason),
270
+ UNIQUE(team_id, id)
271
+ );
272
+
273
+ CREATE TRIGGER message_recipients_require_team_principal
274
+ BEFORE INSERT ON message_recipients
275
+ FOR EACH ROW WHEN NOT EXISTS (
276
+ SELECT 1 FROM principals AS p
277
+ WHERE p.id = NEW.recipient_principal_id
278
+ AND p.status = 'active'
279
+ AND (
280
+ p.scope_team_id = NEW.team_id
281
+ OR EXISTS (
282
+ SELECT 1 FROM memberships AS m
283
+ WHERE m.team_id = NEW.team_id
284
+ AND m.principal_id = p.id
285
+ AND m.status = 'active'
286
+ )
287
+ )
288
+ )
289
+ BEGIN
290
+ SELECT RAISE(ABORT, 'message recipient does not belong to this team');
291
+ END;
292
+
293
+ CREATE TRIGGER message_recipient_identity_is_immutable
294
+ BEFORE UPDATE OF id, team_id, message_id, recipient_principal_id,
295
+ reason, delivery_key, created_at
296
+ ON message_recipients
297
+ BEGIN
298
+ SELECT RAISE(ABORT, 'message recipient identity is immutable');
299
+ END;
300
+
301
+ CREATE TABLE message_receipts (
302
+ id TEXT PRIMARY KEY,
303
+ team_id TEXT NOT NULL,
304
+ recipient_id TEXT NOT NULL UNIQUE,
305
+ delivered_at INTEGER CHECK(delivered_at IS NULL OR delivered_at >= 0),
306
+ read_at INTEGER CHECK(read_at IS NULL OR read_at >= 0),
307
+ acknowledged_at INTEGER CHECK(acknowledged_at IS NULL OR acknowledged_at >= 0),
308
+ FOREIGN KEY(team_id, recipient_id) REFERENCES message_recipients(team_id, id) ON DELETE CASCADE,
309
+ CHECK(read_at IS NULL OR delivered_at IS NOT NULL),
310
+ CHECK(acknowledged_at IS NULL OR delivered_at IS NOT NULL)
311
+ );
312
+
313
+ CREATE TABLE message_mentions (
314
+ team_id TEXT NOT NULL,
315
+ message_id TEXT NOT NULL,
316
+ principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
317
+ start_offset INTEGER NOT NULL CHECK(start_offset >= 0),
318
+ end_offset INTEGER NOT NULL CHECK(end_offset > start_offset),
319
+ PRIMARY KEY(message_id, principal_id, start_offset),
320
+ FOREIGN KEY(team_id, message_id) REFERENCES messages(team_id, id) ON DELETE CASCADE
321
+ );
322
+
323
+ CREATE TRIGGER message_mentions_require_team_principal
324
+ BEFORE INSERT ON message_mentions
325
+ FOR EACH ROW WHEN NOT EXISTS (
326
+ SELECT 1 FROM principals AS p
327
+ WHERE p.id = NEW.principal_id
328
+ AND p.status = 'active'
329
+ AND (
330
+ p.scope_team_id = NEW.team_id
331
+ OR EXISTS (
332
+ SELECT 1 FROM memberships AS m
333
+ WHERE m.team_id = NEW.team_id
334
+ AND m.principal_id = p.id
335
+ AND m.status = 'active'
336
+ )
337
+ )
338
+ )
339
+ BEGIN
340
+ SELECT RAISE(ABORT, 'mentioned principal does not belong to this team');
341
+ END;
342
+
343
+ CREATE TRIGGER message_mentions_are_immutable
344
+ BEFORE UPDATE ON message_mentions
345
+ BEGIN
346
+ SELECT RAISE(ABORT, 'message mentions are immutable');
347
+ END;
348
+
349
+ CREATE TABLE tags (
350
+ id TEXT PRIMARY KEY,
351
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
352
+ name TEXT NOT NULL CHECK(name = lower(trim(name))) CHECK(length(name) BETWEEN 1 AND 64),
353
+ color TEXT CHECK(color IS NULL OR length(color) BETWEEN 1 AND 32),
354
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
355
+ UNIQUE(team_id, name),
356
+ UNIQUE(team_id, id)
357
+ );
358
+
359
+ CREATE TABLE message_tags (
360
+ team_id TEXT NOT NULL,
361
+ message_id TEXT NOT NULL,
362
+ tag_id TEXT NOT NULL,
363
+ added_by_principal_id TEXT NOT NULL,
364
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
365
+ PRIMARY KEY(message_id, tag_id),
366
+ FOREIGN KEY(team_id, message_id) REFERENCES messages(team_id, id) ON DELETE CASCADE,
367
+ FOREIGN KEY(team_id, tag_id) REFERENCES tags(team_id, id) ON DELETE CASCADE,
368
+ FOREIGN KEY(team_id, added_by_principal_id)
369
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT
370
+ );
371
+
372
+ CREATE TABLE pins (
373
+ id TEXT PRIMARY KEY,
374
+ team_id TEXT NOT NULL,
375
+ channel_id TEXT NOT NULL,
376
+ message_id TEXT NOT NULL,
377
+ pinned_by_principal_id TEXT NOT NULL,
378
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
379
+ FOREIGN KEY(team_id, channel_id) REFERENCES channels(team_id, id) ON DELETE CASCADE,
380
+ FOREIGN KEY(team_id, channel_id, message_id)
381
+ REFERENCES messages(team_id, channel_id, id) ON DELETE CASCADE,
382
+ FOREIGN KEY(team_id, pinned_by_principal_id)
383
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
384
+ UNIQUE(channel_id, message_id)
385
+ );
386
+
387
+ CREATE TABLE assignments (
388
+ id TEXT PRIMARY KEY,
389
+ team_id TEXT NOT NULL,
390
+ message_id TEXT NOT NULL,
391
+ assignee_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
392
+ assigned_by_principal_id TEXT NOT NULL,
393
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'in_progress', 'done', 'cancelled')),
394
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
395
+ updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
396
+ FOREIGN KEY(team_id, message_id) REFERENCES messages(team_id, id) ON DELETE CASCADE,
397
+ FOREIGN KEY(team_id, assigned_by_principal_id)
398
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
399
+ UNIQUE(message_id, assignee_principal_id)
400
+ );
401
+
402
+ CREATE TRIGGER assignments_require_team_principal
403
+ BEFORE INSERT ON assignments
404
+ FOR EACH ROW WHEN NOT EXISTS (
405
+ SELECT 1 FROM principals AS p
406
+ WHERE p.id = NEW.assignee_principal_id
407
+ AND p.status = 'active'
408
+ AND (
409
+ p.scope_team_id = NEW.team_id
410
+ OR EXISTS (
411
+ SELECT 1 FROM memberships AS m
412
+ WHERE m.team_id = NEW.team_id
413
+ AND m.principal_id = p.id
414
+ AND m.status = 'active'
415
+ )
416
+ )
417
+ )
418
+ BEGIN
419
+ SELECT RAISE(ABORT, 'assignee does not belong to this team');
420
+ END;
421
+
422
+ CREATE TRIGGER assignment_identity_is_immutable
423
+ BEFORE UPDATE OF id, team_id, message_id, assignee_principal_id,
424
+ assigned_by_principal_id, created_at
425
+ ON assignments
426
+ BEGIN
427
+ SELECT RAISE(ABORT, 'assignment identity is immutable');
428
+ END;
429
+
430
+ CREATE TABLE dispatch_requests (
431
+ queue_ordinal INTEGER PRIMARY KEY AUTOINCREMENT,
432
+ id TEXT NOT NULL UNIQUE,
433
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
434
+ requester_kind TEXT NOT NULL DEFAULT 'human' CHECK(requester_kind IN ('human', 'agent')),
435
+ requested_by_human_principal_id TEXT NOT NULL,
436
+ requesting_node_id TEXT,
437
+ requesting_agent_id TEXT,
438
+ requesting_chat_id TEXT,
439
+ requesting_run_id TEXT,
440
+ authorization_capability_id TEXT,
441
+ source_message_id TEXT,
442
+ target_kind TEXT NOT NULL CHECK(target_kind IN ('agent', 'chat')),
443
+ target_node_id TEXT NOT NULL,
444
+ target_agent_id TEXT,
445
+ target_chat_id TEXT,
446
+ request_text TEXT NOT NULL CHECK(length(request_text) BETWEEN 1 AND 65536),
447
+ idempotency_key BLOB NOT NULL
448
+ CHECK(typeof(idempotency_key) = 'blob' AND length(idempotency_key) = 32),
449
+ status TEXT NOT NULL DEFAULT 'registered'
450
+ CHECK(status IN ('registered', 'queued', 'running', 'succeeded', 'failed', 'cancelled', 'expired')),
451
+ target_run_id TEXT,
452
+ causation_dispatch_id TEXT,
453
+ root_dispatch_id TEXT,
454
+ hop_count INTEGER NOT NULL DEFAULT 0 CHECK(hop_count BETWEEN 0 AND 4),
455
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
456
+ updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
457
+ expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
458
+ terminal_at INTEGER CHECK(terminal_at IS NULL OR terminal_at >= created_at),
459
+ FOREIGN KEY(team_id, requested_by_human_principal_id)
460
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
461
+ FOREIGN KEY(team_id, requesting_node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
462
+ FOREIGN KEY(team_id, requesting_agent_id) REFERENCES agents(team_id, id) ON DELETE RESTRICT,
463
+ FOREIGN KEY(team_id, requesting_chat_id) REFERENCES chats(team_id, id) ON DELETE RESTRICT,
464
+ FOREIGN KEY(team_id, requesting_run_id) REFERENCES runs(team_id, id) ON DELETE RESTRICT,
465
+ FOREIGN KEY(team_id, authorization_capability_id)
466
+ REFERENCES turn_capabilities(team_id, id) ON DELETE RESTRICT,
467
+ FOREIGN KEY(team_id, source_message_id) REFERENCES messages(team_id, id) ON DELETE RESTRICT,
468
+ FOREIGN KEY(team_id, target_node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
469
+ FOREIGN KEY(team_id, target_node_id, target_agent_id)
470
+ REFERENCES agents(team_id, node_id, id) ON DELETE RESTRICT,
471
+ FOREIGN KEY(team_id, target_node_id, target_chat_id)
472
+ REFERENCES chats(team_id, node_id, id) ON DELETE RESTRICT,
473
+ FOREIGN KEY(team_id, target_run_id) REFERENCES runs(team_id, id) ON DELETE RESTRICT,
474
+ FOREIGN KEY(team_id, causation_dispatch_id)
475
+ REFERENCES dispatch_requests(team_id, id) ON DELETE RESTRICT,
476
+ FOREIGN KEY(team_id, root_dispatch_id)
477
+ REFERENCES dispatch_requests(team_id, id) ON DELETE RESTRICT,
478
+ UNIQUE(team_id, idempotency_key),
479
+ UNIQUE(team_id, id),
480
+ CHECK(
481
+ (
482
+ requester_kind = 'human'
483
+ AND requesting_node_id IS NULL
484
+ AND requesting_agent_id IS NULL
485
+ AND requesting_chat_id IS NULL
486
+ AND requesting_run_id IS NULL
487
+ AND authorization_capability_id IS NULL
488
+ )
489
+ OR (
490
+ requester_kind = 'agent'
491
+ AND requesting_node_id IS NOT NULL
492
+ AND requesting_agent_id IS NOT NULL
493
+ AND requesting_chat_id IS NOT NULL
494
+ AND requesting_run_id IS NOT NULL
495
+ AND authorization_capability_id IS NOT NULL
496
+ )
497
+ ),
498
+ CHECK(requester_kind = 'human' OR causation_dispatch_id IS NOT NULL),
499
+ CHECK(
500
+ (target_kind = 'agent' AND target_agent_id IS NOT NULL AND target_chat_id IS NULL)
501
+ OR (
502
+ target_kind = 'chat'
503
+ AND target_agent_id IS NULL
504
+ AND target_chat_id IS NOT NULL
505
+ )
506
+ ),
507
+ CHECK(causation_dispatch_id IS NULL OR causation_dispatch_id <> id),
508
+ CHECK(root_dispatch_id IS NULL OR root_dispatch_id <> id),
509
+ CHECK(
510
+ (
511
+ causation_dispatch_id IS NULL
512
+ AND root_dispatch_id IS NULL
513
+ AND hop_count = 0
514
+ )
515
+ OR (
516
+ causation_dispatch_id IS NOT NULL
517
+ AND root_dispatch_id IS NOT NULL
518
+ AND hop_count BETWEEN 1 AND 4
519
+ )
520
+ )
521
+ );
522
+
523
+ CREATE TRIGGER dispatch_requires_active_human_requester
524
+ BEFORE INSERT ON dispatch_requests
525
+ FOR EACH ROW WHEN NOT EXISTS (
526
+ SELECT 1
527
+ FROM memberships AS m
528
+ JOIN principals AS p ON p.id = m.principal_id
529
+ WHERE m.team_id = NEW.team_id
530
+ AND m.principal_id = NEW.requested_by_human_principal_id
531
+ AND m.status = 'active'
532
+ AND p.kind = 'human'
533
+ AND p.status = 'active'
534
+ )
535
+ BEGIN
536
+ SELECT RAISE(ABORT, 'dispatch requires an active human requester');
537
+ END;
538
+
539
+ CREATE TRIGGER dispatch_agent_request_requires_exact_provenance
540
+ BEFORE INSERT ON dispatch_requests
541
+ FOR EACH ROW WHEN NEW.requester_kind = 'agent' AND NOT EXISTS (
542
+ SELECT 1
543
+ FROM agents AS a
544
+ JOIN chats AS c
545
+ ON c.team_id = a.team_id
546
+ AND c.node_id = a.node_id
547
+ AND c.agent_id = a.id
548
+ AND c.id = NEW.requesting_chat_id
549
+ JOIN runs AS r
550
+ ON r.team_id = a.team_id
551
+ AND r.node_id = a.node_id
552
+ AND r.agent_id = a.id
553
+ AND r.chat_id = c.id
554
+ AND r.id = NEW.requesting_run_id
555
+ JOIN turn_capabilities AS capability
556
+ ON capability.team_id = r.team_id
557
+ AND capability.issued_to_run_id = r.id
558
+ AND capability.id = NEW.authorization_capability_id
559
+ WHERE a.team_id = NEW.team_id
560
+ AND a.id = NEW.requesting_agent_id
561
+ AND a.node_id = NEW.requesting_node_id
562
+ AND r.acting_human_principal_id = NEW.requested_by_human_principal_id
563
+ AND capability.issued_by_principal_id = NEW.requested_by_human_principal_id
564
+ AND capability.action = 'dispatch'
565
+ AND capability.resource_type = NEW.target_kind
566
+ AND capability.resource_id = CASE NEW.target_kind
567
+ WHEN 'agent' THEN NEW.target_agent_id
568
+ ELSE NEW.target_chat_id
569
+ END
570
+ )
571
+ BEGIN
572
+ SELECT RAISE(ABORT, 'agent dispatch requires exact delegated provenance');
573
+ END;
574
+
575
+ CREATE TRIGGER dispatch_target_run_must_match
576
+ BEFORE INSERT ON dispatch_requests
577
+ FOR EACH ROW WHEN NEW.target_run_id IS NOT NULL AND NOT EXISTS (
578
+ SELECT 1 FROM runs AS r
579
+ WHERE r.team_id = NEW.team_id
580
+ AND r.id = NEW.target_run_id
581
+ AND r.node_id = NEW.target_node_id
582
+ AND (
583
+ (NEW.target_kind = 'agent' AND r.agent_id = NEW.target_agent_id)
584
+ OR (NEW.target_kind = 'chat' AND r.chat_id = NEW.target_chat_id)
585
+ )
586
+ )
587
+ BEGIN
588
+ SELECT RAISE(ABORT, 'dispatch target run does not match its exact target');
589
+ END;
590
+
591
+ CREATE TRIGGER dispatch_target_run_update_must_match
592
+ BEFORE UPDATE OF target_run_id ON dispatch_requests
593
+ FOR EACH ROW WHEN NEW.target_run_id IS NOT NULL AND NOT EXISTS (
594
+ SELECT 1 FROM runs AS r
595
+ WHERE r.team_id = NEW.team_id
596
+ AND r.id = NEW.target_run_id
597
+ AND r.node_id = NEW.target_node_id
598
+ AND (
599
+ (NEW.target_kind = 'agent' AND r.agent_id = NEW.target_agent_id)
600
+ OR (NEW.target_kind = 'chat' AND r.chat_id = NEW.target_chat_id)
601
+ )
602
+ )
603
+ BEGIN
604
+ SELECT RAISE(ABORT, 'dispatch target run does not match its exact target');
605
+ END;
606
+
607
+ CREATE TRIGGER dispatch_target_run_is_one_way
608
+ BEFORE UPDATE OF target_run_id ON dispatch_requests
609
+ FOR EACH ROW WHEN OLD.target_run_id IS NOT NULL AND NEW.target_run_id IS NOT OLD.target_run_id
610
+ BEGIN
611
+ SELECT RAISE(ABORT, 'dispatch target run binding is one-way');
612
+ END;
613
+
614
+ CREATE TRIGGER dispatch_causation_must_be_bounded
615
+ BEFORE INSERT ON dispatch_requests
616
+ FOR EACH ROW WHEN NEW.causation_dispatch_id IS NOT NULL AND NOT EXISTS (
617
+ SELECT 1 FROM dispatch_requests AS parent
618
+ WHERE parent.team_id = NEW.team_id
619
+ AND parent.id = NEW.causation_dispatch_id
620
+ AND NEW.root_dispatch_id = COALESCE(parent.root_dispatch_id, parent.id)
621
+ AND NEW.hop_count = parent.hop_count + 1
622
+ AND parent.hop_count < 4
623
+ AND (
624
+ NEW.requester_kind = 'human'
625
+ OR parent.target_run_id = NEW.requesting_run_id
626
+ )
627
+ )
628
+ BEGIN
629
+ SELECT RAISE(ABORT, 'dispatch causation must extend its bounded team-local chain');
630
+ END;
631
+
632
+ CREATE TRIGGER dispatch_authority_is_immutable
633
+ BEFORE UPDATE OF queue_ordinal, id, team_id, requester_kind,
634
+ requested_by_human_principal_id,
635
+ requesting_node_id, requesting_agent_id, requesting_chat_id,
636
+ requesting_run_id, authorization_capability_id,
637
+ source_message_id, target_kind, target_node_id, target_agent_id,
638
+ target_chat_id, request_text, idempotency_key, causation_dispatch_id,
639
+ root_dispatch_id, hop_count, created_at, expires_at
640
+ ON dispatch_requests
641
+ BEGIN
642
+ SELECT RAISE(ABORT, 'dispatch authority is immutable');
643
+ END;
644
+
645
+ CREATE INDEX dispatch_requests_target_fifo
646
+ ON dispatch_requests(team_id, target_node_id, target_chat_id, queue_ordinal)
647
+ WHERE status IN ('registered', 'queued');
648
+
649
+ CREATE TABLE subscriptions (
650
+ id TEXT PRIMARY KEY,
651
+ team_id TEXT NOT NULL,
652
+ subscriber_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE CASCADE,
653
+ channel_id TEXT NOT NULL,
654
+ level TEXT NOT NULL CHECK(level IN ('mentions', 'all')),
655
+ delivery_mode TEXT NOT NULL DEFAULT 'inbox' CHECK(delivery_mode = 'inbox'),
656
+ wake_mode TEXT NOT NULL DEFAULT 'never' CHECK(wake_mode = 'never'),
657
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'paused', 'revoked')),
658
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
659
+ updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
660
+ FOREIGN KEY(team_id, channel_id) REFERENCES channels(team_id, id) ON DELETE CASCADE,
661
+ UNIQUE(channel_id, subscriber_principal_id),
662
+ UNIQUE(team_id, id)
663
+ );
664
+
665
+ CREATE TRIGGER subscriptions_require_team_principal
666
+ BEFORE INSERT ON subscriptions
667
+ FOR EACH ROW WHEN NOT EXISTS (
668
+ SELECT 1 FROM principals AS p
669
+ WHERE p.id = NEW.subscriber_principal_id
670
+ AND p.status = 'active'
671
+ AND (
672
+ p.scope_team_id = NEW.team_id
673
+ OR EXISTS (
674
+ SELECT 1 FROM memberships AS m
675
+ WHERE m.team_id = NEW.team_id
676
+ AND m.principal_id = p.id
677
+ AND m.status = 'active'
678
+ )
679
+ )
680
+ )
681
+ BEGIN
682
+ SELECT RAISE(ABORT, 'subscriber does not belong to this team');
683
+ END;
684
+
685
+ CREATE TRIGGER subscription_identity_is_immutable
686
+ BEFORE UPDATE OF id, team_id, subscriber_principal_id, channel_id,
687
+ delivery_mode, wake_mode, created_at
688
+ ON subscriptions
689
+ BEGIN
690
+ SELECT RAISE(ABORT, 'subscription identity is immutable');
691
+ END;
692
+
693
+ CREATE TABLE artifacts (
694
+ id TEXT PRIMARY KEY,
695
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
696
+ kind TEXT NOT NULL CHECK(kind IN ('attachment', 'runbook', 'skill', 'diff', 'signature')),
697
+ display_name TEXT NOT NULL CHECK(length(trim(display_name)) BETWEEN 1 AND 240),
698
+ created_by_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
699
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
700
+ retention_until INTEGER CHECK(retention_until IS NULL OR retention_until > created_at),
701
+ UNIQUE(team_id, id)
702
+ );
703
+
704
+ CREATE TRIGGER artifacts_require_team_creator
705
+ BEFORE INSERT ON artifacts
706
+ FOR EACH ROW WHEN NOT EXISTS (
707
+ SELECT 1 FROM principals AS p
708
+ WHERE p.id = NEW.created_by_principal_id
709
+ AND p.status = 'active'
710
+ AND p.kind IN ('human', 'service', 'agent')
711
+ AND (
712
+ p.scope_team_id = NEW.team_id
713
+ OR EXISTS (
714
+ SELECT 1 FROM memberships AS m
715
+ WHERE m.team_id = NEW.team_id
716
+ AND m.principal_id = p.id
717
+ AND m.status = 'active'
718
+ )
719
+ )
720
+ )
721
+ BEGIN
722
+ SELECT RAISE(ABORT, 'artifact creator does not belong to this team');
723
+ END;
724
+
725
+ CREATE TRIGGER artifact_identity_is_immutable
726
+ BEFORE UPDATE OF id, team_id, kind, created_by_principal_id, created_at
727
+ ON artifacts
728
+ BEGIN
729
+ SELECT RAISE(ABORT, 'artifact identity is immutable');
730
+ END;
731
+
732
+ CREATE TABLE artifact_versions (
733
+ id TEXT PRIMARY KEY,
734
+ team_id TEXT NOT NULL,
735
+ artifact_id TEXT NOT NULL,
736
+ version INTEGER NOT NULL CHECK(version >= 1),
737
+ media_type TEXT NOT NULL CHECK(length(trim(media_type)) BETWEEN 3 AND 160),
738
+ byte_size INTEGER NOT NULL CHECK(byte_size BETWEEN 0 AND 10485760),
739
+ sha256 BLOB NOT NULL CHECK(typeof(sha256) = 'blob' AND length(sha256) = 32),
740
+ storage_key TEXT NOT NULL UNIQUE CHECK(length(storage_key) BETWEEN 8 AND 512),
741
+ signature_artifact_version_id TEXT,
742
+ scan_state TEXT NOT NULL DEFAULT 'pending' CHECK(scan_state IN ('pending', 'clean', 'rejected')),
743
+ created_by_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
744
+ acting_human_principal_id TEXT,
745
+ provenance_node_id TEXT,
746
+ provenance_agent_id TEXT,
747
+ provenance_chat_id TEXT,
748
+ provenance_run_id TEXT,
749
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
750
+ FOREIGN KEY(team_id, artifact_id) REFERENCES artifacts(team_id, id) ON DELETE CASCADE,
751
+ FOREIGN KEY(team_id, signature_artifact_version_id)
752
+ REFERENCES artifact_versions(team_id, id) ON DELETE RESTRICT,
753
+ FOREIGN KEY(team_id, acting_human_principal_id)
754
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
755
+ FOREIGN KEY(team_id, provenance_node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
756
+ FOREIGN KEY(team_id, provenance_agent_id) REFERENCES agents(team_id, id) ON DELETE RESTRICT,
757
+ FOREIGN KEY(team_id, provenance_chat_id) REFERENCES chats(team_id, id) ON DELETE RESTRICT,
758
+ FOREIGN KEY(team_id, provenance_run_id) REFERENCES runs(team_id, id) ON DELETE RESTRICT,
759
+ UNIQUE(artifact_id, version),
760
+ UNIQUE(team_id, id),
761
+ CHECK(
762
+ (
763
+ acting_human_principal_id IS NULL
764
+ AND provenance_agent_id IS NULL
765
+ AND provenance_node_id IS NULL
766
+ AND provenance_chat_id IS NULL
767
+ AND provenance_run_id IS NULL
768
+ )
769
+ OR (
770
+ acting_human_principal_id IS NOT NULL
771
+ AND provenance_agent_id IS NOT NULL
772
+ AND provenance_node_id IS NOT NULL
773
+ AND provenance_chat_id IS NOT NULL
774
+ AND provenance_run_id IS NOT NULL
775
+ )
776
+ )
777
+ );
778
+
779
+ CREATE TRIGGER artifact_versions_require_team_creator
780
+ BEFORE INSERT ON artifact_versions
781
+ FOR EACH ROW WHEN NOT EXISTS (
782
+ SELECT 1 FROM principals AS p
783
+ WHERE p.id = NEW.created_by_principal_id
784
+ AND p.status = 'active'
785
+ AND p.kind IN ('human', 'service', 'agent')
786
+ AND (
787
+ p.scope_team_id = NEW.team_id
788
+ OR EXISTS (
789
+ SELECT 1 FROM memberships AS m
790
+ WHERE m.team_id = NEW.team_id
791
+ AND m.principal_id = p.id
792
+ AND m.status = 'active'
793
+ )
794
+ )
795
+ )
796
+ BEGIN
797
+ SELECT RAISE(ABORT, 'artifact version creator does not belong to this team');
798
+ END;
799
+
800
+ CREATE TRIGGER artifact_versions_reject_false_agent_provenance
801
+ BEFORE INSERT ON artifact_versions
802
+ FOR EACH ROW WHEN NEW.provenance_agent_id IS NOT NULL AND COALESCE(
803
+ (SELECT kind FROM principals WHERE id = NEW.created_by_principal_id), ''
804
+ ) <> 'agent'
805
+ BEGIN
806
+ SELECT RAISE(ABORT, 'only an agent creator may claim agent provenance');
807
+ END;
808
+
809
+ CREATE TRIGGER artifact_versions_require_agent_provenance
810
+ BEFORE INSERT ON artifact_versions
811
+ FOR EACH ROW WHEN COALESCE(
812
+ (SELECT kind FROM principals WHERE id = NEW.created_by_principal_id), ''
813
+ ) = 'agent' AND NOT EXISTS (
814
+ SELECT 1
815
+ FROM agents AS a
816
+ JOIN chats AS c ON c.team_id = a.team_id AND c.id = NEW.provenance_chat_id
817
+ JOIN runs AS r ON r.team_id = a.team_id AND r.id = NEW.provenance_run_id
818
+ WHERE a.team_id = NEW.team_id
819
+ AND a.id = NEW.provenance_agent_id
820
+ AND a.principal_id = NEW.created_by_principal_id
821
+ AND a.node_id = NEW.provenance_node_id
822
+ AND c.node_id = a.node_id
823
+ AND c.agent_id = a.id
824
+ AND r.node_id = a.node_id
825
+ AND r.agent_id = a.id
826
+ AND r.chat_id = c.id
827
+ AND r.acting_human_principal_id = NEW.acting_human_principal_id
828
+ )
829
+ BEGIN
830
+ SELECT RAISE(ABORT, 'agent-created artifact version requires exact provenance');
831
+ END;
832
+
833
+ CREATE TRIGGER artifact_version_content_is_immutable
834
+ BEFORE UPDATE OF id, team_id, artifact_id, version, media_type, byte_size,
835
+ sha256, storage_key, signature_artifact_version_id,
836
+ created_by_principal_id, acting_human_principal_id, provenance_node_id,
837
+ provenance_agent_id, provenance_chat_id, provenance_run_id, created_at
838
+ ON artifact_versions
839
+ BEGIN
840
+ SELECT RAISE(ABORT, 'artifact version content and provenance are immutable');
841
+ END;
842
+
843
+ CREATE TABLE artifact_acl_entries (
844
+ id TEXT PRIMARY KEY,
845
+ team_id TEXT NOT NULL,
846
+ artifact_id TEXT NOT NULL,
847
+ subject_kind TEXT NOT NULL CHECK(subject_kind IN ('principal', 'role')),
848
+ subject_principal_id TEXT REFERENCES principals(id) ON DELETE CASCADE,
849
+ subject_role TEXT CHECK(subject_role IN ('owner', 'admin', 'member', 'guest', 'automation')),
850
+ can_read INTEGER NOT NULL CHECK(can_read IN (0, 1)),
851
+ can_manage INTEGER NOT NULL CHECK(can_manage IN (0, 1)),
852
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
853
+ FOREIGN KEY(team_id, artifact_id) REFERENCES artifacts(team_id, id) ON DELETE CASCADE,
854
+ CHECK(
855
+ (subject_kind = 'principal' AND subject_principal_id IS NOT NULL AND subject_role IS NULL)
856
+ OR (subject_kind = 'role' AND subject_principal_id IS NULL AND subject_role IS NOT NULL)
857
+ )
858
+ );
859
+
860
+ CREATE TRIGGER artifact_acl_require_team_principal
861
+ BEFORE INSERT ON artifact_acl_entries
862
+ FOR EACH ROW WHEN NEW.subject_kind = 'principal' AND NOT EXISTS (
863
+ SELECT 1 FROM principals AS p
864
+ WHERE p.id = NEW.subject_principal_id
865
+ AND p.status = 'active'
866
+ AND (
867
+ p.scope_team_id = NEW.team_id
868
+ OR EXISTS (
869
+ SELECT 1 FROM memberships AS m
870
+ WHERE m.team_id = NEW.team_id
871
+ AND m.principal_id = p.id
872
+ AND m.status = 'active'
873
+ )
874
+ )
875
+ )
876
+ BEGIN
877
+ SELECT RAISE(ABORT, 'artifact ACL principal does not belong to this team');
878
+ END;
879
+
880
+ CREATE TRIGGER artifact_acl_subject_is_immutable
881
+ BEFORE UPDATE OF id, team_id, artifact_id, subject_kind,
882
+ subject_principal_id, subject_role, created_at
883
+ ON artifact_acl_entries
884
+ BEGIN
885
+ SELECT RAISE(ABORT, 'artifact ACL subject is immutable');
886
+ END;
887
+
888
+ CREATE UNIQUE INDEX artifact_acl_one_principal_entry
889
+ ON artifact_acl_entries(artifact_id, subject_principal_id)
890
+ WHERE subject_kind = 'principal';
891
+
892
+ CREATE UNIQUE INDEX artifact_acl_one_role_entry
893
+ ON artifact_acl_entries(artifact_id, subject_role)
894
+ WHERE subject_kind = 'role';
895
+
896
+ CREATE TABLE message_attachments (
897
+ team_id TEXT NOT NULL,
898
+ message_id TEXT NOT NULL,
899
+ artifact_version_id TEXT NOT NULL,
900
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
901
+ PRIMARY KEY(message_id, artifact_version_id),
902
+ FOREIGN KEY(team_id, message_id) REFERENCES messages(team_id, id) ON DELETE CASCADE,
903
+ FOREIGN KEY(team_id, artifact_version_id)
904
+ REFERENCES artifact_versions(team_id, id) ON DELETE RESTRICT
905
+ );
906
+
907
+ CREATE TABLE library_items (
908
+ id TEXT PRIMARY KEY,
909
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
910
+ kind TEXT NOT NULL CHECK(kind IN ('runbook', 'skill')),
911
+ slug TEXT NOT NULL CHECK(slug = lower(trim(slug))) CHECK(length(slug) BETWEEN 1 AND 100),
912
+ display_name TEXT NOT NULL CHECK(length(trim(display_name)) BETWEEN 1 AND 200),
913
+ created_by_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
914
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
915
+ updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
916
+ archived_at INTEGER CHECK(archived_at IS NULL OR archived_at >= created_at),
917
+ UNIQUE(team_id, slug),
918
+ UNIQUE(team_id, id)
919
+ );
920
+
921
+ CREATE TRIGGER library_items_require_team_creator
922
+ BEFORE INSERT ON library_items
923
+ FOR EACH ROW WHEN NOT EXISTS (
924
+ SELECT 1 FROM principals AS p
925
+ WHERE p.id = NEW.created_by_principal_id
926
+ AND p.status = 'active'
927
+ AND p.kind IN ('human', 'service', 'agent')
928
+ AND (
929
+ p.scope_team_id = NEW.team_id
930
+ OR EXISTS (
931
+ SELECT 1 FROM memberships AS m
932
+ WHERE m.team_id = NEW.team_id
933
+ AND m.principal_id = p.id
934
+ AND m.status = 'active'
935
+ )
936
+ )
937
+ )
938
+ BEGIN
939
+ SELECT RAISE(ABORT, 'library creator does not belong to this team');
940
+ END;
941
+
942
+ CREATE TRIGGER library_item_identity_is_immutable
943
+ BEFORE UPDATE OF id, team_id, kind, slug, created_by_principal_id, created_at
944
+ ON library_items
945
+ BEGIN
946
+ SELECT RAISE(ABORT, 'library item identity is immutable');
947
+ END;
948
+
949
+ CREATE TABLE library_versions (
950
+ id TEXT PRIMARY KEY,
951
+ team_id TEXT NOT NULL,
952
+ library_item_id TEXT NOT NULL,
953
+ version INTEGER NOT NULL CHECK(version >= 1),
954
+ artifact_version_id TEXT NOT NULL,
955
+ status TEXT NOT NULL DEFAULT 'draft'
956
+ CHECK(status IN ('draft', 'in_review', 'approved', 'rejected', 'withdrawn')),
957
+ permissions_json TEXT NOT NULL DEFAULT '{}'
958
+ CHECK(json_valid(permissions_json) AND json_type(permissions_json) = 'object'),
959
+ provenance_node_id TEXT,
960
+ provenance_agent_id TEXT,
961
+ provenance_chat_id TEXT,
962
+ provenance_run_id TEXT,
963
+ acting_human_principal_id TEXT,
964
+ published_by_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
965
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
966
+ reviewed_at INTEGER CHECK(reviewed_at IS NULL OR reviewed_at >= created_at),
967
+ FOREIGN KEY(team_id, library_item_id) REFERENCES library_items(team_id, id) ON DELETE CASCADE,
968
+ FOREIGN KEY(team_id, artifact_version_id)
969
+ REFERENCES artifact_versions(team_id, id) ON DELETE RESTRICT,
970
+ FOREIGN KEY(team_id, provenance_node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
971
+ FOREIGN KEY(team_id, provenance_agent_id) REFERENCES agents(team_id, id) ON DELETE RESTRICT,
972
+ FOREIGN KEY(team_id, provenance_chat_id) REFERENCES chats(team_id, id) ON DELETE RESTRICT,
973
+ FOREIGN KEY(team_id, provenance_run_id) REFERENCES runs(team_id, id) ON DELETE RESTRICT,
974
+ FOREIGN KEY(team_id, acting_human_principal_id)
975
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
976
+ UNIQUE(library_item_id, version),
977
+ UNIQUE(team_id, id),
978
+ CHECK(
979
+ (
980
+ acting_human_principal_id IS NULL
981
+ AND provenance_agent_id IS NULL
982
+ AND provenance_node_id IS NULL
983
+ AND provenance_chat_id IS NULL
984
+ AND provenance_run_id IS NULL
985
+ )
986
+ OR (
987
+ acting_human_principal_id IS NOT NULL
988
+ AND provenance_agent_id IS NOT NULL
989
+ AND provenance_node_id IS NOT NULL
990
+ AND provenance_chat_id IS NOT NULL
991
+ AND provenance_run_id IS NOT NULL
992
+ )
993
+ )
994
+ );
995
+
996
+ CREATE TRIGGER library_versions_require_team_publisher
997
+ BEFORE INSERT ON library_versions
998
+ FOR EACH ROW WHEN NOT EXISTS (
999
+ SELECT 1 FROM principals AS p
1000
+ WHERE p.id = NEW.published_by_principal_id
1001
+ AND p.status = 'active'
1002
+ AND p.kind IN ('human', 'service', 'agent')
1003
+ AND (
1004
+ p.scope_team_id = NEW.team_id
1005
+ OR EXISTS (
1006
+ SELECT 1 FROM memberships AS m
1007
+ WHERE m.team_id = NEW.team_id
1008
+ AND m.principal_id = p.id
1009
+ AND m.status = 'active'
1010
+ )
1011
+ )
1012
+ )
1013
+ BEGIN
1014
+ SELECT RAISE(ABORT, 'library publisher does not belong to this team');
1015
+ END;
1016
+
1017
+ CREATE TRIGGER library_versions_reject_false_agent_provenance
1018
+ BEFORE INSERT ON library_versions
1019
+ FOR EACH ROW WHEN NEW.provenance_agent_id IS NOT NULL AND COALESCE(
1020
+ (SELECT kind FROM principals WHERE id = NEW.published_by_principal_id), ''
1021
+ ) <> 'agent'
1022
+ BEGIN
1023
+ SELECT RAISE(ABORT, 'only an agent publisher may claim agent provenance');
1024
+ END;
1025
+
1026
+ CREATE TRIGGER library_versions_require_agent_provenance
1027
+ BEFORE INSERT ON library_versions
1028
+ FOR EACH ROW WHEN COALESCE(
1029
+ (SELECT kind FROM principals WHERE id = NEW.published_by_principal_id), ''
1030
+ ) = 'agent' AND NOT EXISTS (
1031
+ SELECT 1
1032
+ FROM agents AS a
1033
+ JOIN chats AS c ON c.team_id = a.team_id AND c.id = NEW.provenance_chat_id
1034
+ JOIN runs AS r ON r.team_id = a.team_id AND r.id = NEW.provenance_run_id
1035
+ WHERE a.team_id = NEW.team_id
1036
+ AND a.id = NEW.provenance_agent_id
1037
+ AND a.principal_id = NEW.published_by_principal_id
1038
+ AND a.node_id = NEW.provenance_node_id
1039
+ AND c.node_id = a.node_id
1040
+ AND c.agent_id = a.id
1041
+ AND r.node_id = a.node_id
1042
+ AND r.agent_id = a.id
1043
+ AND r.chat_id = c.id
1044
+ AND r.acting_human_principal_id = NEW.acting_human_principal_id
1045
+ )
1046
+ BEGIN
1047
+ SELECT RAISE(ABORT, 'agent-published library version requires exact provenance');
1048
+ END;
1049
+
1050
+ CREATE TRIGGER library_version_content_is_immutable
1051
+ BEFORE UPDATE OF id, team_id, library_item_id, version, artifact_version_id,
1052
+ permissions_json, provenance_node_id, provenance_agent_id,
1053
+ provenance_chat_id, provenance_run_id, acting_human_principal_id,
1054
+ published_by_principal_id, created_at
1055
+ ON library_versions
1056
+ BEGIN
1057
+ SELECT RAISE(ABORT, 'library version content and provenance are immutable');
1058
+ END;
1059
+
1060
+ CREATE TABLE library_reviews (
1061
+ id TEXT PRIMARY KEY,
1062
+ team_id TEXT NOT NULL,
1063
+ library_version_id TEXT NOT NULL,
1064
+ reviewer_principal_id TEXT NOT NULL,
1065
+ decision TEXT NOT NULL CHECK(decision IN ('approved', 'changes_requested', 'rejected')),
1066
+ review_note TEXT CHECK(review_note IS NULL OR length(review_note) <= 16000),
1067
+ reviewed_at INTEGER NOT NULL CHECK(reviewed_at >= 0),
1068
+ FOREIGN KEY(team_id, library_version_id)
1069
+ REFERENCES library_versions(team_id, id) ON DELETE CASCADE,
1070
+ FOREIGN KEY(team_id, reviewer_principal_id)
1071
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
1072
+ UNIQUE(library_version_id, reviewer_principal_id)
1073
+ );
1074
+
1075
+ CREATE TABLE skill_install_requests (
1076
+ id TEXT PRIMARY KEY,
1077
+ team_id TEXT NOT NULL,
1078
+ library_version_id TEXT NOT NULL,
1079
+ target_node_id TEXT NOT NULL,
1080
+ target_agent_id TEXT,
1081
+ diff_artifact_version_id TEXT NOT NULL,
1082
+ requested_by_principal_id TEXT NOT NULL,
1083
+ approved_by_principal_id TEXT,
1084
+ status TEXT NOT NULL DEFAULT 'requested'
1085
+ CHECK(status IN ('requested', 'approved', 'rejected', 'installed', 'failed', 'cancelled')),
1086
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
1087
+ updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
1088
+ FOREIGN KEY(team_id, library_version_id)
1089
+ REFERENCES library_versions(team_id, id) ON DELETE RESTRICT,
1090
+ FOREIGN KEY(team_id, target_node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
1091
+ FOREIGN KEY(team_id, target_node_id, target_agent_id)
1092
+ REFERENCES agents(team_id, node_id, id) ON DELETE RESTRICT,
1093
+ FOREIGN KEY(team_id, diff_artifact_version_id)
1094
+ REFERENCES artifact_versions(team_id, id) ON DELETE RESTRICT,
1095
+ FOREIGN KEY(team_id, requested_by_principal_id)
1096
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
1097
+ FOREIGN KEY(team_id, approved_by_principal_id)
1098
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
1099
+ UNIQUE(team_id, id),
1100
+ CHECK(status NOT IN ('approved', 'installed') OR approved_by_principal_id IS NOT NULL)
1101
+ );
1102
+
1103
+ CREATE TABLE audit_events (
1104
+ id TEXT PRIMARY KEY,
1105
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE RESTRICT,
1106
+ actor_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
1107
+ acting_human_principal_id TEXT,
1108
+ node_id TEXT,
1109
+ agent_id TEXT,
1110
+ chat_id TEXT,
1111
+ run_id TEXT,
1112
+ action TEXT NOT NULL CHECK(length(trim(action)) BETWEEN 1 AND 160),
1113
+ resource_type TEXT NOT NULL CHECK(length(trim(resource_type)) BETWEEN 1 AND 80),
1114
+ resource_id TEXT NOT NULL CHECK(length(trim(resource_id)) BETWEEN 1 AND 240),
1115
+ outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'denied', 'succeeded', 'failed', 'cancelled')),
1116
+ metadata_json TEXT NOT NULL DEFAULT '{}'
1117
+ CHECK(json_valid(metadata_json) AND json_type(metadata_json) = 'object'),
1118
+ previous_event_hash BLOB CHECK(previous_event_hash IS NULL OR (
1119
+ typeof(previous_event_hash) = 'blob' AND length(previous_event_hash) = 32
1120
+ )),
1121
+ event_hash BLOB NOT NULL CHECK(typeof(event_hash) = 'blob' AND length(event_hash) = 32),
1122
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
1123
+ FOREIGN KEY(team_id, acting_human_principal_id)
1124
+ REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
1125
+ FOREIGN KEY(team_id, node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
1126
+ FOREIGN KEY(team_id, agent_id) REFERENCES agents(team_id, id) ON DELETE RESTRICT,
1127
+ FOREIGN KEY(team_id, chat_id) REFERENCES chats(team_id, id) ON DELETE RESTRICT,
1128
+ FOREIGN KEY(team_id, run_id) REFERENCES runs(team_id, id) ON DELETE RESTRICT,
1129
+ UNIQUE(team_id, event_hash),
1130
+ CHECK(
1131
+ (
1132
+ acting_human_principal_id IS NULL
1133
+ AND
1134
+ agent_id IS NULL
1135
+ AND node_id IS NULL
1136
+ AND chat_id IS NULL
1137
+ AND run_id IS NULL
1138
+ )
1139
+ OR (
1140
+ acting_human_principal_id IS NULL
1141
+ AND node_id IS NOT NULL
1142
+ AND agent_id IS NULL
1143
+ AND chat_id IS NULL
1144
+ AND run_id IS NULL
1145
+ )
1146
+ OR (
1147
+ acting_human_principal_id IS NOT NULL
1148
+ AND agent_id IS NOT NULL
1149
+ AND node_id IS NOT NULL
1150
+ AND chat_id IS NOT NULL
1151
+ AND run_id IS NOT NULL
1152
+ )
1153
+ )
1154
+ );
1155
+
1156
+ CREATE TRIGGER audit_events_require_team_actor
1157
+ BEFORE INSERT ON audit_events
1158
+ FOR EACH ROW WHEN NOT EXISTS (
1159
+ SELECT 1 FROM principals AS p
1160
+ WHERE p.id = NEW.actor_principal_id
1161
+ AND p.kind IN ('human', 'service', 'node', 'agent')
1162
+ AND (
1163
+ p.scope_team_id = NEW.team_id
1164
+ OR EXISTS (
1165
+ SELECT 1 FROM memberships AS m
1166
+ WHERE m.team_id = NEW.team_id
1167
+ AND m.principal_id = p.id
1168
+ )
1169
+ )
1170
+ )
1171
+ BEGIN
1172
+ SELECT RAISE(ABORT, 'audit actor does not belong to this team');
1173
+ END;
1174
+
1175
+ CREATE TRIGGER audit_events_reject_false_node_provenance
1176
+ BEFORE INSERT ON audit_events
1177
+ FOR EACH ROW WHEN NEW.node_id IS NOT NULL AND COALESCE(
1178
+ (SELECT kind FROM principals WHERE id = NEW.actor_principal_id), ''
1179
+ ) NOT IN ('node', 'agent')
1180
+ BEGIN
1181
+ SELECT RAISE(ABORT, 'only a node or agent actor may claim node provenance');
1182
+ END;
1183
+
1184
+ CREATE TRIGGER audit_events_require_node_provenance
1185
+ BEFORE INSERT ON audit_events
1186
+ FOR EACH ROW WHEN COALESCE(
1187
+ (SELECT kind FROM principals WHERE id = NEW.actor_principal_id), ''
1188
+ ) = 'node' AND NOT EXISTS (
1189
+ SELECT 1 FROM nodes AS n
1190
+ WHERE n.team_id = NEW.team_id
1191
+ AND n.id = NEW.node_id
1192
+ AND n.principal_id = NEW.actor_principal_id
1193
+ )
1194
+ BEGIN
1195
+ SELECT RAISE(ABORT, 'node audit event requires exact node provenance');
1196
+ END;
1197
+
1198
+ CREATE TRIGGER audit_events_reject_false_agent_provenance
1199
+ BEFORE INSERT ON audit_events
1200
+ FOR EACH ROW WHEN NEW.agent_id IS NOT NULL AND COALESCE(
1201
+ (SELECT kind FROM principals WHERE id = NEW.actor_principal_id), ''
1202
+ ) <> 'agent'
1203
+ BEGIN
1204
+ SELECT RAISE(ABORT, 'only an agent actor may claim agent provenance');
1205
+ END;
1206
+
1207
+ CREATE TRIGGER audit_events_require_agent_provenance
1208
+ BEFORE INSERT ON audit_events
1209
+ FOR EACH ROW WHEN COALESCE(
1210
+ (SELECT kind FROM principals WHERE id = NEW.actor_principal_id), ''
1211
+ ) = 'agent' AND NOT EXISTS (
1212
+ SELECT 1
1213
+ FROM agents AS a
1214
+ JOIN chats AS c ON c.team_id = a.team_id AND c.id = NEW.chat_id
1215
+ JOIN runs AS r ON r.team_id = a.team_id AND r.id = NEW.run_id
1216
+ WHERE a.team_id = NEW.team_id
1217
+ AND a.id = NEW.agent_id
1218
+ AND a.principal_id = NEW.actor_principal_id
1219
+ AND a.node_id = NEW.node_id
1220
+ AND c.node_id = a.node_id
1221
+ AND c.agent_id = a.id
1222
+ AND r.node_id = a.node_id
1223
+ AND r.agent_id = a.id
1224
+ AND r.chat_id = c.id
1225
+ AND r.acting_human_principal_id = NEW.acting_human_principal_id
1226
+ )
1227
+ BEGIN
1228
+ SELECT RAISE(ABORT, 'agent audit event requires exact provenance');
1229
+ END;
1230
+
1231
+ CREATE TRIGGER audit_events_are_immutable_update
1232
+ BEFORE UPDATE ON audit_events
1233
+ BEGIN
1234
+ SELECT RAISE(ABORT, 'audit events are immutable');
1235
+ END;
1236
+
1237
+ CREATE TRIGGER audit_events_are_immutable_delete
1238
+ BEFORE DELETE ON audit_events
1239
+ BEGIN
1240
+ SELECT RAISE(ABORT, 'audit events are immutable');
1241
+ END;
1242
+
1243
+ CREATE TABLE outbox_events (
1244
+ id TEXT PRIMARY KEY,
1245
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
1246
+ aggregate_type TEXT NOT NULL CHECK(length(trim(aggregate_type)) BETWEEN 1 AND 80),
1247
+ aggregate_id TEXT NOT NULL CHECK(length(trim(aggregate_id)) BETWEEN 1 AND 240),
1248
+ event_type TEXT NOT NULL CHECK(length(trim(event_type)) BETWEEN 1 AND 160),
1249
+ metadata_json TEXT NOT NULL CHECK(json_valid(metadata_json) AND json_type(metadata_json) = 'object'),
1250
+ idempotency_key BLOB NOT NULL UNIQUE
1251
+ CHECK(typeof(idempotency_key) = 'blob' AND length(idempotency_key) = 32),
1252
+ state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending', 'leased', 'delivered', 'dead_letter')),
1253
+ available_at INTEGER NOT NULL CHECK(available_at >= 0),
1254
+ lease_owner TEXT,
1255
+ lease_expires_at INTEGER,
1256
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK(attempt_count BETWEEN 0 AND 100),
1257
+ delivered_at INTEGER CHECK(delivered_at IS NULL OR delivered_at >= available_at),
1258
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
1259
+ CHECK(
1260
+ (state = 'leased' AND lease_owner IS NOT NULL AND lease_expires_at IS NOT NULL)
1261
+ OR (state <> 'leased')
1262
+ )
1263
+ );
1264
+
1265
+ CREATE TRIGGER outbox_event_identity_is_immutable
1266
+ BEFORE UPDATE OF id, team_id, aggregate_type, aggregate_id, event_type,
1267
+ metadata_json, idempotency_key, created_at
1268
+ ON outbox_events
1269
+ BEGIN
1270
+ SELECT RAISE(ABORT, 'outbox event identity and payload are immutable');
1271
+ END;
1272
+
1273
+ CREATE TRIGGER outbox_delivery_state_is_monotonic
1274
+ BEFORE UPDATE ON outbox_events
1275
+ FOR EACH ROW WHEN
1276
+ NEW.attempt_count < OLD.attempt_count
1277
+ OR (OLD.delivered_at IS NOT NULL AND NEW.delivered_at IS NOT OLD.delivered_at)
1278
+ BEGIN
1279
+ SELECT RAISE(ABORT, 'outbox delivery counters are monotonic');
1280
+ END;
1281
+
1282
+ CREATE INDEX outbox_pending_delivery
1283
+ ON outbox_events(state, available_at, id)
1284
+ WHERE state IN ('pending', 'leased');
1285
+
1286
+ CREATE TABLE rate_limit_buckets (
1287
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
1288
+ subject_key TEXT NOT NULL CHECK(length(subject_key) BETWEEN 1 AND 240),
1289
+ action TEXT NOT NULL CHECK(length(action) BETWEEN 1 AND 120),
1290
+ window_started_at INTEGER NOT NULL CHECK(window_started_at >= 0),
1291
+ count INTEGER NOT NULL CHECK(count >= 0),
1292
+ updated_at INTEGER NOT NULL CHECK(updated_at >= window_started_at),
1293
+ PRIMARY KEY(team_id, subject_key, action)
1294
+ );
1295
+
1296
+ CREATE VIRTUAL TABLE message_search USING fts5(
1297
+ team_id UNINDEXED,
1298
+ channel_id UNINDEXED,
1299
+ id UNINDEXED,
1300
+ body,
1301
+ content = 'messages',
1302
+ content_rowid = 'rowid',
1303
+ tokenize = 'unicode61 remove_diacritics 2'
1304
+ );
1305
+
1306
+ CREATE TRIGGER messages_search_insert
1307
+ AFTER INSERT ON messages
1308
+ FOR EACH ROW WHEN NEW.deleted_at IS NULL
1309
+ BEGIN
1310
+ INSERT INTO message_search(rowid, team_id, channel_id, id, body)
1311
+ VALUES (NEW.rowid, NEW.team_id, NEW.channel_id, NEW.id, NEW.body);
1312
+ END;
1313
+
1314
+ CREATE TRIGGER messages_search_delete
1315
+ AFTER DELETE ON messages
1316
+ FOR EACH ROW WHEN OLD.deleted_at IS NULL
1317
+ BEGIN
1318
+ INSERT INTO message_search(message_search, rowid, team_id, channel_id, id, body)
1319
+ VALUES ('delete', OLD.rowid, OLD.team_id, OLD.channel_id, OLD.id, OLD.body);
1320
+ END;
1321
+
1322
+ CREATE TRIGGER messages_search_update_remove
1323
+ AFTER UPDATE ON messages
1324
+ FOR EACH ROW WHEN OLD.deleted_at IS NULL
1325
+ BEGIN
1326
+ INSERT INTO message_search(message_search, rowid, team_id, channel_id, id, body)
1327
+ VALUES ('delete', OLD.rowid, OLD.team_id, OLD.channel_id, OLD.id, OLD.body);
1328
+ END;
1329
+
1330
+ CREATE TRIGGER messages_search_update_add
1331
+ AFTER UPDATE ON messages
1332
+ FOR EACH ROW WHEN NEW.deleted_at IS NULL
1333
+ BEGIN
1334
+ INSERT INTO message_search(rowid, team_id, channel_id, id, body)
1335
+ VALUES (NEW.rowid, NEW.team_id, NEW.channel_id, NEW.id, NEW.body);
1336
+ END;