@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.
- package/LICENSE +202 -0
- package/NOTICE +19 -0
- package/README.md +60 -0
- package/npm/cli.cjs +289 -0
- package/package.json +32 -0
- package/server/LICENSE +202 -0
- package/server/NOTICE +19 -0
- package/server/VERSION +1 -0
- package/server/activation_transaction.py +2458 -0
- package/server/agent_server.py +93226 -0
- package/server/agentsdock_chats.py +1237 -0
- package/server/agentsdock_emergency.py +338 -0
- package/server/agentsdock_jobs.py +640 -0
- package/server/agentsdock_mail.py +325 -0
- package/server/agentsdock_publish.py +404 -0
- package/server/agentsdock_team.py +793 -0
- package/server/agentsdock_team_hub/__init__.py +33 -0
- package/server/agentsdock_team_hub/auth.py +710 -0
- package/server/agentsdock_team_hub/cli.py +647 -0
- package/server/agentsdock_team_hub/database.py +310 -0
- package/server/agentsdock_team_hub/mail_hint_streams.py +176 -0
- package/server/agentsdock_team_hub/mail_hints.py +201 -0
- package/server/agentsdock_team_hub/migrations/0001_identity_auth.sql +690 -0
- package/server/agentsdock_team_hub/migrations/0002_teamspace_ledger.sql +1336 -0
- package/server/agentsdock_team_hub/migrations/0003_service_runtime.sql +361 -0
- package/server/agentsdock_team_hub/migrations/0004_managed_host_binding.sql +22 -0
- package/server/agentsdock_team_hub/migrations/0005_tailnet_bootstrap_delegations.sql +63 -0
- package/server/agentsdock_team_hub/migrations/0006_team_network_mailbox.sql +423 -0
- package/server/agentsdock_team_hub/migrations/0007_local_agent_mail.sql +63 -0
- package/server/agentsdock_team_hub/migrations/0008_managed_server_session.sql +73 -0
- package/server/agentsdock_team_hub/migrations/0009_team_messages.sql +324 -0
- package/server/agentsdock_team_hub/migrations/0010_team_attachment_orphan_reclamation.sql +52 -0
- package/server/agentsdock_team_hub/migrations/0011_human_admin_paging.sql +65 -0
- package/server/agentsdock_team_hub/migrations/0012_network_content_deletions.sql +69 -0
- package/server/agentsdock_team_hub/migrations/0013_team_message_revisions.sql +48 -0
- package/server/agentsdock_team_hub/migrations/0014_managed_network_owner.sql +50 -0
- package/server/agentsdock_team_hub/migrations/0015_team_message_inbox_dismissals.sql +3 -0
- package/server/agentsdock_team_hub/migrations/0016_team_message_all_servers.sql +4 -0
- package/server/agentsdock_team_hub/migrations/0017_skill_announcement_deletions.sql +16 -0
- package/server/agentsdock_team_hub/migrations/0018_team_mail_subjects.sql +8 -0
- package/server/agentsdock_team_hub/migrations/0019_team_mailbox_state.sql +5 -0
- package/server/agentsdock_team_hub/migrations/0020_team_mail_arrivals.sql +44 -0
- package/server/agentsdock_team_hub/migrations/0021_team_mail_threads.sql +4 -0
- package/server/agentsdock_team_hub/migrations/0022_team_bulletin_changes.sql +49 -0
- package/server/agentsdock_team_hub/migrations/0023_team_message_search.sql +93 -0
- package/server/agentsdock_team_hub/migrations/__init__.py +1 -0
- package/server/agentsdock_team_hub/notification_hints.py +306 -0
- package/server/agentsdock_team_hub/secure_peer.py +11215 -0
- package/server/agentsdock_team_hub/secure_peer_hub.py +1372 -0
- package/server/agentsdock_team_hub/security.py +318 -0
- package/server/agentsdock_team_hub/service.py +1737 -0
- package/server/agentsdock_team_hub/store.py +16276 -0
- package/server/chat_mailbox.py +448 -0
- package/server/claude_background_reconciliation.py +92 -0
- package/server/claude_goals.py +245 -0
- package/server/claude_history_provenance.py +137 -0
- package/server/claude_history_repair.py +1601 -0
- package/server/claude_sdk_client.py +3731 -0
- package/server/claude_side_question.py +212 -0
- package/server/codex_app_server.py +3030 -0
- package/server/codex_auth.py +93 -0
- package/server/codex_history_repair.py +1247 -0
- package/server/codex_provider.py +1120 -0
- package/server/codex_side_question.py +301 -0
- package/server/cursor_agent_client.py +654 -0
- package/server/cursor_process_guard.py +347 -0
- package/server/execution_activation.py +610 -0
- package/server/execution_control.py +233 -0
- package/server/execution_durability.py +184 -0
- package/server/execution_http.py +256 -0
- package/server/execution_install.py +792 -0
- package/server/execution_legacy_runner.py +406 -0
- package/server/execution_maintenance.py +137 -0
- package/server/execution_manage.py +615 -0
- package/server/execution_ownership.py +83 -0
- package/server/execution_preparation.py +362 -0
- package/server/execution_recovery.py +845 -0
- package/server/execution_recovery_status.py +195 -0
- package/server/execution_service.py +387 -0
- package/server/execution_transport.py +622 -0
- package/server/execution_uninstall.py +406 -0
- package/server/execution_update_status.py +49 -0
- package/server/install.sh +7074 -0
- package/server/interactive_chat_controls.py +153 -0
- package/server/interactive_chat_native.py +66 -0
- package/server/interactive_chat_projection.py +152 -0
- package/server/interactive_chat_runtime.py +132 -0
- package/server/interactive_chat_share_routes.py +534 -0
- package/server/interactive_chat_share_web.py +12 -0
- package/server/interactive_chat_shares.py +238 -0
- package/server/provider_commands.py +425 -0
- package/server/public_chat_share_routes.py +426 -0
- package/server/public_chat_shares.py +759 -0
- package/server/public_chat_transcript.py +357 -0
- package/server/pyproject.toml +29 -0
- package/server/release-public-key.pem +3 -0
- package/server/secure_peer_delivery.py +1236 -0
- package/server/secure_peer_runtime.py +7694 -0
- package/server/shared_chat_video_stream.py +262 -0
- package/server/shared_chat_videos.py +155 -0
- package/server/side_questions.py +594 -0
- package/server/team_hub_host.py +1354 -0
- package/server/team_mail_grants.py +136 -0
- package/server/team_mail_runtime.py +302 -0
- package/server/team_mail_websocket.py +211 -0
- package/server/title_generation.py +333 -0
- package/server/uninstall.sh +359 -0
- package/server/update_handoff.py +237 -0
- package/server/update_preparation.py +264 -0
- package/server/update_recovery.py +140 -0
- package/server/update_runner.py +1819 -0
- package/server/uv.lock +1101 -0
- package/server/workspace_git.py +545 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
-- Team Hub identity and authentication roots. Secret material is represented
|
|
2
|
+
-- only by one-way hashes; node private keys and AgentsServer bearers never
|
|
3
|
+
-- belong in this database.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE principals (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
kind TEXT NOT NULL CHECK(kind IN ('human', 'service', 'node', 'agent', 'chat')),
|
|
8
|
+
scope_team_id TEXT,
|
|
9
|
+
display_name TEXT NOT NULL CHECK(length(trim(display_name)) BETWEEN 1 AND 160),
|
|
10
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'suspended', 'revoked')),
|
|
11
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
12
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
|
|
13
|
+
UNIQUE(scope_team_id, id),
|
|
14
|
+
CHECK(
|
|
15
|
+
(kind IN ('human', 'service') AND scope_team_id IS NULL)
|
|
16
|
+
OR (kind IN ('node', 'agent', 'chat') AND scope_team_id IS NOT NULL)
|
|
17
|
+
),
|
|
18
|
+
FOREIGN KEY(scope_team_id) REFERENCES teams(id) ON DELETE CASCADE
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE TRIGGER principals_scope_is_immutable
|
|
22
|
+
BEFORE UPDATE OF id, kind, scope_team_id, created_at ON principals
|
|
23
|
+
BEGIN
|
|
24
|
+
SELECT RAISE(ABORT, 'principal identity and scope are immutable');
|
|
25
|
+
END;
|
|
26
|
+
|
|
27
|
+
CREATE TRIGGER principal_revocation_is_terminal
|
|
28
|
+
BEFORE UPDATE OF status ON principals
|
|
29
|
+
FOR EACH ROW WHEN OLD.status = 'revoked' AND NEW.status <> 'revoked'
|
|
30
|
+
BEGIN
|
|
31
|
+
SELECT RAISE(ABORT, 'principal revocation is terminal');
|
|
32
|
+
END;
|
|
33
|
+
|
|
34
|
+
CREATE TABLE human_accounts (
|
|
35
|
+
principal_id TEXT PRIMARY KEY REFERENCES principals(id) ON DELETE RESTRICT,
|
|
36
|
+
email_normalized TEXT NOT NULL UNIQUE
|
|
37
|
+
CHECK(email_normalized = lower(trim(email_normalized)))
|
|
38
|
+
CHECK(length(email_normalized) BETWEEN 3 AND 320),
|
|
39
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TRIGGER human_accounts_require_human_principal
|
|
43
|
+
BEFORE INSERT ON human_accounts
|
|
44
|
+
FOR EACH ROW WHEN COALESCE(
|
|
45
|
+
(SELECT kind FROM principals WHERE id = NEW.principal_id), ''
|
|
46
|
+
) <> 'human'
|
|
47
|
+
BEGIN
|
|
48
|
+
SELECT RAISE(ABORT, 'human account requires a human principal');
|
|
49
|
+
END;
|
|
50
|
+
|
|
51
|
+
CREATE TRIGGER human_account_identity_is_immutable
|
|
52
|
+
BEFORE UPDATE OF principal_id, email_normalized, created_at ON human_accounts
|
|
53
|
+
BEGIN
|
|
54
|
+
SELECT RAISE(ABORT, 'human account identity is immutable');
|
|
55
|
+
END;
|
|
56
|
+
|
|
57
|
+
CREATE TABLE service_accounts (
|
|
58
|
+
principal_id TEXT PRIMARY KEY REFERENCES principals(id) ON DELETE RESTRICT,
|
|
59
|
+
service_identifier TEXT NOT NULL UNIQUE
|
|
60
|
+
CHECK(length(service_identifier) BETWEEN 1 AND 160),
|
|
61
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TRIGGER service_accounts_require_service_principal
|
|
65
|
+
BEFORE INSERT ON service_accounts
|
|
66
|
+
FOR EACH ROW WHEN COALESCE(
|
|
67
|
+
(SELECT kind FROM principals WHERE id = NEW.principal_id), ''
|
|
68
|
+
) <> 'service'
|
|
69
|
+
BEGIN
|
|
70
|
+
SELECT RAISE(ABORT, 'service account requires a service principal');
|
|
71
|
+
END;
|
|
72
|
+
|
|
73
|
+
CREATE TRIGGER service_account_identity_is_immutable
|
|
74
|
+
BEFORE UPDATE OF principal_id, service_identifier, created_at ON service_accounts
|
|
75
|
+
BEGIN
|
|
76
|
+
SELECT RAISE(ABORT, 'service account identity is immutable');
|
|
77
|
+
END;
|
|
78
|
+
|
|
79
|
+
CREATE TABLE teams (
|
|
80
|
+
id TEXT PRIMARY KEY,
|
|
81
|
+
kind TEXT NOT NULL CHECK(kind IN ('personal', 'shared')),
|
|
82
|
+
slug TEXT NOT NULL UNIQUE
|
|
83
|
+
CHECK(slug = lower(trim(slug)))
|
|
84
|
+
CHECK(length(slug) BETWEEN 3 AND 80),
|
|
85
|
+
display_name TEXT NOT NULL CHECK(length(trim(display_name)) BETWEEN 1 AND 160),
|
|
86
|
+
personal_owner_principal_id TEXT REFERENCES human_accounts(principal_id) ON DELETE RESTRICT,
|
|
87
|
+
retention_days INTEGER NOT NULL DEFAULT 365 CHECK(retention_days BETWEEN 1 AND 3650),
|
|
88
|
+
created_by_principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
|
|
89
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
90
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
|
|
91
|
+
UNIQUE(personal_owner_principal_id),
|
|
92
|
+
UNIQUE(id, personal_owner_principal_id),
|
|
93
|
+
CHECK(
|
|
94
|
+
(kind = 'personal' AND personal_owner_principal_id IS NOT NULL)
|
|
95
|
+
OR (kind = 'shared' AND personal_owner_principal_id IS NULL)
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE TRIGGER team_identity_is_immutable
|
|
100
|
+
BEFORE UPDATE OF id, kind, slug, personal_owner_principal_id,
|
|
101
|
+
created_by_principal_id, created_at
|
|
102
|
+
ON teams
|
|
103
|
+
BEGIN
|
|
104
|
+
SELECT RAISE(ABORT, 'team identity and personal owner are immutable');
|
|
105
|
+
END;
|
|
106
|
+
|
|
107
|
+
CREATE TABLE memberships (
|
|
108
|
+
id TEXT PRIMARY KEY,
|
|
109
|
+
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
110
|
+
principal_id TEXT NOT NULL REFERENCES principals(id) ON DELETE RESTRICT,
|
|
111
|
+
role TEXT NOT NULL CHECK(role IN ('owner', 'admin', 'member', 'guest', 'automation')),
|
|
112
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'suspended', 'revoked')),
|
|
113
|
+
invited_by_principal_id TEXT REFERENCES principals(id) ON DELETE SET NULL,
|
|
114
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
115
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
|
|
116
|
+
UNIQUE(team_id, principal_id),
|
|
117
|
+
UNIQUE(team_id, id)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
CREATE UNIQUE INDEX memberships_one_active_owner_per_team
|
|
121
|
+
ON memberships(team_id)
|
|
122
|
+
WHERE role = 'owner' AND status = 'active';
|
|
123
|
+
|
|
124
|
+
CREATE TRIGGER memberships_require_compatible_principal
|
|
125
|
+
BEFORE INSERT ON memberships
|
|
126
|
+
FOR EACH ROW WHEN
|
|
127
|
+
(NEW.role = 'automation' AND COALESCE(
|
|
128
|
+
(SELECT kind FROM principals WHERE id = NEW.principal_id), ''
|
|
129
|
+
) <> 'service')
|
|
130
|
+
OR
|
|
131
|
+
(NEW.role <> 'automation' AND COALESCE(
|
|
132
|
+
(SELECT kind FROM principals WHERE id = NEW.principal_id), ''
|
|
133
|
+
) <> 'human')
|
|
134
|
+
BEGIN
|
|
135
|
+
SELECT RAISE(ABORT, 'membership role is incompatible with principal kind');
|
|
136
|
+
END;
|
|
137
|
+
|
|
138
|
+
CREATE TRIGGER memberships_update_requires_compatible_principal
|
|
139
|
+
BEFORE UPDATE OF principal_id, role ON memberships
|
|
140
|
+
FOR EACH ROW WHEN
|
|
141
|
+
(NEW.role = 'automation' AND COALESCE(
|
|
142
|
+
(SELECT kind FROM principals WHERE id = NEW.principal_id), ''
|
|
143
|
+
) <> 'service')
|
|
144
|
+
OR
|
|
145
|
+
(NEW.role <> 'automation' AND COALESCE(
|
|
146
|
+
(SELECT kind FROM principals WHERE id = NEW.principal_id), ''
|
|
147
|
+
) <> 'human')
|
|
148
|
+
BEGIN
|
|
149
|
+
SELECT RAISE(ABORT, 'membership role is incompatible with principal kind');
|
|
150
|
+
END;
|
|
151
|
+
|
|
152
|
+
CREATE TRIGGER memberships_identity_is_immutable
|
|
153
|
+
BEFORE UPDATE OF id, team_id, principal_id, invited_by_principal_id, created_at
|
|
154
|
+
ON memberships
|
|
155
|
+
BEGIN
|
|
156
|
+
SELECT RAISE(ABORT, 'membership identity is immutable');
|
|
157
|
+
END;
|
|
158
|
+
|
|
159
|
+
CREATE TRIGGER membership_revocation_is_terminal
|
|
160
|
+
BEFORE UPDATE OF status ON memberships
|
|
161
|
+
FOR EACH ROW WHEN OLD.status = 'revoked' AND NEW.status <> 'revoked'
|
|
162
|
+
BEGIN
|
|
163
|
+
SELECT RAISE(ABORT, 'membership revocation is terminal');
|
|
164
|
+
END;
|
|
165
|
+
|
|
166
|
+
CREATE TRIGGER memberships_cannot_be_deleted
|
|
167
|
+
BEFORE DELETE ON memberships
|
|
168
|
+
BEGIN
|
|
169
|
+
SELECT RAISE(ABORT, 'membership ledger rows cannot be deleted');
|
|
170
|
+
END;
|
|
171
|
+
|
|
172
|
+
CREATE TABLE invitations (
|
|
173
|
+
id TEXT PRIMARY KEY,
|
|
174
|
+
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
175
|
+
token_hash BLOB NOT NULL UNIQUE
|
|
176
|
+
CHECK(typeof(token_hash) = 'blob' AND length(token_hash) = 32),
|
|
177
|
+
invitee_email_normalized TEXT
|
|
178
|
+
CHECK(invitee_email_normalized IS NULL OR (
|
|
179
|
+
invitee_email_normalized = lower(trim(invitee_email_normalized))
|
|
180
|
+
AND length(invitee_email_normalized) BETWEEN 3 AND 320
|
|
181
|
+
)),
|
|
182
|
+
role TEXT NOT NULL CHECK(role IN ('admin', 'member', 'guest')),
|
|
183
|
+
issued_by_principal_id TEXT NOT NULL,
|
|
184
|
+
expires_at INTEGER NOT NULL CHECK(expires_at >= 0),
|
|
185
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0 AND expires_at > created_at),
|
|
186
|
+
redeemed_at INTEGER CHECK(redeemed_at IS NULL OR redeemed_at >= created_at),
|
|
187
|
+
redeemed_by_principal_id TEXT REFERENCES human_accounts(principal_id) ON DELETE RESTRICT,
|
|
188
|
+
revoked_at INTEGER CHECK(revoked_at IS NULL OR revoked_at >= created_at),
|
|
189
|
+
FOREIGN KEY(team_id, issued_by_principal_id)
|
|
190
|
+
REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
|
|
191
|
+
CHECK((redeemed_at IS NULL) = (redeemed_by_principal_id IS NULL)),
|
|
192
|
+
CHECK(redeemed_at IS NULL OR revoked_at IS NULL)
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
CREATE TRIGGER invitation_authority_is_immutable
|
|
196
|
+
BEFORE UPDATE OF id, team_id, token_hash, invitee_email_normalized,
|
|
197
|
+
role, issued_by_principal_id, expires_at, created_at
|
|
198
|
+
ON invitations
|
|
199
|
+
BEGIN
|
|
200
|
+
SELECT RAISE(ABORT, 'invitation authority is immutable');
|
|
201
|
+
END;
|
|
202
|
+
|
|
203
|
+
CREATE TRIGGER invitation_terminal_state_is_one_way
|
|
204
|
+
BEFORE UPDATE ON invitations
|
|
205
|
+
FOR EACH ROW WHEN
|
|
206
|
+
(
|
|
207
|
+
OLD.redeemed_at IS NOT NULL
|
|
208
|
+
AND (
|
|
209
|
+
NEW.redeemed_at IS NOT OLD.redeemed_at
|
|
210
|
+
OR NEW.redeemed_by_principal_id IS NOT OLD.redeemed_by_principal_id
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
OR (OLD.revoked_at IS NOT NULL AND NEW.revoked_at IS NOT OLD.revoked_at)
|
|
214
|
+
BEGIN
|
|
215
|
+
SELECT RAISE(ABORT, 'invitation terminal state is one-way');
|
|
216
|
+
END;
|
|
217
|
+
|
|
218
|
+
CREATE TRIGGER invitations_cannot_be_deleted
|
|
219
|
+
BEFORE DELETE ON invitations
|
|
220
|
+
BEGIN
|
|
221
|
+
SELECT RAISE(ABORT, 'invitation ledger rows cannot be deleted');
|
|
222
|
+
END;
|
|
223
|
+
|
|
224
|
+
CREATE INDEX invitations_team_expiry
|
|
225
|
+
ON invitations(team_id, expires_at)
|
|
226
|
+
WHERE redeemed_at IS NULL AND revoked_at IS NULL;
|
|
227
|
+
|
|
228
|
+
CREATE TABLE device_sessions (
|
|
229
|
+
id TEXT PRIMARY KEY,
|
|
230
|
+
human_principal_id TEXT NOT NULL REFERENCES human_accounts(principal_id) ON DELETE CASCADE,
|
|
231
|
+
device_label TEXT NOT NULL CHECK(length(trim(device_label)) BETWEEN 1 AND 160),
|
|
232
|
+
device_public_key TEXT CHECK(device_public_key IS NULL OR length(device_public_key) BETWEEN 32 AND 16384),
|
|
233
|
+
refresh_generation INTEGER NOT NULL DEFAULT 0 CHECK(refresh_generation >= 0),
|
|
234
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
235
|
+
last_seen_at INTEGER NOT NULL CHECK(last_seen_at >= created_at),
|
|
236
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
|
|
237
|
+
revoked_at INTEGER CHECK(revoked_at IS NULL OR revoked_at >= created_at)
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
CREATE TRIGGER device_session_authority_is_immutable
|
|
241
|
+
BEFORE UPDATE OF id, human_principal_id, device_public_key, created_at, expires_at
|
|
242
|
+
ON device_sessions
|
|
243
|
+
BEGIN
|
|
244
|
+
SELECT RAISE(ABORT, 'device session authority is immutable');
|
|
245
|
+
END;
|
|
246
|
+
|
|
247
|
+
CREATE TRIGGER device_session_counters_are_monotonic
|
|
248
|
+
BEFORE UPDATE ON device_sessions
|
|
249
|
+
FOR EACH ROW WHEN
|
|
250
|
+
NEW.refresh_generation < OLD.refresh_generation
|
|
251
|
+
OR NEW.last_seen_at < OLD.last_seen_at
|
|
252
|
+
OR (OLD.revoked_at IS NOT NULL AND NEW.revoked_at IS NOT OLD.revoked_at)
|
|
253
|
+
BEGIN
|
|
254
|
+
SELECT RAISE(ABORT, 'device session counters and revocation are monotonic');
|
|
255
|
+
END;
|
|
256
|
+
|
|
257
|
+
CREATE TRIGGER device_sessions_cannot_be_deleted
|
|
258
|
+
BEFORE DELETE ON device_sessions
|
|
259
|
+
BEGIN
|
|
260
|
+
SELECT RAISE(ABORT, 'device session ledger rows cannot be deleted');
|
|
261
|
+
END;
|
|
262
|
+
|
|
263
|
+
CREATE TABLE refresh_tokens (
|
|
264
|
+
id TEXT PRIMARY KEY,
|
|
265
|
+
device_session_id TEXT NOT NULL REFERENCES device_sessions(id) ON DELETE CASCADE,
|
|
266
|
+
token_hash BLOB NOT NULL UNIQUE
|
|
267
|
+
CHECK(typeof(token_hash) = 'blob' AND length(token_hash) = 32),
|
|
268
|
+
generation INTEGER NOT NULL CHECK(generation >= 0),
|
|
269
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
270
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
|
|
271
|
+
consumed_at INTEGER CHECK(consumed_at IS NULL OR consumed_at >= created_at),
|
|
272
|
+
replaced_by_token_id TEXT,
|
|
273
|
+
revoked_at INTEGER CHECK(revoked_at IS NULL OR revoked_at >= created_at),
|
|
274
|
+
UNIQUE(device_session_id, generation),
|
|
275
|
+
UNIQUE(device_session_id, id),
|
|
276
|
+
FOREIGN KEY(device_session_id, replaced_by_token_id)
|
|
277
|
+
REFERENCES refresh_tokens(device_session_id, id) ON DELETE RESTRICT,
|
|
278
|
+
CHECK((consumed_at IS NULL) = (replaced_by_token_id IS NULL)),
|
|
279
|
+
CHECK(consumed_at IS NULL OR revoked_at IS NULL)
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
CREATE TRIGGER refresh_token_authority_is_immutable
|
|
283
|
+
BEFORE UPDATE OF id, device_session_id, token_hash, generation, created_at, expires_at
|
|
284
|
+
ON refresh_tokens
|
|
285
|
+
BEGIN
|
|
286
|
+
SELECT RAISE(ABORT, 'refresh token authority is immutable');
|
|
287
|
+
END;
|
|
288
|
+
|
|
289
|
+
CREATE TRIGGER refresh_token_terminal_state_is_one_way
|
|
290
|
+
BEFORE UPDATE ON refresh_tokens
|
|
291
|
+
FOR EACH ROW WHEN
|
|
292
|
+
(
|
|
293
|
+
OLD.consumed_at IS NOT NULL
|
|
294
|
+
AND (
|
|
295
|
+
NEW.consumed_at IS NOT OLD.consumed_at
|
|
296
|
+
OR NEW.replaced_by_token_id IS NOT OLD.replaced_by_token_id
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
OR (OLD.revoked_at IS NOT NULL AND NEW.revoked_at IS NOT OLD.revoked_at)
|
|
300
|
+
BEGIN
|
|
301
|
+
SELECT RAISE(ABORT, 'refresh token terminal state is one-way');
|
|
302
|
+
END;
|
|
303
|
+
|
|
304
|
+
CREATE TRIGGER refresh_token_rotation_stays_in_session
|
|
305
|
+
BEFORE UPDATE OF consumed_at, replaced_by_token_id ON refresh_tokens
|
|
306
|
+
FOR EACH ROW WHEN NEW.consumed_at IS NOT NULL AND NOT EXISTS (
|
|
307
|
+
SELECT 1 FROM refresh_tokens AS replacement
|
|
308
|
+
WHERE replacement.device_session_id = OLD.device_session_id
|
|
309
|
+
AND replacement.id = NEW.replaced_by_token_id
|
|
310
|
+
AND replacement.generation = OLD.generation + 1
|
|
311
|
+
AND replacement.consumed_at IS NULL
|
|
312
|
+
AND replacement.revoked_at IS NULL
|
|
313
|
+
)
|
|
314
|
+
BEGIN
|
|
315
|
+
SELECT RAISE(ABORT, 'refresh token rotation requires the next token in this session');
|
|
316
|
+
END;
|
|
317
|
+
|
|
318
|
+
CREATE TRIGGER refresh_tokens_cannot_be_deleted
|
|
319
|
+
BEFORE DELETE ON refresh_tokens
|
|
320
|
+
BEGIN
|
|
321
|
+
SELECT RAISE(ABORT, 'refresh token ledger rows cannot be deleted');
|
|
322
|
+
END;
|
|
323
|
+
|
|
324
|
+
CREATE TABLE access_token_revocations (
|
|
325
|
+
jti_hash BLOB PRIMARY KEY
|
|
326
|
+
CHECK(typeof(jti_hash) = 'blob' AND length(jti_hash) = 32),
|
|
327
|
+
device_session_id TEXT NOT NULL REFERENCES device_sessions(id) ON DELETE CASCADE,
|
|
328
|
+
expires_at INTEGER NOT NULL CHECK(expires_at >= 0),
|
|
329
|
+
revoked_at INTEGER NOT NULL CHECK(revoked_at >= 0),
|
|
330
|
+
reason TEXT NOT NULL CHECK(length(trim(reason)) BETWEEN 1 AND 240)
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
CREATE TRIGGER access_token_revocations_are_immutable_update
|
|
334
|
+
BEFORE UPDATE ON access_token_revocations
|
|
335
|
+
BEGIN
|
|
336
|
+
SELECT RAISE(ABORT, 'access token revocations are immutable');
|
|
337
|
+
END;
|
|
338
|
+
|
|
339
|
+
CREATE TRIGGER access_token_revocations_are_immutable_delete
|
|
340
|
+
BEFORE DELETE ON access_token_revocations
|
|
341
|
+
BEGIN
|
|
342
|
+
SELECT RAISE(ABORT, 'access token revocations are immutable');
|
|
343
|
+
END;
|
|
344
|
+
|
|
345
|
+
CREATE TABLE node_enrollment_grants (
|
|
346
|
+
id TEXT PRIMARY KEY,
|
|
347
|
+
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
348
|
+
token_hash BLOB NOT NULL UNIQUE
|
|
349
|
+
CHECK(typeof(token_hash) = 'blob' AND length(token_hash) = 32),
|
|
350
|
+
issued_by_principal_id TEXT NOT NULL,
|
|
351
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
352
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
|
|
353
|
+
consumed_at INTEGER CHECK(consumed_at IS NULL OR consumed_at >= created_at),
|
|
354
|
+
consumed_by_node_id TEXT,
|
|
355
|
+
revoked_at INTEGER CHECK(revoked_at IS NULL OR revoked_at >= created_at),
|
|
356
|
+
FOREIGN KEY(team_id, issued_by_principal_id)
|
|
357
|
+
REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
|
|
358
|
+
FOREIGN KEY(team_id, consumed_by_node_id)
|
|
359
|
+
REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
|
|
360
|
+
CHECK((consumed_at IS NULL) = (consumed_by_node_id IS NULL)),
|
|
361
|
+
CHECK(consumed_at IS NULL OR revoked_at IS NULL)
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
CREATE TRIGGER node_enrollment_authority_is_immutable
|
|
365
|
+
BEFORE UPDATE OF id, team_id, token_hash, issued_by_principal_id,
|
|
366
|
+
created_at, expires_at
|
|
367
|
+
ON node_enrollment_grants
|
|
368
|
+
BEGIN
|
|
369
|
+
SELECT RAISE(ABORT, 'node enrollment authority is immutable');
|
|
370
|
+
END;
|
|
371
|
+
|
|
372
|
+
CREATE TRIGGER node_enrollment_terminal_state_is_one_way
|
|
373
|
+
BEFORE UPDATE ON node_enrollment_grants
|
|
374
|
+
FOR EACH ROW WHEN
|
|
375
|
+
(
|
|
376
|
+
OLD.consumed_at IS NOT NULL
|
|
377
|
+
AND (
|
|
378
|
+
NEW.consumed_at IS NOT OLD.consumed_at
|
|
379
|
+
OR NEW.consumed_by_node_id IS NOT OLD.consumed_by_node_id
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
OR (OLD.revoked_at IS NOT NULL AND NEW.revoked_at IS NOT OLD.revoked_at)
|
|
383
|
+
BEGIN
|
|
384
|
+
SELECT RAISE(ABORT, 'node enrollment terminal state is one-way');
|
|
385
|
+
END;
|
|
386
|
+
|
|
387
|
+
CREATE TRIGGER node_enrollment_grants_cannot_be_deleted
|
|
388
|
+
BEFORE DELETE ON node_enrollment_grants
|
|
389
|
+
BEGIN
|
|
390
|
+
SELECT RAISE(ABORT, 'node enrollment ledger rows cannot be deleted');
|
|
391
|
+
END;
|
|
392
|
+
|
|
393
|
+
CREATE TABLE nodes (
|
|
394
|
+
id TEXT PRIMARY KEY,
|
|
395
|
+
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
396
|
+
principal_id TEXT NOT NULL UNIQUE REFERENCES principals(id) ON DELETE RESTRICT,
|
|
397
|
+
server_identity TEXT NOT NULL UNIQUE CHECK(length(trim(server_identity)) BETWEEN 8 AND 240),
|
|
398
|
+
display_name TEXT NOT NULL CHECK(length(trim(display_name)) BETWEEN 1 AND 160),
|
|
399
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'offline', 'suspended', 'revoked')),
|
|
400
|
+
enrolled_at INTEGER NOT NULL CHECK(enrolled_at >= 0),
|
|
401
|
+
last_seen_at INTEGER CHECK(last_seen_at IS NULL OR last_seen_at >= enrolled_at),
|
|
402
|
+
UNIQUE(team_id, id),
|
|
403
|
+
UNIQUE(team_id, principal_id)
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
CREATE TRIGGER nodes_identity_is_immutable
|
|
407
|
+
BEFORE UPDATE OF id, team_id, principal_id, server_identity, enrolled_at ON nodes
|
|
408
|
+
BEGIN
|
|
409
|
+
SELECT RAISE(ABORT, 'node identity is immutable');
|
|
410
|
+
END;
|
|
411
|
+
|
|
412
|
+
CREATE TRIGGER node_revocation_is_terminal
|
|
413
|
+
BEFORE UPDATE OF status ON nodes
|
|
414
|
+
FOR EACH ROW WHEN OLD.status = 'revoked' AND NEW.status <> 'revoked'
|
|
415
|
+
BEGIN
|
|
416
|
+
SELECT RAISE(ABORT, 'node revocation is terminal');
|
|
417
|
+
END;
|
|
418
|
+
|
|
419
|
+
CREATE TRIGGER nodes_require_scoped_node_principal
|
|
420
|
+
BEFORE INSERT ON nodes
|
|
421
|
+
FOR EACH ROW WHEN NOT EXISTS (
|
|
422
|
+
SELECT 1 FROM principals
|
|
423
|
+
WHERE id = NEW.principal_id AND kind = 'node' AND scope_team_id = NEW.team_id
|
|
424
|
+
)
|
|
425
|
+
BEGIN
|
|
426
|
+
SELECT RAISE(ABORT, 'node requires a node principal scoped to the same team');
|
|
427
|
+
END;
|
|
428
|
+
|
|
429
|
+
CREATE TABLE node_credentials (
|
|
430
|
+
id TEXT PRIMARY KEY,
|
|
431
|
+
team_id TEXT NOT NULL,
|
|
432
|
+
node_id TEXT NOT NULL,
|
|
433
|
+
credential_kind TEXT NOT NULL CHECK(credential_kind = 'ed25519'),
|
|
434
|
+
public_material TEXT NOT NULL CHECK(length(public_material) BETWEEN 32 AND 16384),
|
|
435
|
+
fingerprint_sha256 BLOB NOT NULL UNIQUE
|
|
436
|
+
CHECK(typeof(fingerprint_sha256) = 'blob' AND length(fingerprint_sha256) = 32),
|
|
437
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
438
|
+
expires_at INTEGER CHECK(expires_at IS NULL OR expires_at > created_at),
|
|
439
|
+
revoked_at INTEGER CHECK(revoked_at IS NULL OR revoked_at >= created_at),
|
|
440
|
+
FOREIGN KEY(team_id, node_id) REFERENCES nodes(team_id, id) ON DELETE CASCADE,
|
|
441
|
+
UNIQUE(team_id, id)
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
CREATE TRIGGER node_credential_authority_is_immutable
|
|
445
|
+
BEFORE UPDATE OF id, team_id, node_id, credential_kind, public_material,
|
|
446
|
+
fingerprint_sha256, created_at, expires_at
|
|
447
|
+
ON node_credentials
|
|
448
|
+
BEGIN
|
|
449
|
+
SELECT RAISE(ABORT, 'node credential authority is immutable');
|
|
450
|
+
END;
|
|
451
|
+
|
|
452
|
+
CREATE TRIGGER node_credential_revocation_is_one_way
|
|
453
|
+
BEFORE UPDATE OF revoked_at ON node_credentials
|
|
454
|
+
FOR EACH ROW WHEN OLD.revoked_at IS NOT NULL AND NEW.revoked_at IS NOT OLD.revoked_at
|
|
455
|
+
BEGIN
|
|
456
|
+
SELECT RAISE(ABORT, 'node credential revocation is one-way');
|
|
457
|
+
END;
|
|
458
|
+
|
|
459
|
+
CREATE TRIGGER node_credentials_cannot_be_deleted
|
|
460
|
+
BEFORE DELETE ON node_credentials
|
|
461
|
+
BEGIN
|
|
462
|
+
SELECT RAISE(ABORT, 'node credential ledger rows cannot be deleted');
|
|
463
|
+
END;
|
|
464
|
+
|
|
465
|
+
CREATE TABLE legacy_server_bindings (
|
|
466
|
+
id TEXT PRIMARY KEY,
|
|
467
|
+
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
468
|
+
server_identity TEXT NOT NULL UNIQUE CHECK(length(trim(server_identity)) BETWEEN 8 AND 240),
|
|
469
|
+
node_id TEXT,
|
|
470
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
471
|
+
FOREIGN KEY(team_id, node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
|
|
472
|
+
UNIQUE(team_id, id)
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
CREATE TRIGGER legacy_server_binding_identity_is_immutable
|
|
476
|
+
BEFORE UPDATE OF id, team_id, server_identity, created_at ON legacy_server_bindings
|
|
477
|
+
BEGIN
|
|
478
|
+
SELECT RAISE(ABORT, 'legacy server binding identity is immutable');
|
|
479
|
+
END;
|
|
480
|
+
|
|
481
|
+
CREATE TRIGGER legacy_server_binding_node_is_one_way
|
|
482
|
+
BEFORE UPDATE OF node_id ON legacy_server_bindings
|
|
483
|
+
FOR EACH ROW WHEN OLD.node_id IS NOT NULL OR NEW.node_id IS NULL
|
|
484
|
+
BEGIN
|
|
485
|
+
SELECT RAISE(ABORT, 'legacy server node binding is one-way');
|
|
486
|
+
END;
|
|
487
|
+
|
|
488
|
+
CREATE TABLE agents (
|
|
489
|
+
id TEXT PRIMARY KEY,
|
|
490
|
+
team_id TEXT NOT NULL,
|
|
491
|
+
principal_id TEXT NOT NULL UNIQUE REFERENCES principals(id) ON DELETE RESTRICT,
|
|
492
|
+
node_id TEXT NOT NULL,
|
|
493
|
+
external_agent_id TEXT NOT NULL CHECK(length(trim(external_agent_id)) BETWEEN 1 AND 240),
|
|
494
|
+
backend TEXT NOT NULL CHECK(backend IN ('codex', 'claude', 'other')),
|
|
495
|
+
display_name TEXT NOT NULL CHECK(length(trim(display_name)) BETWEEN 1 AND 160),
|
|
496
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'offline', 'suspended', 'retired')),
|
|
497
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
498
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
|
|
499
|
+
FOREIGN KEY(team_id, node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
|
|
500
|
+
UNIQUE(node_id, external_agent_id),
|
|
501
|
+
UNIQUE(team_id, id),
|
|
502
|
+
UNIQUE(team_id, node_id, id),
|
|
503
|
+
UNIQUE(team_id, principal_id)
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
CREATE TRIGGER agents_identity_is_immutable
|
|
507
|
+
BEFORE UPDATE OF id, team_id, principal_id, node_id, external_agent_id, backend, created_at
|
|
508
|
+
ON agents
|
|
509
|
+
BEGIN
|
|
510
|
+
SELECT RAISE(ABORT, 'agent identity is immutable');
|
|
511
|
+
END;
|
|
512
|
+
|
|
513
|
+
CREATE TRIGGER agent_retirement_is_terminal
|
|
514
|
+
BEFORE UPDATE OF status ON agents
|
|
515
|
+
FOR EACH ROW WHEN OLD.status = 'retired' AND NEW.status <> 'retired'
|
|
516
|
+
BEGIN
|
|
517
|
+
SELECT RAISE(ABORT, 'agent retirement is terminal');
|
|
518
|
+
END;
|
|
519
|
+
|
|
520
|
+
CREATE TRIGGER agents_require_scoped_agent_principal
|
|
521
|
+
BEFORE INSERT ON agents
|
|
522
|
+
FOR EACH ROW WHEN NOT EXISTS (
|
|
523
|
+
SELECT 1 FROM principals
|
|
524
|
+
WHERE id = NEW.principal_id AND kind = 'agent' AND scope_team_id = NEW.team_id
|
|
525
|
+
)
|
|
526
|
+
BEGIN
|
|
527
|
+
SELECT RAISE(ABORT, 'agent requires an agent principal scoped to the same team');
|
|
528
|
+
END;
|
|
529
|
+
|
|
530
|
+
CREATE TABLE chats (
|
|
531
|
+
id TEXT PRIMARY KEY,
|
|
532
|
+
team_id TEXT NOT NULL,
|
|
533
|
+
principal_id TEXT NOT NULL UNIQUE REFERENCES principals(id) ON DELETE RESTRICT,
|
|
534
|
+
node_id TEXT NOT NULL,
|
|
535
|
+
agent_id TEXT NOT NULL,
|
|
536
|
+
external_chat_id TEXT NOT NULL CHECK(length(trim(external_chat_id)) BETWEEN 1 AND 240),
|
|
537
|
+
display_name TEXT NOT NULL CHECK(length(trim(display_name)) BETWEEN 1 AND 240),
|
|
538
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived', 'deleted')),
|
|
539
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
540
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at),
|
|
541
|
+
FOREIGN KEY(team_id, node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
|
|
542
|
+
FOREIGN KEY(team_id, node_id, agent_id)
|
|
543
|
+
REFERENCES agents(team_id, node_id, id) ON DELETE RESTRICT,
|
|
544
|
+
UNIQUE(node_id, external_chat_id),
|
|
545
|
+
UNIQUE(team_id, id),
|
|
546
|
+
UNIQUE(team_id, node_id, id),
|
|
547
|
+
UNIQUE(team_id, node_id, agent_id, id),
|
|
548
|
+
UNIQUE(team_id, principal_id)
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
CREATE TRIGGER chats_identity_is_immutable
|
|
552
|
+
BEFORE UPDATE OF id, team_id, principal_id, node_id, agent_id, external_chat_id, created_at
|
|
553
|
+
ON chats
|
|
554
|
+
BEGIN
|
|
555
|
+
SELECT RAISE(ABORT, 'chat identity is immutable');
|
|
556
|
+
END;
|
|
557
|
+
|
|
558
|
+
CREATE TRIGGER chat_deletion_is_terminal
|
|
559
|
+
BEFORE UPDATE OF status ON chats
|
|
560
|
+
FOR EACH ROW WHEN OLD.status = 'deleted' AND NEW.status <> 'deleted'
|
|
561
|
+
BEGIN
|
|
562
|
+
SELECT RAISE(ABORT, 'chat deletion is terminal');
|
|
563
|
+
END;
|
|
564
|
+
|
|
565
|
+
CREATE TRIGGER chats_require_scoped_chat_principal
|
|
566
|
+
BEFORE INSERT ON chats
|
|
567
|
+
FOR EACH ROW WHEN NOT EXISTS (
|
|
568
|
+
SELECT 1 FROM principals
|
|
569
|
+
WHERE id = NEW.principal_id AND kind = 'chat' AND scope_team_id = NEW.team_id
|
|
570
|
+
)
|
|
571
|
+
BEGIN
|
|
572
|
+
SELECT RAISE(ABORT, 'chat requires a chat principal scoped to the same team');
|
|
573
|
+
END;
|
|
574
|
+
|
|
575
|
+
CREATE TABLE runs (
|
|
576
|
+
id TEXT PRIMARY KEY,
|
|
577
|
+
team_id TEXT NOT NULL,
|
|
578
|
+
node_id TEXT NOT NULL,
|
|
579
|
+
agent_id TEXT NOT NULL,
|
|
580
|
+
chat_id TEXT NOT NULL,
|
|
581
|
+
acting_human_principal_id TEXT,
|
|
582
|
+
external_run_id TEXT NOT NULL CHECK(length(trim(external_run_id)) BETWEEN 1 AND 240),
|
|
583
|
+
status TEXT NOT NULL CHECK(status IN ('registered', 'running', 'succeeded', 'failed', 'cancelled')),
|
|
584
|
+
started_at INTEGER NOT NULL CHECK(started_at >= 0),
|
|
585
|
+
finished_at INTEGER CHECK(finished_at IS NULL OR finished_at >= started_at),
|
|
586
|
+
FOREIGN KEY(team_id, node_id) REFERENCES nodes(team_id, id) ON DELETE RESTRICT,
|
|
587
|
+
FOREIGN KEY(team_id, node_id, agent_id)
|
|
588
|
+
REFERENCES agents(team_id, node_id, id) ON DELETE RESTRICT,
|
|
589
|
+
FOREIGN KEY(team_id, node_id, chat_id)
|
|
590
|
+
REFERENCES chats(team_id, node_id, id) ON DELETE RESTRICT,
|
|
591
|
+
FOREIGN KEY(team_id, node_id, agent_id, chat_id)
|
|
592
|
+
REFERENCES chats(team_id, node_id, agent_id, id) ON DELETE RESTRICT,
|
|
593
|
+
FOREIGN KEY(team_id, acting_human_principal_id)
|
|
594
|
+
REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
|
|
595
|
+
UNIQUE(node_id, external_run_id),
|
|
596
|
+
UNIQUE(team_id, id)
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
CREATE TRIGGER runs_require_active_human_actor
|
|
600
|
+
BEFORE INSERT ON runs
|
|
601
|
+
FOR EACH ROW WHEN NEW.acting_human_principal_id IS NOT NULL AND NOT EXISTS (
|
|
602
|
+
SELECT 1
|
|
603
|
+
FROM memberships AS m
|
|
604
|
+
JOIN principals AS p ON p.id = m.principal_id
|
|
605
|
+
WHERE m.team_id = NEW.team_id
|
|
606
|
+
AND m.principal_id = NEW.acting_human_principal_id
|
|
607
|
+
AND m.status = 'active'
|
|
608
|
+
AND p.kind = 'human'
|
|
609
|
+
AND p.status = 'active'
|
|
610
|
+
)
|
|
611
|
+
BEGIN
|
|
612
|
+
SELECT RAISE(ABORT, 'run acting user must be an active human member');
|
|
613
|
+
END;
|
|
614
|
+
|
|
615
|
+
CREATE TRIGGER runs_provenance_is_immutable
|
|
616
|
+
BEFORE UPDATE OF id, team_id, node_id, agent_id, chat_id,
|
|
617
|
+
acting_human_principal_id, external_run_id, started_at
|
|
618
|
+
ON runs
|
|
619
|
+
BEGIN
|
|
620
|
+
SELECT RAISE(ABORT, 'run provenance is immutable');
|
|
621
|
+
END;
|
|
622
|
+
|
|
623
|
+
CREATE TABLE turn_capabilities (
|
|
624
|
+
id TEXT PRIMARY KEY,
|
|
625
|
+
team_id TEXT NOT NULL,
|
|
626
|
+
issued_to_run_id TEXT NOT NULL,
|
|
627
|
+
issued_by_principal_id TEXT NOT NULL,
|
|
628
|
+
token_hash BLOB NOT NULL UNIQUE
|
|
629
|
+
CHECK(typeof(token_hash) = 'blob' AND length(token_hash) = 32),
|
|
630
|
+
action TEXT NOT NULL CHECK(length(trim(action)) BETWEEN 1 AND 120),
|
|
631
|
+
resource_type TEXT NOT NULL CHECK(length(trim(resource_type)) BETWEEN 1 AND 80),
|
|
632
|
+
resource_id TEXT NOT NULL CHECK(length(trim(resource_id)) BETWEEN 1 AND 240),
|
|
633
|
+
nonce_hash BLOB NOT NULL UNIQUE
|
|
634
|
+
CHECK(typeof(nonce_hash) = 'blob' AND length(nonce_hash) = 32),
|
|
635
|
+
max_uses INTEGER NOT NULL CHECK(max_uses BETWEEN 1 AND 16),
|
|
636
|
+
used_count INTEGER NOT NULL DEFAULT 0 CHECK(used_count BETWEEN 0 AND max_uses),
|
|
637
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
638
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > created_at),
|
|
639
|
+
revoked_at INTEGER CHECK(revoked_at IS NULL OR revoked_at >= created_at),
|
|
640
|
+
FOREIGN KEY(team_id, issued_to_run_id) REFERENCES runs(team_id, id) ON DELETE CASCADE,
|
|
641
|
+
FOREIGN KEY(team_id, issued_by_principal_id)
|
|
642
|
+
REFERENCES memberships(team_id, principal_id) ON DELETE RESTRICT,
|
|
643
|
+
UNIQUE(team_id, id)
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
CREATE TRIGGER turn_capabilities_require_active_human_issuer
|
|
647
|
+
BEFORE INSERT ON turn_capabilities
|
|
648
|
+
FOR EACH ROW WHEN NOT EXISTS (
|
|
649
|
+
SELECT 1
|
|
650
|
+
FROM memberships AS m
|
|
651
|
+
JOIN principals AS p ON p.id = m.principal_id
|
|
652
|
+
WHERE m.team_id = NEW.team_id
|
|
653
|
+
AND m.principal_id = NEW.issued_by_principal_id
|
|
654
|
+
AND m.status = 'active'
|
|
655
|
+
AND p.kind = 'human'
|
|
656
|
+
AND p.status = 'active'
|
|
657
|
+
)
|
|
658
|
+
BEGIN
|
|
659
|
+
SELECT RAISE(ABORT, 'turn capability issuer must be an active human member');
|
|
660
|
+
END;
|
|
661
|
+
|
|
662
|
+
CREATE TRIGGER turn_capability_authority_is_immutable
|
|
663
|
+
BEFORE UPDATE OF id, team_id, issued_to_run_id, issued_by_principal_id,
|
|
664
|
+
token_hash, action, resource_type, resource_id, nonce_hash, max_uses,
|
|
665
|
+
created_at, expires_at
|
|
666
|
+
ON turn_capabilities
|
|
667
|
+
BEGIN
|
|
668
|
+
SELECT RAISE(ABORT, 'turn capability authority is immutable');
|
|
669
|
+
END;
|
|
670
|
+
|
|
671
|
+
CREATE TRIGGER turn_capability_use_and_revocation_are_monotonic
|
|
672
|
+
BEFORE UPDATE ON turn_capabilities
|
|
673
|
+
FOR EACH ROW WHEN
|
|
674
|
+
NEW.used_count < OLD.used_count
|
|
675
|
+
OR NEW.used_count > OLD.used_count + 1
|
|
676
|
+
OR (NEW.revoked_at IS NOT NULL AND NEW.used_count <> OLD.used_count)
|
|
677
|
+
OR (OLD.revoked_at IS NOT NULL AND NEW.revoked_at IS NOT OLD.revoked_at)
|
|
678
|
+
BEGIN
|
|
679
|
+
SELECT RAISE(ABORT, 'turn capability use and revocation are monotonic');
|
|
680
|
+
END;
|
|
681
|
+
|
|
682
|
+
CREATE TRIGGER turn_capabilities_cannot_be_deleted
|
|
683
|
+
BEFORE DELETE ON turn_capabilities
|
|
684
|
+
BEGIN
|
|
685
|
+
SELECT RAISE(ABORT, 'turn capability ledger rows cannot be deleted');
|
|
686
|
+
END;
|
|
687
|
+
|
|
688
|
+
CREATE INDEX turn_capabilities_live_lookup
|
|
689
|
+
ON turn_capabilities(token_hash, expires_at)
|
|
690
|
+
WHERE revoked_at IS NULL;
|