@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,324 @@
1
+ -- Team Messages V2. One team messaging model with attachments plus a
2
+ -- versioned Skills library. Messages are passive records: creating one never
3
+ -- starts, steers, or authorizes an agent. Messages and skill versions are
4
+ -- immutable; skills carry only pin, archive, and current-version pointers.
5
+ -- Attachment bytes live outside SQLite in a content-addressed directory; this
6
+ -- schema stores their metadata and upload state only.
7
+
8
+ CREATE TABLE team_skills (
9
+ id TEXT PRIMARY KEY,
10
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE RESTRICT,
11
+ slug TEXT NOT NULL
12
+ CHECK(slug = lower(trim(slug)))
13
+ CHECK(length(slug) BETWEEN 1 AND 64),
14
+ title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 160),
15
+ summary TEXT NOT NULL DEFAULT '' CHECK(length(summary) <= 280),
16
+ tags_json TEXT NOT NULL DEFAULT '[]'
17
+ CHECK(json_valid(tags_json) AND json_type(tags_json) = 'array'),
18
+ current_version INTEGER NOT NULL DEFAULT 1 CHECK(current_version >= 1),
19
+ created_by_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
20
+ pinned_at INTEGER CHECK(pinned_at IS NULL OR pinned_at >= 0),
21
+ pinned_by_principal_id TEXT REFERENCES principals(id) ON DELETE RESTRICT,
22
+ archived_at INTEGER CHECK(archived_at IS NULL OR archived_at >= created_at),
23
+ archived_by_principal_id TEXT REFERENCES principals(id) ON DELETE RESTRICT,
24
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
25
+ updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
26
+ UNIQUE(team_id, slug),
27
+ UNIQUE(team_id, id),
28
+ CHECK((pinned_at IS NULL) = (pinned_by_principal_id IS NULL)),
29
+ CHECK((archived_at IS NULL) = (archived_by_principal_id IS NULL))
30
+ );
31
+
32
+ CREATE INDEX team_skills_listing
33
+ ON team_skills(team_id, archived_at, pinned_at, updated_at);
34
+
35
+ CREATE TRIGGER team_skills_require_team_creator
36
+ BEFORE INSERT ON team_skills
37
+ FOR EACH ROW WHEN NOT EXISTS (
38
+ SELECT 1 FROM principals AS p
39
+ WHERE p.id = NEW.created_by_principal_id
40
+ AND p.status = 'active'
41
+ AND p.kind IN ('human', 'service', 'agent')
42
+ AND (
43
+ p.scope_team_id = NEW.team_id
44
+ OR EXISTS (
45
+ SELECT 1 FROM memberships AS m
46
+ WHERE m.team_id = NEW.team_id
47
+ AND m.principal_id = p.id
48
+ AND m.status = 'active'
49
+ )
50
+ )
51
+ )
52
+ BEGIN
53
+ SELECT RAISE(ABORT, 'team skill creator does not belong to this team');
54
+ END;
55
+
56
+ CREATE TRIGGER team_skills_limit_per_team
57
+ BEFORE INSERT ON team_skills
58
+ FOR EACH ROW WHEN (
59
+ SELECT COUNT(*) FROM team_skills WHERE team_id = NEW.team_id
60
+ ) >= 500
61
+ BEGIN
62
+ SELECT RAISE(ABORT, 'team skill limit exceeded');
63
+ END;
64
+
65
+ CREATE TRIGGER team_skill_identity_is_immutable
66
+ BEFORE UPDATE OF id, team_id, slug, created_by_principal_id, created_at
67
+ ON team_skills
68
+ BEGIN
69
+ SELECT RAISE(ABORT, 'team skill identity is immutable');
70
+ END;
71
+
72
+ CREATE TRIGGER team_skills_cannot_be_deleted
73
+ BEFORE DELETE ON team_skills
74
+ BEGIN
75
+ SELECT RAISE(ABORT, 'team skills are archived, never deleted');
76
+ END;
77
+
78
+ CREATE TABLE team_messages (
79
+ queue_ordinal INTEGER PRIMARY KEY AUTOINCREMENT,
80
+ id TEXT NOT NULL UNIQUE,
81
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE RESTRICT,
82
+ kind TEXT NOT NULL CHECK(kind IN ('message', 'skill')),
83
+ title TEXT CHECK(title IS NULL OR length(trim(title)) BETWEEN 1 AND 160),
84
+ body_format TEXT NOT NULL DEFAULT 'markdown' CHECK(body_format IN ('plain', 'markdown')),
85
+ body TEXT NOT NULL CHECK(length(CAST(body AS BLOB)) BETWEEN 1 AND 49152),
86
+ body_sha256 BLOB NOT NULL
87
+ CHECK(typeof(body_sha256) = 'blob' AND length(body_sha256) = 32),
88
+ sender_kind TEXT NOT NULL CHECK(sender_kind IN ('human', 'server')),
89
+ sender_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
90
+ sender_node_id TEXT,
91
+ provenance_json TEXT NOT NULL DEFAULT '{}'
92
+ CHECK(
93
+ json_valid(provenance_json)
94
+ AND json_type(provenance_json) = 'object'
95
+ AND length(provenance_json) <= 2048
96
+ ),
97
+ in_reply_to_message_id TEXT,
98
+ skill_id TEXT,
99
+ skill_version INTEGER CHECK(skill_version IS NULL OR skill_version >= 1),
100
+ attachment_count INTEGER NOT NULL DEFAULT 0 CHECK(attachment_count BETWEEN 0 AND 16),
101
+ attachment_bytes INTEGER NOT NULL DEFAULT 0 CHECK(attachment_bytes >= 0),
102
+ idempotency_key BLOB NOT NULL
103
+ CHECK(typeof(idempotency_key) = 'blob' AND length(idempotency_key) = 32),
104
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
105
+ FOREIGN KEY(team_id, sender_node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
106
+ FOREIGN KEY(team_id, in_reply_to_message_id)
107
+ REFERENCES team_messages(team_id, id) ON DELETE RESTRICT,
108
+ FOREIGN KEY(team_id, skill_id) REFERENCES team_skills(team_id, id) ON DELETE RESTRICT,
109
+ UNIQUE(team_id, id),
110
+ UNIQUE(team_id, idempotency_key),
111
+ CHECK(
112
+ (sender_kind = 'human' AND sender_node_id IS NULL)
113
+ OR (sender_kind = 'server' AND sender_node_id IS NOT NULL)
114
+ ),
115
+ CHECK(
116
+ (kind = 'skill' AND title IS NOT NULL AND skill_id IS NOT NULL AND skill_version IS NOT NULL)
117
+ OR (kind = 'message' AND skill_id IS NULL AND skill_version IS NULL)
118
+ )
119
+ );
120
+
121
+ CREATE INDEX team_messages_team_order ON team_messages(team_id, queue_ordinal);
122
+ CREATE INDEX team_messages_sender ON team_messages(team_id, sender_principal_id, queue_ordinal);
123
+
124
+ CREATE TRIGGER team_messages_require_team_sender
125
+ BEFORE INSERT ON team_messages
126
+ FOR EACH ROW WHEN NOT EXISTS (
127
+ SELECT 1 FROM principals AS p
128
+ WHERE p.id = NEW.sender_principal_id
129
+ AND p.status = 'active'
130
+ AND p.kind IN ('human', 'service', 'agent')
131
+ AND (
132
+ p.scope_team_id = NEW.team_id
133
+ OR EXISTS (
134
+ SELECT 1 FROM memberships AS m
135
+ WHERE m.team_id = NEW.team_id
136
+ AND m.principal_id = p.id
137
+ AND m.status = 'active'
138
+ )
139
+ )
140
+ )
141
+ BEGIN
142
+ SELECT RAISE(ABORT, 'team message sender does not belong to this team');
143
+ END;
144
+
145
+ CREATE TRIGGER team_messages_are_immutable
146
+ BEFORE UPDATE ON team_messages
147
+ BEGIN
148
+ SELECT RAISE(ABORT, 'team messages are immutable');
149
+ END;
150
+
151
+ CREATE TRIGGER team_messages_cannot_be_deleted
152
+ BEFORE DELETE ON team_messages
153
+ BEGIN
154
+ SELECT RAISE(ABORT, 'team messages cannot be deleted');
155
+ END;
156
+
157
+ CREATE TABLE team_message_recipients (
158
+ id TEXT PRIMARY KEY,
159
+ team_id TEXT NOT NULL,
160
+ message_id TEXT NOT NULL,
161
+ recipient_kind TEXT NOT NULL CHECK(recipient_kind IN ('server', 'human', 'all')),
162
+ recipient_node_id TEXT,
163
+ recipient_principal_id TEXT REFERENCES principals(id) ON DELETE RESTRICT,
164
+ state TEXT NOT NULL DEFAULT 'available'
165
+ CHECK(state IN ('available', 'delivered', 'read')),
166
+ delivered_at INTEGER CHECK(delivered_at IS NULL OR delivered_at >= 0),
167
+ read_at INTEGER CHECK(read_at IS NULL OR read_at >= 0),
168
+ FOREIGN KEY(team_id, message_id) REFERENCES team_messages(team_id, id) ON DELETE RESTRICT,
169
+ FOREIGN KEY(team_id, recipient_node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
170
+ UNIQUE(team_id, id),
171
+ CHECK(
172
+ (recipient_kind = 'server' AND recipient_node_id IS NOT NULL AND recipient_principal_id IS NULL)
173
+ OR (recipient_kind = 'human' AND recipient_principal_id IS NOT NULL AND recipient_node_id IS NULL)
174
+ OR (recipient_kind = 'all' AND recipient_node_id IS NULL AND recipient_principal_id IS NULL)
175
+ ),
176
+ CHECK(
177
+ (state = 'available' AND delivered_at IS NULL AND read_at IS NULL)
178
+ OR (state = 'delivered' AND delivered_at IS NOT NULL AND read_at IS NULL)
179
+ OR (state = 'read' AND delivered_at IS NOT NULL AND read_at IS NOT NULL)
180
+ )
181
+ );
182
+
183
+ CREATE UNIQUE INDEX team_message_recipients_one_per_target
184
+ ON team_message_recipients(
185
+ message_id,
186
+ recipient_kind,
187
+ COALESCE(recipient_node_id, recipient_principal_id, '')
188
+ );
189
+
190
+ CREATE INDEX team_message_recipients_inbox
191
+ ON team_message_recipients(team_id, recipient_kind, recipient_node_id, recipient_principal_id, state);
192
+
193
+ CREATE TRIGGER team_message_recipient_identity_is_immutable
194
+ BEFORE UPDATE OF id, team_id, message_id, recipient_kind, recipient_node_id,
195
+ recipient_principal_id
196
+ ON team_message_recipients
197
+ BEGIN
198
+ SELECT RAISE(ABORT, 'team message recipient identity is immutable');
199
+ END;
200
+
201
+ CREATE TRIGGER team_message_recipient_state_is_monotonic
202
+ BEFORE UPDATE OF state, delivered_at, read_at ON team_message_recipients
203
+ FOR EACH ROW WHEN
204
+ (OLD.state = 'delivered' AND NEW.state = 'available')
205
+ OR (OLD.state = 'read' AND NEW.state <> 'read')
206
+ OR (OLD.delivered_at IS NOT NULL AND NEW.delivered_at IS NOT OLD.delivered_at)
207
+ OR (OLD.read_at IS NOT NULL AND NEW.read_at IS NOT OLD.read_at)
208
+ OR OLD.recipient_kind = 'all'
209
+ BEGIN
210
+ SELECT RAISE(ABORT, 'team message receipts are monotonic');
211
+ END;
212
+
213
+ CREATE TRIGGER team_message_recipients_cannot_be_deleted
214
+ BEFORE DELETE ON team_message_recipients
215
+ BEGIN
216
+ SELECT RAISE(ABORT, 'team message recipients cannot be deleted');
217
+ END;
218
+
219
+ CREATE TABLE team_attachments (
220
+ id TEXT PRIMARY KEY,
221
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE RESTRICT,
222
+ message_id TEXT,
223
+ file_name TEXT NOT NULL
224
+ CHECK(length(file_name) BETWEEN 1 AND 255)
225
+ CHECK(instr(file_name, '/') = 0 AND instr(file_name, '\') = 0)
226
+ CHECK(file_name NOT IN ('.', '..')),
227
+ media_type TEXT NOT NULL CHECK(length(trim(media_type)) BETWEEN 3 AND 160),
228
+ byte_size INTEGER NOT NULL CHECK(byte_size >= 1),
229
+ sha256 BLOB NOT NULL CHECK(typeof(sha256) = 'blob' AND length(sha256) = 32),
230
+ storage_key TEXT NOT NULL CHECK(length(storage_key) = 64 AND storage_key = lower(storage_key)),
231
+ state TEXT NOT NULL DEFAULT 'uploading' CHECK(state IN ('uploading', 'ready', 'failed')),
232
+ received_bytes INTEGER NOT NULL DEFAULT 0 CHECK(received_bytes >= 0),
233
+ uploaded_by_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
234
+ uploader_node_id TEXT,
235
+ idempotency_key BLOB NOT NULL
236
+ CHECK(typeof(idempotency_key) = 'blob' AND length(idempotency_key) = 32),
237
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
238
+ ready_at INTEGER CHECK(ready_at IS NULL OR ready_at >= created_at),
239
+ expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
240
+ FOREIGN KEY(team_id, message_id) REFERENCES team_messages(team_id, id) ON DELETE RESTRICT,
241
+ FOREIGN KEY(team_id, uploader_node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
242
+ UNIQUE(team_id, id),
243
+ UNIQUE(team_id, idempotency_key),
244
+ CHECK(received_bytes <= byte_size),
245
+ CHECK(
246
+ (state = 'ready' AND ready_at IS NOT NULL AND received_bytes = byte_size)
247
+ OR (state <> 'ready' AND ready_at IS NULL)
248
+ ),
249
+ CHECK(message_id IS NULL OR state = 'ready')
250
+ );
251
+
252
+ CREATE INDEX team_attachments_by_message ON team_attachments(team_id, message_id);
253
+ CREATE INDEX team_attachments_by_storage ON team_attachments(storage_key, state);
254
+ CREATE INDEX team_attachments_pending ON team_attachments(state, expires_at);
255
+
256
+ CREATE TRIGGER team_attachment_identity_is_immutable
257
+ BEFORE UPDATE OF id, team_id, file_name, media_type, byte_size, sha256,
258
+ storage_key, uploaded_by_principal_id, uploader_node_id, idempotency_key,
259
+ created_at
260
+ ON team_attachments
261
+ BEGIN
262
+ SELECT RAISE(ABORT, 'team attachment identity is immutable');
263
+ END;
264
+
265
+ CREATE TRIGGER team_attachment_binding_is_permanent
266
+ BEFORE UPDATE OF message_id ON team_attachments
267
+ FOR EACH ROW WHEN OLD.message_id IS NOT NULL
268
+ BEGIN
269
+ SELECT RAISE(ABORT, 'team attachment is already bound to a message');
270
+ END;
271
+
272
+ CREATE TRIGGER team_attachment_state_is_forward_only
273
+ BEFORE UPDATE OF state ON team_attachments
274
+ FOR EACH ROW WHEN OLD.state <> 'uploading' AND NEW.state <> OLD.state
275
+ BEGIN
276
+ SELECT RAISE(ABORT, 'team attachment state is terminal');
277
+ END;
278
+
279
+ CREATE TRIGGER team_attachments_bound_cannot_be_deleted
280
+ BEFORE DELETE ON team_attachments
281
+ FOR EACH ROW WHEN OLD.message_id IS NOT NULL OR OLD.state = 'ready'
282
+ BEGIN
283
+ SELECT RAISE(ABORT, 'ready or bound team attachments cannot be deleted');
284
+ END;
285
+
286
+ CREATE TABLE team_skill_versions (
287
+ id TEXT PRIMARY KEY,
288
+ team_id TEXT NOT NULL,
289
+ skill_id TEXT NOT NULL,
290
+ version INTEGER NOT NULL CHECK(version >= 1),
291
+ message_id TEXT NOT NULL,
292
+ title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 160),
293
+ summary TEXT NOT NULL DEFAULT '' CHECK(length(summary) <= 280),
294
+ tags_json TEXT NOT NULL DEFAULT '[]'
295
+ CHECK(json_valid(tags_json) AND json_type(tags_json) = 'array'),
296
+ change_note TEXT NOT NULL DEFAULT '' CHECK(length(change_note) <= 280),
297
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
298
+ FOREIGN KEY(team_id, skill_id) REFERENCES team_skills(team_id, id) ON DELETE RESTRICT,
299
+ FOREIGN KEY(team_id, message_id) REFERENCES team_messages(team_id, id) ON DELETE RESTRICT,
300
+ UNIQUE(skill_id, version),
301
+ UNIQUE(message_id),
302
+ UNIQUE(team_id, id)
303
+ );
304
+
305
+ CREATE TRIGGER team_skill_versions_limit
306
+ BEFORE INSERT ON team_skill_versions
307
+ FOR EACH ROW WHEN (
308
+ SELECT COUNT(*) FROM team_skill_versions WHERE skill_id = NEW.skill_id
309
+ ) >= 200
310
+ BEGIN
311
+ SELECT RAISE(ABORT, 'team skill version limit exceeded');
312
+ END;
313
+
314
+ CREATE TRIGGER team_skill_versions_are_immutable
315
+ BEFORE UPDATE ON team_skill_versions
316
+ BEGIN
317
+ SELECT RAISE(ABORT, 'team skill versions are immutable');
318
+ END;
319
+
320
+ CREATE TRIGGER team_skill_versions_cannot_be_deleted
321
+ BEFORE DELETE ON team_skill_versions
322
+ BEGIN
323
+ SELECT RAISE(ABORT, 'team skill versions cannot be deleted');
324
+ END;
@@ -0,0 +1,52 @@
1
+ -- Team attachment declarations are intentionally retryable for a bounded
2
+ -- window, but an unbound ready upload is not durable message content. Permit
3
+ -- the store's expiry collector to reclaim those orphan rows while continuing
4
+ -- to make message-bound attachments undeletable.
5
+
6
+ DROP TRIGGER team_attachments_bound_cannot_be_deleted;
7
+
8
+ CREATE TRIGGER team_attachments_bound_cannot_be_deleted
9
+ BEFORE DELETE ON team_attachments
10
+ FOR EACH ROW WHEN OLD.message_id IS NOT NULL
11
+ BEGIN
12
+ SELECT RAISE(ABORT, 'bound team attachments cannot be deleted');
13
+ END;
14
+
15
+ -- The opportunistic collector always walks oldest expired declarations first.
16
+ -- Partial indexes keep both its team-scoped hot path and its maintenance-wide
17
+ -- path bounded by the requested LIMIT without sorting or scanning live rows.
18
+ CREATE INDEX team_attachments_reclaim_expired_by_team
19
+ ON team_attachments(team_id, expires_at, id)
20
+ WHERE message_id IS NULL;
21
+
22
+ CREATE INDEX team_attachments_reclaim_expired_global
23
+ ON team_attachments(expires_at, id)
24
+ WHERE message_id IS NULL;
25
+
26
+ -- Metadata deletion and physical deletion are separate commit points. Record
27
+ -- exact, validated relative-path components before deleting metadata so a
28
+ -- failed unlink (or a process crash after commit) remains durably retryable.
29
+ -- A later collector rechecks active metadata references before touching bytes.
30
+ CREATE TABLE team_attachment_cleanup_queue (
31
+ path_kind TEXT NOT NULL CHECK(path_kind IN ('staging', 'content')),
32
+ path_key TEXT NOT NULL,
33
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
34
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK(attempt_count >= 0),
35
+ PRIMARY KEY(path_kind, path_key),
36
+ CHECK(
37
+ (
38
+ path_kind = 'staging'
39
+ AND length(path_key) = 37
40
+ AND substr(path_key, 1, 5) = 'tatt_'
41
+ AND substr(path_key, 6) NOT GLOB '*[^0-9a-f]*'
42
+ )
43
+ OR (
44
+ path_kind = 'content'
45
+ AND length(path_key) = 64
46
+ AND path_key NOT GLOB '*[^0-9a-f]*'
47
+ )
48
+ )
49
+ );
50
+
51
+ CREATE INDEX team_attachment_cleanup_queue_oldest
52
+ ON team_attachment_cleanup_queue(attempt_count, created_at, path_kind, path_key);
@@ -0,0 +1,65 @@
1
+ -- Bound owner/self-service administration scans to their keyset order.
2
+ -- The append-only AUTOINCREMENT ledger gives a traversal a stable high-water:
3
+ -- SQLite table rowids can otherwise be reused after deletion.
4
+ CREATE TABLE human_admin_page_entries (
5
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ resource_kind TEXT NOT NULL
7
+ CHECK(resource_kind IN ('device_session', 'invitation', 'membership')),
8
+ resource_id TEXT NOT NULL,
9
+ UNIQUE(resource_kind, resource_id)
10
+ );
11
+
12
+ INSERT INTO human_admin_page_entries(resource_kind, resource_id)
13
+ SELECT 'device_session', id FROM device_sessions ORDER BY rowid;
14
+ INSERT INTO human_admin_page_entries(resource_kind, resource_id)
15
+ SELECT 'invitation', id FROM invitations ORDER BY rowid;
16
+ INSERT INTO human_admin_page_entries(resource_kind, resource_id)
17
+ SELECT 'membership', id FROM memberships ORDER BY rowid;
18
+
19
+ CREATE TRIGGER human_admin_page_device_session_insert
20
+ AFTER INSERT ON device_sessions
21
+ BEGIN
22
+ INSERT INTO human_admin_page_entries(resource_kind, resource_id)
23
+ VALUES ('device_session', NEW.id);
24
+ END;
25
+
26
+ CREATE TRIGGER human_admin_page_invitation_insert
27
+ AFTER INSERT ON invitations
28
+ BEGIN
29
+ INSERT INTO human_admin_page_entries(resource_kind, resource_id)
30
+ VALUES ('invitation', NEW.id);
31
+ END;
32
+
33
+ CREATE TRIGGER human_admin_page_membership_insert
34
+ AFTER INSERT ON memberships
35
+ BEGIN
36
+ INSERT INTO human_admin_page_entries(resource_kind, resource_id)
37
+ VALUES ('membership', NEW.id);
38
+ END;
39
+
40
+ CREATE TRIGGER human_admin_page_entries_immutable
41
+ BEFORE UPDATE ON human_admin_page_entries
42
+ BEGIN
43
+ SELECT RAISE(ABORT, 'human administration page sequence is immutable');
44
+ END;
45
+
46
+ CREATE TRIGGER human_admin_page_entries_cannot_be_deleted
47
+ BEFORE DELETE ON human_admin_page_entries
48
+ BEGIN
49
+ SELECT RAISE(ABORT, 'human administration page sequence cannot be deleted');
50
+ END;
51
+
52
+ CREATE INDEX device_sessions_human_created_id_idx
53
+ ON device_sessions(human_principal_id, created_at DESC, id DESC);
54
+
55
+ CREATE INDEX invitations_pending_team_created_id_idx
56
+ ON invitations(team_id, created_at DESC, id DESC)
57
+ WHERE redeemed_at IS NULL AND revoked_at IS NULL;
58
+
59
+ CREATE INDEX memberships_active_team_created_principal_idx
60
+ ON memberships(team_id, created_at DESC, principal_id DESC)
61
+ WHERE status = 'active';
62
+
63
+ CREATE INDEX memberships_manageable_team_created_principal_idx
64
+ ON memberships(team_id, created_at DESC, principal_id DESC)
65
+ WHERE status IN ('active', 'suspended');
@@ -0,0 +1,69 @@
1
+ -- Immutable soft-deletion journal for Team Messages V2 and the legacy Team
2
+ -- Network bulletin. Source records and bound attachment blobs remain
3
+ -- immutable; readers suppress content through this additive ledger.
4
+
5
+ CREATE TABLE network_content_deletions (
6
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ id TEXT NOT NULL UNIQUE,
8
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE RESTRICT,
9
+ resource_kind TEXT NOT NULL CHECK(resource_kind IN ('message', 'bulletin')),
10
+ resource_id TEXT NOT NULL,
11
+ deleted_by_principal_id TEXT NOT NULL
12
+ REFERENCES principals(id) ON DELETE RESTRICT,
13
+ deleted_at INTEGER NOT NULL CHECK(deleted_at >= 0),
14
+ UNIQUE(team_id, resource_kind, resource_id)
15
+ );
16
+
17
+ CREATE INDEX network_content_deletions_team_sequence
18
+ ON network_content_deletions(team_id, sequence);
19
+
20
+ CREATE TRIGGER network_content_deletions_require_message
21
+ BEFORE INSERT ON network_content_deletions
22
+ FOR EACH ROW WHEN NEW.resource_kind = 'message' AND NOT EXISTS (
23
+ SELECT 1 FROM team_messages AS m
24
+ WHERE m.team_id = NEW.team_id AND m.id = NEW.resource_id
25
+ AND m.kind = 'message'
26
+ )
27
+ BEGIN
28
+ SELECT RAISE(ABORT, 'team message deletion source is unavailable');
29
+ END;
30
+
31
+ CREATE TRIGGER network_content_deletions_require_bulletin
32
+ BEFORE INSERT ON network_content_deletions
33
+ FOR EACH ROW WHEN NEW.resource_kind = 'bulletin' AND NOT EXISTS (
34
+ SELECT 1
35
+ FROM messages AS m
36
+ JOIN network_boards AS b
37
+ ON b.team_id = m.team_id AND b.channel_id = m.channel_id
38
+ WHERE m.team_id = NEW.team_id AND m.id = NEW.resource_id
39
+ )
40
+ BEGIN
41
+ SELECT RAISE(ABORT, 'bulletin deletion source is unavailable');
42
+ END;
43
+
44
+ CREATE TRIGGER network_content_deletions_require_team_actor
45
+ BEFORE INSERT ON network_content_deletions
46
+ FOR EACH ROW WHEN NOT EXISTS (
47
+ SELECT 1
48
+ FROM principals AS p
49
+ JOIN memberships AS m
50
+ ON m.team_id = NEW.team_id AND m.principal_id = p.id
51
+ WHERE p.id = NEW.deleted_by_principal_id
52
+ AND p.status = 'active'
53
+ AND m.status = 'active'
54
+ )
55
+ BEGIN
56
+ SELECT RAISE(ABORT, 'deletion actor does not belong to this team');
57
+ END;
58
+
59
+ CREATE TRIGGER network_content_deletions_are_immutable
60
+ BEFORE UPDATE ON network_content_deletions
61
+ BEGIN
62
+ SELECT RAISE(ABORT, 'network content deletions are immutable');
63
+ END;
64
+
65
+ CREATE TRIGGER network_content_deletions_cannot_be_deleted
66
+ BEFORE DELETE ON network_content_deletions
67
+ BEGIN
68
+ SELECT RAISE(ABORT, 'network content deletions cannot be deleted');
69
+ END;
@@ -0,0 +1,48 @@
1
+ -- Immutable, author-only revisions for ordinary Team Messages. The original
2
+ -- team_messages row remains version 1; every edit appends a new body revision.
3
+
4
+ CREATE TABLE team_message_revisions (
5
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ id TEXT NOT NULL UNIQUE,
7
+ team_id TEXT NOT NULL,
8
+ message_id TEXT NOT NULL,
9
+ version INTEGER NOT NULL CHECK(version >= 2),
10
+ body_format TEXT NOT NULL CHECK(body_format IN ('plain', 'markdown')),
11
+ body TEXT NOT NULL CHECK(length(CAST(body AS BLOB)) BETWEEN 1 AND 49152),
12
+ body_sha256 BLOB NOT NULL
13
+ CHECK(typeof(body_sha256) = 'blob' AND length(body_sha256) = 32),
14
+ editor_kind TEXT NOT NULL CHECK(editor_kind IN ('human', 'server')),
15
+ edited_by_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
16
+ editor_node_id TEXT,
17
+ idempotency_key BLOB NOT NULL
18
+ CHECK(typeof(idempotency_key) = 'blob' AND length(idempotency_key) = 32),
19
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
20
+ FOREIGN KEY(team_id, message_id)
21
+ REFERENCES team_messages(team_id, id) ON DELETE RESTRICT,
22
+ FOREIGN KEY(team_id, editor_node_id)
23
+ REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
24
+ UNIQUE(team_id, message_id, version),
25
+ UNIQUE(team_id, message_id, idempotency_key),
26
+ CHECK(
27
+ (editor_kind = 'human' AND editor_node_id IS NULL)
28
+ OR (editor_kind = 'server' AND editor_node_id IS NOT NULL)
29
+ )
30
+ );
31
+
32
+ CREATE INDEX team_message_revisions_by_message
33
+ ON team_message_revisions(team_id, message_id, version DESC);
34
+
35
+ CREATE INDEX team_message_revisions_by_team
36
+ ON team_message_revisions(team_id, sequence);
37
+
38
+ CREATE TRIGGER team_message_revisions_are_immutable
39
+ BEFORE UPDATE ON team_message_revisions
40
+ BEGIN
41
+ SELECT RAISE(ABORT, 'team message revisions are immutable');
42
+ END;
43
+
44
+ CREATE TRIGGER team_message_revisions_cannot_be_deleted
45
+ BEFORE DELETE ON team_message_revisions
46
+ BEGIN
47
+ SELECT RAISE(ABORT, 'team message revisions cannot be deleted');
48
+ END;
@@ -0,0 +1,50 @@
1
+ -- A server-created shared network has one internal service owner. Ordinary
2
+ -- services still cannot acquire human membership roles or own personal teams.
3
+ DROP TRIGGER memberships_require_compatible_principal;
4
+ DROP TRIGGER memberships_update_requires_compatible_principal;
5
+
6
+ CREATE TRIGGER memberships_require_compatible_principal
7
+ BEFORE INSERT ON memberships
8
+ FOR EACH ROW WHEN NOT (
9
+ (NEW.role = 'automation' AND COALESCE(
10
+ (SELECT kind FROM principals WHERE id = NEW.principal_id), ''
11
+ ) = 'service')
12
+ OR (NEW.role <> 'automation' AND COALESCE(
13
+ (SELECT kind FROM principals WHERE id = NEW.principal_id), ''
14
+ ) = 'human')
15
+ OR (NEW.role = 'owner' AND NEW.principal_id = 'service_managed_network_owner'
16
+ AND EXISTS (
17
+ SELECT 1 FROM principals p JOIN service_accounts s ON s.principal_id=p.id
18
+ JOIN teams t ON t.id=NEW.team_id
19
+ JOIN managed_host_bindings b ON b.singleton=1
20
+ WHERE p.id=NEW.principal_id AND p.kind='service' AND p.status='active'
21
+ AND s.service_identifier='agentsdock.team-hub.managed-network-owner'
22
+ AND t.kind='shared' AND t.created_by_principal_id=p.id
23
+ ))
24
+ )
25
+ BEGIN
26
+ SELECT RAISE(ABORT, 'membership role is incompatible with principal kind');
27
+ END;
28
+
29
+ CREATE TRIGGER memberships_update_requires_compatible_principal
30
+ BEFORE UPDATE OF principal_id, role ON memberships
31
+ FOR EACH ROW WHEN NOT (
32
+ (NEW.role = 'automation' AND COALESCE(
33
+ (SELECT kind FROM principals WHERE id = NEW.principal_id), ''
34
+ ) = 'service')
35
+ OR (NEW.role <> 'automation' AND COALESCE(
36
+ (SELECT kind FROM principals WHERE id = NEW.principal_id), ''
37
+ ) = 'human')
38
+ OR (NEW.role = 'owner' AND NEW.principal_id = 'service_managed_network_owner'
39
+ AND EXISTS (
40
+ SELECT 1 FROM principals p JOIN service_accounts s ON s.principal_id=p.id
41
+ JOIN teams t ON t.id=NEW.team_id
42
+ JOIN managed_host_bindings b ON b.singleton=1
43
+ WHERE p.id=NEW.principal_id AND p.kind='service' AND p.status='active'
44
+ AND s.service_identifier='agentsdock.team-hub.managed-network-owner'
45
+ AND t.kind='shared' AND t.created_by_principal_id=p.id
46
+ ))
47
+ )
48
+ BEGIN
49
+ SELECT RAISE(ABORT, 'membership role is incompatible with principal kind');
50
+ END;
@@ -0,0 +1,3 @@
1
+ -- Inbox removal belongs to one addressed mailbox, never the shared message.
2
+ ALTER TABLE team_message_recipients ADD COLUMN dismissed_at INTEGER
3
+ CHECK(dismissed_at IS NULL OR dismissed_at >= 0);
@@ -0,0 +1,4 @@
1
+ -- Explicitly mark new all-server inbox mail. Historical shared Bulletin rows
2
+ -- retain NULL and are never expanded or reclassified as mail.
3
+ ALTER TABLE team_messages ADD COLUMN destination TEXT
4
+ CHECK(destination IS NULL OR destination = 'all_servers');
@@ -0,0 +1,16 @@
1
+ -- A skill version's announcement may be hidden independently of the library.
2
+ -- Keep the original message, immutable versions, and bound attachments intact.
3
+ -- Poster authorization remains in HubStore; the journal retains its existing
4
+ -- same-team actor and append-only constraints.
5
+ DROP TRIGGER network_content_deletions_require_message;
6
+
7
+ CREATE TRIGGER network_content_deletions_require_message
8
+ BEFORE INSERT ON network_content_deletions
9
+ FOR EACH ROW WHEN NEW.resource_kind = 'message' AND NOT EXISTS (
10
+ SELECT 1 FROM team_messages AS m
11
+ WHERE m.team_id = NEW.team_id AND m.id = NEW.resource_id
12
+ AND m.kind IN ('message', 'skill')
13
+ )
14
+ BEGIN
15
+ SELECT RAISE(ABORT, 'team message deletion source is unavailable');
16
+ END;
@@ -0,0 +1,8 @@
1
+ -- Subjects are opt-in on the wire. Never backfill the legacy title column:
2
+ -- old ordinary-message titles were intentionally hidden from strict clients.
3
+ -- The existing whole-row immutability trigger also protects this new column.
4
+ ALTER TABLE team_messages ADD COLUMN mail_subject TEXT
5
+ CHECK(mail_subject IS NULL OR (
6
+ kind = 'message' AND typeof(mail_subject) = 'text'
7
+ AND length(mail_subject) BETWEEN 1 AND 160
8
+ ));
@@ -0,0 +1,5 @@
1
+ -- Inbox attention is independent of historical delivery/read receipts.
2
+ ALTER TABLE team_message_recipients ADD COLUMN inbox_unread INTEGER
3
+ CHECK(inbox_unread IS NULL OR inbox_unread IN (0, 1));
4
+ ALTER TABLE team_message_recipients ADD COLUMN mailbox_state_version INTEGER NOT NULL DEFAULT 0
5
+ CHECK(mailbox_state_version >= 0);