@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,1236 @@
|
|
|
1
|
+
"""Durable local admission ledger for secure-peer chat deliveries.
|
|
2
|
+
|
|
3
|
+
The authoritative exchange and six-leg counter live on the designated Hub.
|
|
4
|
+
This ledger bridges that remote transaction to one local durable chat turn. A
|
|
5
|
+
delivery is never acknowledged remotely until its exact target route has been
|
|
6
|
+
resolved to a private local chat and this intent has been fsynced. If the
|
|
7
|
+
process dies between the remote CAS and local queue admission, startup resumes
|
|
8
|
+
the saved intent instead of re-running or losing it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import sqlite3
|
|
19
|
+
import stat
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from typing import Any, Iterator, Mapping
|
|
23
|
+
|
|
24
|
+
from agentsdock_team_hub.security import canonical_json, ensure_private_directory
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DELIVERY_STATES = frozenset({
|
|
28
|
+
"prepared",
|
|
29
|
+
"authorized",
|
|
30
|
+
"queued",
|
|
31
|
+
"running",
|
|
32
|
+
"completed",
|
|
33
|
+
"failed",
|
|
34
|
+
})
|
|
35
|
+
TERMINAL_DELIVERY_STATES = frozenset({"completed", "failed"})
|
|
36
|
+
MAX_NONTERMINAL_DELIVERIES = 2_048
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SecurePeerDeliveryLedger:
|
|
40
|
+
"""Owner-private, crash-safe mapping from a relay envelope to one turn."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, path: Path) -> None:
|
|
43
|
+
self.path = Path(path)
|
|
44
|
+
ensure_private_directory(self.path.parent)
|
|
45
|
+
self._lock = threading.RLock()
|
|
46
|
+
self._initialize()
|
|
47
|
+
|
|
48
|
+
def _connect(self) -> sqlite3.Connection:
|
|
49
|
+
if self.path.is_symlink():
|
|
50
|
+
raise PermissionError("secure peer delivery ledger must not be a symlink")
|
|
51
|
+
connection = sqlite3.connect(self.path, timeout=30)
|
|
52
|
+
connection.row_factory = sqlite3.Row
|
|
53
|
+
connection.execute("PRAGMA journal_mode=WAL")
|
|
54
|
+
connection.execute("PRAGMA synchronous=FULL")
|
|
55
|
+
connection.execute("PRAGMA foreign_keys=ON")
|
|
56
|
+
os.chmod(self.path, 0o600)
|
|
57
|
+
info = os.stat(self.path, follow_symlinks=False)
|
|
58
|
+
if (
|
|
59
|
+
not stat.S_ISREG(info.st_mode)
|
|
60
|
+
or info.st_nlink != 1
|
|
61
|
+
or info.st_uid != os.getuid()
|
|
62
|
+
or stat.S_IMODE(info.st_mode) != 0o600
|
|
63
|
+
):
|
|
64
|
+
connection.close()
|
|
65
|
+
raise PermissionError("secure peer delivery ledger is unsafe")
|
|
66
|
+
return connection
|
|
67
|
+
|
|
68
|
+
@contextmanager
|
|
69
|
+
def _transaction(self) -> Iterator[sqlite3.Connection]:
|
|
70
|
+
with self._lock:
|
|
71
|
+
connection = self._connect()
|
|
72
|
+
try:
|
|
73
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
74
|
+
yield connection
|
|
75
|
+
connection.execute("COMMIT")
|
|
76
|
+
except BaseException:
|
|
77
|
+
if connection.in_transaction:
|
|
78
|
+
connection.execute("ROLLBACK")
|
|
79
|
+
raise
|
|
80
|
+
finally:
|
|
81
|
+
connection.close()
|
|
82
|
+
|
|
83
|
+
def _initialize(self) -> None:
|
|
84
|
+
with self._lock:
|
|
85
|
+
connection = self._connect()
|
|
86
|
+
try:
|
|
87
|
+
connection.executescript(
|
|
88
|
+
"""
|
|
89
|
+
CREATE TABLE IF NOT EXISTS secure_peer_deliveries (
|
|
90
|
+
envelope_id TEXT PRIMARY KEY,
|
|
91
|
+
request_id TEXT NOT NULL,
|
|
92
|
+
claim_digest TEXT NOT NULL,
|
|
93
|
+
transport_role TEXT NOT NULL CHECK(transport_role IN ('host','client')),
|
|
94
|
+
connection_id TEXT NOT NULL,
|
|
95
|
+
lease_token TEXT NOT NULL,
|
|
96
|
+
team_id TEXT NOT NULL,
|
|
97
|
+
source_peer_id TEXT,
|
|
98
|
+
source_server_identity TEXT NOT NULL,
|
|
99
|
+
source_route_id TEXT NOT NULL,
|
|
100
|
+
source_route_revision TEXT NOT NULL,
|
|
101
|
+
target_peer_id TEXT,
|
|
102
|
+
target_server_identity TEXT NOT NULL,
|
|
103
|
+
target_route_id TEXT NOT NULL,
|
|
104
|
+
target_route_revision TEXT NOT NULL,
|
|
105
|
+
target_chat_id TEXT NOT NULL,
|
|
106
|
+
action TEXT NOT NULL CHECK(action IN ('instruction','request_reply')),
|
|
107
|
+
kind TEXT NOT NULL CHECK(kind IN ('instruction','request_reply','response')),
|
|
108
|
+
exchange_id TEXT NOT NULL,
|
|
109
|
+
parent_envelope_id TEXT,
|
|
110
|
+
parent_leg INTEGER,
|
|
111
|
+
used_legs INTEGER NOT NULL CHECK(used_legs BETWEEN 1 AND 6),
|
|
112
|
+
max_legs INTEGER NOT NULL CHECK(max_legs=6),
|
|
113
|
+
expires_at INTEGER NOT NULL,
|
|
114
|
+
body_json TEXT NOT NULL,
|
|
115
|
+
state TEXT NOT NULL CHECK(state IN (
|
|
116
|
+
'prepared','authorized','queued','running','completed','failed'
|
|
117
|
+
)),
|
|
118
|
+
queued_id TEXT,
|
|
119
|
+
run_id TEXT,
|
|
120
|
+
admission_retry_at INTEGER,
|
|
121
|
+
admission_attempts INTEGER NOT NULL DEFAULT 0,
|
|
122
|
+
admission_last_error TEXT,
|
|
123
|
+
response_committed INTEGER NOT NULL DEFAULT 0
|
|
124
|
+
CHECK(response_committed IN (0,1)),
|
|
125
|
+
response_request_id TEXT,
|
|
126
|
+
response_request_digest TEXT,
|
|
127
|
+
response_body TEXT,
|
|
128
|
+
response_request_response INTEGER CHECK(
|
|
129
|
+
response_request_response IS NULL
|
|
130
|
+
OR response_request_response IN (0,1)
|
|
131
|
+
),
|
|
132
|
+
response_retry_at INTEGER,
|
|
133
|
+
response_attempts INTEGER NOT NULL DEFAULT 0,
|
|
134
|
+
response_last_error TEXT,
|
|
135
|
+
result_hash TEXT,
|
|
136
|
+
error TEXT,
|
|
137
|
+
created_at INTEGER NOT NULL,
|
|
138
|
+
updated_at INTEGER NOT NULL
|
|
139
|
+
);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS secure_peer_delivery_state
|
|
141
|
+
ON secure_peer_deliveries(state,updated_at);
|
|
142
|
+
CREATE INDEX IF NOT EXISTS secure_peer_delivery_run
|
|
143
|
+
ON secure_peer_deliveries(run_id,state);
|
|
144
|
+
CREATE UNIQUE INDEX IF NOT EXISTS secure_peer_delivery_queued_owner
|
|
145
|
+
ON secure_peer_deliveries(queued_id) WHERE queued_id IS NOT NULL;
|
|
146
|
+
CREATE UNIQUE INDEX IF NOT EXISTS secure_peer_delivery_run_owner
|
|
147
|
+
ON secure_peer_deliveries(run_id) WHERE run_id IS NOT NULL;
|
|
148
|
+
CREATE TABLE IF NOT EXISTS secure_peer_outbound_intents (
|
|
149
|
+
request_id TEXT PRIMARY KEY,
|
|
150
|
+
intent_digest TEXT NOT NULL,
|
|
151
|
+
source_session_id TEXT NOT NULL,
|
|
152
|
+
source_run_id TEXT NOT NULL,
|
|
153
|
+
connection_id TEXT NOT NULL,
|
|
154
|
+
source_route_id TEXT NOT NULL,
|
|
155
|
+
source_route_revision TEXT NOT NULL,
|
|
156
|
+
snapshot_json TEXT NOT NULL,
|
|
157
|
+
body TEXT NOT NULL,
|
|
158
|
+
action TEXT NOT NULL CHECK(action IN ('instruction','request_reply')),
|
|
159
|
+
expires_at INTEGER NOT NULL,
|
|
160
|
+
state TEXT NOT NULL CHECK(state IN ('pending','committed','failed')),
|
|
161
|
+
response_json TEXT,
|
|
162
|
+
retry_at INTEGER,
|
|
163
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
164
|
+
last_error TEXT,
|
|
165
|
+
created_at INTEGER NOT NULL,
|
|
166
|
+
updated_at INTEGER NOT NULL
|
|
167
|
+
);
|
|
168
|
+
CREATE INDEX IF NOT EXISTS secure_peer_outbound_pending
|
|
169
|
+
ON secure_peer_outbound_intents(state,retry_at,created_at);
|
|
170
|
+
"""
|
|
171
|
+
)
|
|
172
|
+
# Development snapshots created before the full immutable
|
|
173
|
+
# claim landed are upgraded additively. No released build has
|
|
174
|
+
# written these rows, but keeping the migration safe avoids a
|
|
175
|
+
# local optional-feature boot failure while iterating.
|
|
176
|
+
columns = {
|
|
177
|
+
str(row["name"])
|
|
178
|
+
for row in connection.execute(
|
|
179
|
+
"PRAGMA table_info(secure_peer_deliveries)"
|
|
180
|
+
).fetchall()
|
|
181
|
+
}
|
|
182
|
+
for name, declaration in (
|
|
183
|
+
("request_id", "TEXT"),
|
|
184
|
+
("lease_token", "TEXT"),
|
|
185
|
+
("team_id", "TEXT"),
|
|
186
|
+
("source_peer_id", "TEXT"),
|
|
187
|
+
("target_peer_id", "TEXT"),
|
|
188
|
+
("parent_leg", "INTEGER"),
|
|
189
|
+
("response_request_id", "TEXT"),
|
|
190
|
+
("response_request_digest", "TEXT"),
|
|
191
|
+
("response_body", "TEXT"),
|
|
192
|
+
("response_request_response", "INTEGER"),
|
|
193
|
+
("response_retry_at", "INTEGER"),
|
|
194
|
+
("response_attempts", "INTEGER NOT NULL DEFAULT 0"),
|
|
195
|
+
("response_last_error", "TEXT"),
|
|
196
|
+
("admission_retry_at", "INTEGER"),
|
|
197
|
+
("admission_attempts", "INTEGER NOT NULL DEFAULT 0"),
|
|
198
|
+
("admission_last_error", "TEXT"),
|
|
199
|
+
):
|
|
200
|
+
if name not in columns:
|
|
201
|
+
connection.execute(
|
|
202
|
+
f"ALTER TABLE secure_peer_deliveries ADD COLUMN {name} {declaration}"
|
|
203
|
+
)
|
|
204
|
+
outbound_columns = {
|
|
205
|
+
str(row["name"])
|
|
206
|
+
for row in connection.execute(
|
|
207
|
+
"PRAGMA table_info(secure_peer_outbound_intents)"
|
|
208
|
+
).fetchall()
|
|
209
|
+
}
|
|
210
|
+
for name in (
|
|
211
|
+
"connection_id",
|
|
212
|
+
"source_route_id",
|
|
213
|
+
"source_route_revision",
|
|
214
|
+
):
|
|
215
|
+
if name not in outbound_columns:
|
|
216
|
+
connection.execute(
|
|
217
|
+
f"ALTER TABLE secure_peer_outbound_intents ADD COLUMN {name} TEXT"
|
|
218
|
+
)
|
|
219
|
+
# Unreleased development ledgers predate the indexed route
|
|
220
|
+
# projection. Recover it only from the already-digested
|
|
221
|
+
# immutable snapshot; malformed rows fail closed.
|
|
222
|
+
legacy_rows = connection.execute(
|
|
223
|
+
"""SELECT request_id,snapshot_json FROM secure_peer_outbound_intents
|
|
224
|
+
WHERE connection_id IS NULL OR source_route_id IS NULL
|
|
225
|
+
OR source_route_revision IS NULL"""
|
|
226
|
+
).fetchall()
|
|
227
|
+
for row in legacy_rows:
|
|
228
|
+
try:
|
|
229
|
+
snapshot = json.loads(row["snapshot_json"])
|
|
230
|
+
connection_id = str(snapshot.get("connection_id") or "")
|
|
231
|
+
source_route_id = str(snapshot.get("source_route_id") or "")
|
|
232
|
+
source_route_revision = str(
|
|
233
|
+
snapshot.get("source_route_revision") or ""
|
|
234
|
+
)
|
|
235
|
+
if not all(
|
|
236
|
+
(connection_id, source_route_id, source_route_revision)
|
|
237
|
+
):
|
|
238
|
+
raise ValueError("missing outbound route identity")
|
|
239
|
+
except Exception:
|
|
240
|
+
connection.execute(
|
|
241
|
+
"""UPDATE secure_peer_outbound_intents SET
|
|
242
|
+
state='failed',retry_at=NULL,
|
|
243
|
+
last_error='invalid legacy secure peer route snapshot',
|
|
244
|
+
connection_id='',source_route_id='',source_route_revision=''
|
|
245
|
+
WHERE request_id=?""",
|
|
246
|
+
(row["request_id"],),
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
connection.execute(
|
|
250
|
+
"""UPDATE secure_peer_outbound_intents SET
|
|
251
|
+
connection_id=?,source_route_id=?,source_route_revision=?
|
|
252
|
+
WHERE request_id=?""",
|
|
253
|
+
(
|
|
254
|
+
connection_id,
|
|
255
|
+
source_route_id,
|
|
256
|
+
source_route_revision,
|
|
257
|
+
row["request_id"],
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
connection.execute(
|
|
261
|
+
"""CREATE INDEX IF NOT EXISTS secure_peer_outbound_source
|
|
262
|
+
ON secure_peer_outbound_intents(state,source_session_id,created_at)"""
|
|
263
|
+
)
|
|
264
|
+
connection.execute(
|
|
265
|
+
"""CREATE INDEX IF NOT EXISTS secure_peer_outbound_connection
|
|
266
|
+
ON secure_peer_outbound_intents(state,connection_id,created_at)"""
|
|
267
|
+
)
|
|
268
|
+
finally:
|
|
269
|
+
connection.close()
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _public(row: sqlite3.Row | None) -> dict[str, Any] | None:
|
|
273
|
+
if row is None:
|
|
274
|
+
return None
|
|
275
|
+
item = dict(row)
|
|
276
|
+
item["body"] = json.loads(item.pop("body_json"))
|
|
277
|
+
return item
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def _claim(
|
|
281
|
+
envelope: Mapping[str, Any],
|
|
282
|
+
*,
|
|
283
|
+
transport_role: str,
|
|
284
|
+
connection_id: str,
|
|
285
|
+
lease_token: str,
|
|
286
|
+
target_chat_id: str,
|
|
287
|
+
) -> tuple[dict[str, Any], str]:
|
|
288
|
+
if transport_role not in {"host", "client"}:
|
|
289
|
+
raise ValueError("secure peer delivery role is invalid")
|
|
290
|
+
action = str(envelope.get("action") or "")
|
|
291
|
+
kind = str(envelope.get("kind") or "")
|
|
292
|
+
if action not in {"instruction", "request_reply"}:
|
|
293
|
+
raise ValueError("secure peer delivery action is invalid")
|
|
294
|
+
if kind not in {"instruction", "request_reply", "response"}:
|
|
295
|
+
raise ValueError("secure peer delivery kind is invalid")
|
|
296
|
+
body = envelope.get("body")
|
|
297
|
+
if (
|
|
298
|
+
not isinstance(body, dict)
|
|
299
|
+
or set(body) != {"message"}
|
|
300
|
+
or not isinstance(body.get("message"), str)
|
|
301
|
+
or not body["message"].strip()
|
|
302
|
+
or len(body["message"]) > 100_000
|
|
303
|
+
):
|
|
304
|
+
raise ValueError("secure peer delivery body is invalid")
|
|
305
|
+
exact = {
|
|
306
|
+
"envelope_id": str(envelope.get("envelope_id") or ""),
|
|
307
|
+
"request_id": str(envelope.get("request_id") or ""),
|
|
308
|
+
"transport_role": transport_role,
|
|
309
|
+
"connection_id": str(connection_id),
|
|
310
|
+
"lease_token": str(lease_token),
|
|
311
|
+
"team_id": str(envelope.get("team_id") or ""),
|
|
312
|
+
"source_peer_id": (
|
|
313
|
+
str(envelope.get("source_peer_id"))
|
|
314
|
+
if envelope.get("source_peer_id") is not None
|
|
315
|
+
else None
|
|
316
|
+
),
|
|
317
|
+
"source_server_identity": str(envelope.get("source_server_identity") or ""),
|
|
318
|
+
"source_route_id": str(envelope.get("source_route_id") or ""),
|
|
319
|
+
"source_route_revision": str(envelope.get("source_route_revision") or ""),
|
|
320
|
+
"target_peer_id": (
|
|
321
|
+
str(envelope.get("target_peer_id"))
|
|
322
|
+
if envelope.get("target_peer_id") is not None
|
|
323
|
+
else None
|
|
324
|
+
),
|
|
325
|
+
"target_server_identity": str(envelope.get("target_server_identity") or ""),
|
|
326
|
+
"target_route_id": str(envelope.get("target_route_id") or ""),
|
|
327
|
+
"target_route_revision": str(envelope.get("target_route_revision") or ""),
|
|
328
|
+
"target_chat_id": str(target_chat_id),
|
|
329
|
+
"action": action,
|
|
330
|
+
"kind": kind,
|
|
331
|
+
"exchange_id": str(envelope.get("exchange_id") or ""),
|
|
332
|
+
"parent_envelope_id": (
|
|
333
|
+
str(envelope.get("parent_envelope_id"))
|
|
334
|
+
if envelope.get("parent_envelope_id") is not None
|
|
335
|
+
else None
|
|
336
|
+
),
|
|
337
|
+
"parent_leg": (
|
|
338
|
+
int(envelope.get("parent_leg"))
|
|
339
|
+
if envelope.get("parent_leg") is not None
|
|
340
|
+
else None
|
|
341
|
+
),
|
|
342
|
+
"used_legs": int(envelope.get("used_legs") or 0),
|
|
343
|
+
"max_legs": int(envelope.get("max_legs") or 0),
|
|
344
|
+
"expires_at": int(envelope.get("expires_at") or 0),
|
|
345
|
+
"body": body,
|
|
346
|
+
}
|
|
347
|
+
required = (
|
|
348
|
+
"envelope_id",
|
|
349
|
+
"request_id",
|
|
350
|
+
"connection_id",
|
|
351
|
+
"lease_token",
|
|
352
|
+
"team_id",
|
|
353
|
+
"source_server_identity",
|
|
354
|
+
"source_route_id",
|
|
355
|
+
"source_route_revision",
|
|
356
|
+
"target_server_identity",
|
|
357
|
+
"target_route_id",
|
|
358
|
+
"target_route_revision",
|
|
359
|
+
"target_chat_id",
|
|
360
|
+
"exchange_id",
|
|
361
|
+
)
|
|
362
|
+
if any(not exact[key] for key in required):
|
|
363
|
+
raise ValueError("secure peer delivery identity is incomplete")
|
|
364
|
+
if exact["max_legs"] != 6 or not 1 <= exact["used_legs"] <= 6:
|
|
365
|
+
raise ValueError("secure peer delivery leg budget is invalid")
|
|
366
|
+
# The lease token belongs to a delivery *claim*, not the immutable
|
|
367
|
+
# envelope. A crash past the lease deadline legitimately redelivers
|
|
368
|
+
# the same envelope with a new token.
|
|
369
|
+
immutable = {
|
|
370
|
+
key: value for key, value in exact.items() if key != "lease_token"
|
|
371
|
+
}
|
|
372
|
+
digest = hashlib.sha256(canonical_json(immutable)).hexdigest()
|
|
373
|
+
return exact, digest
|
|
374
|
+
|
|
375
|
+
def prepare(
|
|
376
|
+
self,
|
|
377
|
+
envelope: Mapping[str, Any],
|
|
378
|
+
*,
|
|
379
|
+
transport_role: str,
|
|
380
|
+
connection_id: str,
|
|
381
|
+
lease_token: str,
|
|
382
|
+
target_chat_id: str,
|
|
383
|
+
) -> tuple[dict[str, Any], bool]:
|
|
384
|
+
exact, digest = self._claim(
|
|
385
|
+
envelope,
|
|
386
|
+
transport_role=transport_role,
|
|
387
|
+
connection_id=connection_id,
|
|
388
|
+
lease_token=lease_token,
|
|
389
|
+
target_chat_id=target_chat_id,
|
|
390
|
+
)
|
|
391
|
+
timestamp = int(time.time())
|
|
392
|
+
with self._transaction() as connection:
|
|
393
|
+
row = connection.execute(
|
|
394
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
395
|
+
(exact["envelope_id"],),
|
|
396
|
+
).fetchone()
|
|
397
|
+
if row is not None:
|
|
398
|
+
if row["claim_digest"] != digest:
|
|
399
|
+
raise RuntimeError("secure peer envelope identity changed")
|
|
400
|
+
if (
|
|
401
|
+
str(row["state"]) == "prepared"
|
|
402
|
+
and str(row["lease_token"] or "") != exact["lease_token"]
|
|
403
|
+
):
|
|
404
|
+
connection.execute(
|
|
405
|
+
"""UPDATE secure_peer_deliveries
|
|
406
|
+
SET lease_token=?,updated_at=?
|
|
407
|
+
WHERE envelope_id=? AND state='prepared' AND claim_digest=?""",
|
|
408
|
+
(
|
|
409
|
+
exact["lease_token"],
|
|
410
|
+
timestamp,
|
|
411
|
+
exact["envelope_id"],
|
|
412
|
+
digest,
|
|
413
|
+
),
|
|
414
|
+
)
|
|
415
|
+
row = connection.execute(
|
|
416
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
417
|
+
(exact["envelope_id"],),
|
|
418
|
+
).fetchone()
|
|
419
|
+
public = self._public(row)
|
|
420
|
+
assert public is not None
|
|
421
|
+
return public, False
|
|
422
|
+
connection.execute(
|
|
423
|
+
"""UPDATE secure_peer_deliveries SET state='failed',
|
|
424
|
+
error='secure peer exchange expired',updated_at=?
|
|
425
|
+
WHERE state='prepared' AND expires_at<=?""",
|
|
426
|
+
(timestamp, timestamp),
|
|
427
|
+
)
|
|
428
|
+
nonterminal_count = int(
|
|
429
|
+
connection.execute(
|
|
430
|
+
"""SELECT COUNT(*) AS count FROM secure_peer_deliveries
|
|
431
|
+
WHERE state IN ('prepared','authorized','queued','running')"""
|
|
432
|
+
).fetchone()["count"]
|
|
433
|
+
)
|
|
434
|
+
if nonterminal_count >= MAX_NONTERMINAL_DELIVERIES:
|
|
435
|
+
raise RuntimeError("secure peer delivery capacity is full")
|
|
436
|
+
connection.execute(
|
|
437
|
+
"""INSERT INTO secure_peer_deliveries(
|
|
438
|
+
envelope_id,request_id,claim_digest,transport_role,connection_id,lease_token,
|
|
439
|
+
team_id,source_peer_id,
|
|
440
|
+
source_server_identity,source_route_id,source_route_revision,
|
|
441
|
+
target_peer_id,
|
|
442
|
+
target_server_identity,target_route_id,target_route_revision,
|
|
443
|
+
target_chat_id,action,kind,exchange_id,parent_envelope_id,parent_leg,
|
|
444
|
+
used_legs,max_legs,expires_at,body_json,state,created_at,updated_at
|
|
445
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'prepared',?,?)""",
|
|
446
|
+
(
|
|
447
|
+
exact["envelope_id"],
|
|
448
|
+
exact["request_id"],
|
|
449
|
+
digest,
|
|
450
|
+
exact["transport_role"],
|
|
451
|
+
exact["connection_id"],
|
|
452
|
+
exact["lease_token"],
|
|
453
|
+
exact["team_id"],
|
|
454
|
+
exact["source_peer_id"],
|
|
455
|
+
exact["source_server_identity"],
|
|
456
|
+
exact["source_route_id"],
|
|
457
|
+
exact["source_route_revision"],
|
|
458
|
+
exact["target_peer_id"],
|
|
459
|
+
exact["target_server_identity"],
|
|
460
|
+
exact["target_route_id"],
|
|
461
|
+
exact["target_route_revision"],
|
|
462
|
+
exact["target_chat_id"],
|
|
463
|
+
exact["action"],
|
|
464
|
+
exact["kind"],
|
|
465
|
+
exact["exchange_id"],
|
|
466
|
+
exact["parent_envelope_id"],
|
|
467
|
+
exact["parent_leg"],
|
|
468
|
+
exact["used_legs"],
|
|
469
|
+
exact["max_legs"],
|
|
470
|
+
exact["expires_at"],
|
|
471
|
+
canonical_json(exact["body"]).decode("utf-8"),
|
|
472
|
+
timestamp,
|
|
473
|
+
timestamp,
|
|
474
|
+
),
|
|
475
|
+
)
|
|
476
|
+
row = connection.execute(
|
|
477
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
478
|
+
(exact["envelope_id"],),
|
|
479
|
+
).fetchone()
|
|
480
|
+
public = self._public(row)
|
|
481
|
+
assert public is not None
|
|
482
|
+
return public, True
|
|
483
|
+
|
|
484
|
+
def get(self, envelope_id: str) -> dict[str, Any] | None:
|
|
485
|
+
with self._lock:
|
|
486
|
+
connection = self._connect()
|
|
487
|
+
try:
|
|
488
|
+
return self._public(connection.execute(
|
|
489
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
490
|
+
(envelope_id,),
|
|
491
|
+
).fetchone())
|
|
492
|
+
finally:
|
|
493
|
+
connection.close()
|
|
494
|
+
|
|
495
|
+
def _transition(
|
|
496
|
+
self,
|
|
497
|
+
envelope_id: str,
|
|
498
|
+
*,
|
|
499
|
+
expected: set[str],
|
|
500
|
+
state: str,
|
|
501
|
+
**values: Any,
|
|
502
|
+
) -> dict[str, Any] | None:
|
|
503
|
+
if state not in DELIVERY_STATES:
|
|
504
|
+
raise ValueError("secure peer delivery state is invalid")
|
|
505
|
+
allowed = {"queued_id", "run_id", "result_hash", "error"}
|
|
506
|
+
patch = {key: value for key, value in values.items() if key in allowed}
|
|
507
|
+
patch.update({"state": state, "updated_at": int(time.time())})
|
|
508
|
+
assignments = ",".join(f"{key}=?" for key in patch)
|
|
509
|
+
placeholders = ",".join("?" for _ in expected)
|
|
510
|
+
arguments = [*patch.values(), envelope_id, *sorted(expected)]
|
|
511
|
+
with self._transaction() as connection:
|
|
512
|
+
changed = connection.execute(
|
|
513
|
+
f"UPDATE secure_peer_deliveries SET {assignments} "
|
|
514
|
+
f"WHERE envelope_id=? AND state IN ({placeholders})",
|
|
515
|
+
arguments,
|
|
516
|
+
).rowcount
|
|
517
|
+
if changed != 1:
|
|
518
|
+
return None
|
|
519
|
+
return self._public(connection.execute(
|
|
520
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
521
|
+
(envelope_id,),
|
|
522
|
+
).fetchone())
|
|
523
|
+
|
|
524
|
+
def authorize(self, envelope_id: str) -> dict[str, Any] | None:
|
|
525
|
+
return self._transition(
|
|
526
|
+
envelope_id,
|
|
527
|
+
expected={"prepared", "authorized"},
|
|
528
|
+
state="authorized",
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
def fail_ownerless_authorized(
|
|
532
|
+
self,
|
|
533
|
+
envelope_id: str,
|
|
534
|
+
*,
|
|
535
|
+
error: str,
|
|
536
|
+
) -> dict[str, Any] | None:
|
|
537
|
+
"""Fail only a receipt-accepted delivery that has no queued/run owner."""
|
|
538
|
+
|
|
539
|
+
timestamp = int(time.time())
|
|
540
|
+
with self._transaction() as connection:
|
|
541
|
+
connection.execute(
|
|
542
|
+
"""UPDATE secure_peer_deliveries SET
|
|
543
|
+
state='failed',error=?,admission_retry_at=NULL,
|
|
544
|
+
admission_last_error=NULL,updated_at=?
|
|
545
|
+
WHERE envelope_id=? AND state='authorized'
|
|
546
|
+
AND queued_id IS NULL AND run_id IS NULL""",
|
|
547
|
+
(str(error)[:400], timestamp, envelope_id),
|
|
548
|
+
)
|
|
549
|
+
return self._public(connection.execute(
|
|
550
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
551
|
+
(envelope_id,),
|
|
552
|
+
).fetchone())
|
|
553
|
+
|
|
554
|
+
def bind_owner(
|
|
555
|
+
self,
|
|
556
|
+
envelope_id: str,
|
|
557
|
+
*,
|
|
558
|
+
queued_id: str | None,
|
|
559
|
+
run_id: str | None,
|
|
560
|
+
) -> dict[str, Any] | None:
|
|
561
|
+
if queued_id is None and run_id is None:
|
|
562
|
+
raise ValueError("secure peer delivery owner is empty")
|
|
563
|
+
timestamp = int(time.time())
|
|
564
|
+
with self._transaction() as connection:
|
|
565
|
+
row = connection.execute(
|
|
566
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
567
|
+
(envelope_id,),
|
|
568
|
+
).fetchone()
|
|
569
|
+
if row is None:
|
|
570
|
+
return None
|
|
571
|
+
state = str(row["state"])
|
|
572
|
+
current_queued = row["queued_id"]
|
|
573
|
+
current_run = row["run_id"]
|
|
574
|
+
if state == "authorized":
|
|
575
|
+
next_state = "running" if run_id is not None else "queued"
|
|
576
|
+
connection.execute(
|
|
577
|
+
"""UPDATE secure_peer_deliveries
|
|
578
|
+
SET state=?,queued_id=?,run_id=?,admission_retry_at=NULL,
|
|
579
|
+
admission_last_error=NULL,updated_at=?
|
|
580
|
+
WHERE envelope_id=? AND state='authorized'
|
|
581
|
+
AND queued_id IS NULL AND run_id IS NULL""",
|
|
582
|
+
(next_state, queued_id, run_id, timestamp, envelope_id),
|
|
583
|
+
)
|
|
584
|
+
elif state == "queued" and run_id is not None:
|
|
585
|
+
if current_queued != queued_id or current_run is not None:
|
|
586
|
+
return None
|
|
587
|
+
connection.execute(
|
|
588
|
+
"""UPDATE secure_peer_deliveries SET state='running',run_id=?,
|
|
589
|
+
admission_retry_at=NULL,admission_last_error=NULL,updated_at=?
|
|
590
|
+
WHERE envelope_id=? AND state='queued' AND queued_id=? AND run_id IS NULL""",
|
|
591
|
+
(run_id, timestamp, envelope_id, queued_id),
|
|
592
|
+
)
|
|
593
|
+
elif state == "queued":
|
|
594
|
+
if current_queued != queued_id or current_run is not None:
|
|
595
|
+
return None
|
|
596
|
+
elif state == "running":
|
|
597
|
+
if current_queued != queued_id or current_run != run_id:
|
|
598
|
+
return None
|
|
599
|
+
else:
|
|
600
|
+
return None
|
|
601
|
+
return self._public(connection.execute(
|
|
602
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
603
|
+
(envelope_id,),
|
|
604
|
+
).fetchone())
|
|
605
|
+
|
|
606
|
+
def defer_admission(
|
|
607
|
+
self,
|
|
608
|
+
envelope_id: str,
|
|
609
|
+
*,
|
|
610
|
+
error: str,
|
|
611
|
+
) -> dict[str, Any] | None:
|
|
612
|
+
now = int(time.time())
|
|
613
|
+
with self._transaction() as connection:
|
|
614
|
+
row = connection.execute(
|
|
615
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
616
|
+
(envelope_id,),
|
|
617
|
+
).fetchone()
|
|
618
|
+
if (
|
|
619
|
+
row is None
|
|
620
|
+
or row["state"] != "authorized"
|
|
621
|
+
or row["queued_id"] is not None
|
|
622
|
+
or row["run_id"] is not None
|
|
623
|
+
):
|
|
624
|
+
return self._public(row)
|
|
625
|
+
attempts = int(row["admission_attempts"] or 0) + 1
|
|
626
|
+
delay = min(300, 2 ** min(attempts, 8))
|
|
627
|
+
connection.execute(
|
|
628
|
+
"""UPDATE secure_peer_deliveries SET
|
|
629
|
+
admission_attempts=?,admission_retry_at=?,admission_last_error=?,updated_at=?
|
|
630
|
+
WHERE envelope_id=? AND state='authorized'
|
|
631
|
+
AND queued_id IS NULL AND run_id IS NULL""",
|
|
632
|
+
(attempts, now + delay, str(error)[:400], now, envelope_id),
|
|
633
|
+
)
|
|
634
|
+
return self._public(connection.execute(
|
|
635
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
636
|
+
(envelope_id,),
|
|
637
|
+
).fetchone())
|
|
638
|
+
|
|
639
|
+
def pending_admissions(
|
|
640
|
+
self,
|
|
641
|
+
*,
|
|
642
|
+
limit: int = 20,
|
|
643
|
+
now: int | None = None,
|
|
644
|
+
) -> list[dict[str, Any]]:
|
|
645
|
+
if type(limit) is not int or not 1 <= limit <= 50:
|
|
646
|
+
raise ValueError("secure peer admission retry limit is invalid")
|
|
647
|
+
timestamp = int(time.time()) if now is None else int(now)
|
|
648
|
+
with self._lock:
|
|
649
|
+
connection = self._connect()
|
|
650
|
+
try:
|
|
651
|
+
rows = connection.execute(
|
|
652
|
+
"""SELECT * FROM secure_peer_deliveries
|
|
653
|
+
WHERE state='authorized' AND queued_id IS NULL AND run_id IS NULL
|
|
654
|
+
AND COALESCE(admission_retry_at,0)<=?
|
|
655
|
+
ORDER BY COALESCE(admission_retry_at,0),created_at,envelope_id
|
|
656
|
+
LIMIT ?""",
|
|
657
|
+
(timestamp, limit),
|
|
658
|
+
).fetchall()
|
|
659
|
+
return [self._public(row) for row in rows if row is not None]
|
|
660
|
+
finally:
|
|
661
|
+
connection.close()
|
|
662
|
+
|
|
663
|
+
def nonterminal_for_chat(self, chat_id: str) -> list[dict[str, Any]]:
|
|
664
|
+
with self._lock:
|
|
665
|
+
connection = self._connect()
|
|
666
|
+
try:
|
|
667
|
+
rows = connection.execute(
|
|
668
|
+
"""SELECT * FROM secure_peer_deliveries
|
|
669
|
+
WHERE target_chat_id=?
|
|
670
|
+
AND state IN ('prepared','authorized','queued','running')
|
|
671
|
+
ORDER BY created_at,envelope_id""",
|
|
672
|
+
(chat_id,),
|
|
673
|
+
).fetchall()
|
|
674
|
+
return [self._public(row) for row in rows if row is not None]
|
|
675
|
+
finally:
|
|
676
|
+
connection.close()
|
|
677
|
+
|
|
678
|
+
def finish(
|
|
679
|
+
self,
|
|
680
|
+
envelope_id: str,
|
|
681
|
+
*,
|
|
682
|
+
succeeded: bool,
|
|
683
|
+
result_text: str = "",
|
|
684
|
+
error: str | None = None,
|
|
685
|
+
) -> dict[str, Any] | None:
|
|
686
|
+
return self._transition(
|
|
687
|
+
envelope_id,
|
|
688
|
+
expected=(
|
|
689
|
+
{"authorized", "queued", "running"}
|
|
690
|
+
if succeeded
|
|
691
|
+
else {"prepared", "authorized", "queued", "running"}
|
|
692
|
+
),
|
|
693
|
+
state="completed" if succeeded else "failed",
|
|
694
|
+
result_hash=(
|
|
695
|
+
hashlib.sha256(result_text.encode("utf-8")).hexdigest()
|
|
696
|
+
if result_text
|
|
697
|
+
else None
|
|
698
|
+
),
|
|
699
|
+
error=error,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
def prepare_response(
|
|
703
|
+
self,
|
|
704
|
+
envelope_id: str,
|
|
705
|
+
*,
|
|
706
|
+
request_id: str,
|
|
707
|
+
body: str,
|
|
708
|
+
request_response: bool,
|
|
709
|
+
) -> dict[str, Any] | None:
|
|
710
|
+
if (
|
|
711
|
+
not request_id
|
|
712
|
+
or not isinstance(body, str)
|
|
713
|
+
or not body.strip()
|
|
714
|
+
or len(body) > 100_000
|
|
715
|
+
or type(request_response) is not bool
|
|
716
|
+
):
|
|
717
|
+
raise ValueError("secure peer response intent is invalid")
|
|
718
|
+
intent = {
|
|
719
|
+
"request_id": request_id,
|
|
720
|
+
"body": body,
|
|
721
|
+
"request_response": request_response,
|
|
722
|
+
}
|
|
723
|
+
digest = hashlib.sha256(canonical_json(intent)).hexdigest()
|
|
724
|
+
timestamp = int(time.time())
|
|
725
|
+
with self._transaction() as connection:
|
|
726
|
+
row = connection.execute(
|
|
727
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
728
|
+
(envelope_id,),
|
|
729
|
+
).fetchone()
|
|
730
|
+
if (
|
|
731
|
+
row is None
|
|
732
|
+
or row["state"] not in {"authorized", "queued", "running"}
|
|
733
|
+
or row["kind"] == "instruction"
|
|
734
|
+
):
|
|
735
|
+
return None
|
|
736
|
+
existing = row["response_request_digest"]
|
|
737
|
+
if existing is not None:
|
|
738
|
+
if existing != digest:
|
|
739
|
+
raise RuntimeError("secure peer response intent changed")
|
|
740
|
+
return self._public(row)
|
|
741
|
+
connection.execute(
|
|
742
|
+
"""UPDATE secure_peer_deliveries SET
|
|
743
|
+
response_request_id=?,response_request_digest=?,response_body=?,
|
|
744
|
+
response_request_response=?,response_retry_at=?,
|
|
745
|
+
response_attempts=0,response_last_error=NULL,updated_at=?
|
|
746
|
+
WHERE envelope_id=? AND response_request_digest IS NULL
|
|
747
|
+
AND response_committed=0
|
|
748
|
+
AND state IN ('authorized','queued','running')""",
|
|
749
|
+
(
|
|
750
|
+
request_id,
|
|
751
|
+
digest,
|
|
752
|
+
body,
|
|
753
|
+
int(request_response),
|
|
754
|
+
timestamp,
|
|
755
|
+
timestamp,
|
|
756
|
+
envelope_id,
|
|
757
|
+
),
|
|
758
|
+
)
|
|
759
|
+
return self._public(connection.execute(
|
|
760
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
761
|
+
(envelope_id,),
|
|
762
|
+
).fetchone())
|
|
763
|
+
|
|
764
|
+
def mark_response_committed(
|
|
765
|
+
self,
|
|
766
|
+
envelope_id: str,
|
|
767
|
+
*,
|
|
768
|
+
request_id: str | None = None,
|
|
769
|
+
) -> dict[str, Any] | None:
|
|
770
|
+
with self._transaction() as connection:
|
|
771
|
+
changed = connection.execute(
|
|
772
|
+
"""UPDATE secure_peer_deliveries
|
|
773
|
+
SET response_committed=1,response_retry_at=NULL,
|
|
774
|
+
response_last_error=NULL,updated_at=?
|
|
775
|
+
WHERE envelope_id=? AND state IN ('authorized','queued','running')
|
|
776
|
+
AND response_committed=0
|
|
777
|
+
AND (? IS NULL OR response_request_id=?)""",
|
|
778
|
+
(int(time.time()), envelope_id, request_id, request_id),
|
|
779
|
+
).rowcount
|
|
780
|
+
if changed not in {0, 1}:
|
|
781
|
+
raise RuntimeError("secure peer response state is ambiguous")
|
|
782
|
+
return self._public(connection.execute(
|
|
783
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
784
|
+
(envelope_id,),
|
|
785
|
+
).fetchone())
|
|
786
|
+
|
|
787
|
+
def clear_response_intent(
|
|
788
|
+
self,
|
|
789
|
+
envelope_id: str,
|
|
790
|
+
*,
|
|
791
|
+
request_id: str,
|
|
792
|
+
) -> dict[str, Any] | None:
|
|
793
|
+
with self._transaction() as connection:
|
|
794
|
+
connection.execute(
|
|
795
|
+
"""UPDATE secure_peer_deliveries SET
|
|
796
|
+
response_request_id=NULL,response_request_digest=NULL,
|
|
797
|
+
response_body=NULL,response_request_response=NULL,
|
|
798
|
+
response_retry_at=NULL,response_attempts=0,
|
|
799
|
+
response_last_error=NULL,updated_at=?
|
|
800
|
+
WHERE envelope_id=? AND response_request_id=?
|
|
801
|
+
AND response_committed=0
|
|
802
|
+
AND state IN ('authorized','queued','running')""",
|
|
803
|
+
(int(time.time()), envelope_id, request_id),
|
|
804
|
+
)
|
|
805
|
+
return self._public(connection.execute(
|
|
806
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
807
|
+
(envelope_id,),
|
|
808
|
+
).fetchone())
|
|
809
|
+
|
|
810
|
+
def defer_response(
|
|
811
|
+
self,
|
|
812
|
+
envelope_id: str,
|
|
813
|
+
*,
|
|
814
|
+
request_id: str,
|
|
815
|
+
error: str,
|
|
816
|
+
) -> dict[str, Any] | None:
|
|
817
|
+
"""Persist bounded exponential retry state for one response intent."""
|
|
818
|
+
|
|
819
|
+
now = int(time.time())
|
|
820
|
+
with self._transaction() as connection:
|
|
821
|
+
row = connection.execute(
|
|
822
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
823
|
+
(envelope_id,),
|
|
824
|
+
).fetchone()
|
|
825
|
+
if (
|
|
826
|
+
row is None
|
|
827
|
+
or row["state"] not in {"authorized", "queued", "running"}
|
|
828
|
+
or row["response_request_id"] != request_id
|
|
829
|
+
or int(row["response_committed"] or 0) != 0
|
|
830
|
+
):
|
|
831
|
+
return self._public(row)
|
|
832
|
+
attempts = int(row["response_attempts"] or 0) + 1
|
|
833
|
+
delay = min(300, 2 ** min(attempts, 8))
|
|
834
|
+
connection.execute(
|
|
835
|
+
"""UPDATE secure_peer_deliveries SET
|
|
836
|
+
response_attempts=?,response_retry_at=?,response_last_error=?,updated_at=?
|
|
837
|
+
WHERE envelope_id=? AND response_request_id=?
|
|
838
|
+
AND response_committed=0
|
|
839
|
+
AND state IN ('authorized','queued','running')""",
|
|
840
|
+
(
|
|
841
|
+
attempts,
|
|
842
|
+
now + delay,
|
|
843
|
+
str(error)[:400],
|
|
844
|
+
now,
|
|
845
|
+
envelope_id,
|
|
846
|
+
request_id,
|
|
847
|
+
),
|
|
848
|
+
)
|
|
849
|
+
return self._public(connection.execute(
|
|
850
|
+
"SELECT * FROM secure_peer_deliveries WHERE envelope_id=?",
|
|
851
|
+
(envelope_id,),
|
|
852
|
+
).fetchone())
|
|
853
|
+
|
|
854
|
+
def pending_responses(
|
|
855
|
+
self,
|
|
856
|
+
*,
|
|
857
|
+
limit: int = 8,
|
|
858
|
+
now: int | None = None,
|
|
859
|
+
) -> list[dict[str, Any]]:
|
|
860
|
+
if (
|
|
861
|
+
type(limit) is not int
|
|
862
|
+
or not 1 <= limit <= MAX_NONTERMINAL_DELIVERIES
|
|
863
|
+
):
|
|
864
|
+
raise ValueError("secure peer response retry limit is invalid")
|
|
865
|
+
timestamp = int(time.time()) if now is None else int(now)
|
|
866
|
+
with self._lock:
|
|
867
|
+
connection = self._connect()
|
|
868
|
+
try:
|
|
869
|
+
rows = connection.execute(
|
|
870
|
+
"""SELECT * FROM secure_peer_deliveries
|
|
871
|
+
WHERE state IN ('authorized','queued','running')
|
|
872
|
+
AND response_request_id IS NOT NULL
|
|
873
|
+
AND response_committed=0
|
|
874
|
+
AND COALESCE(response_retry_at,0)<=?
|
|
875
|
+
ORDER BY COALESCE(response_retry_at,0),created_at,envelope_id
|
|
876
|
+
LIMIT ?""",
|
|
877
|
+
(timestamp, limit),
|
|
878
|
+
).fetchall()
|
|
879
|
+
return [self._public(row) for row in rows if row is not None]
|
|
880
|
+
finally:
|
|
881
|
+
connection.close()
|
|
882
|
+
|
|
883
|
+
def recoverable(self) -> list[dict[str, Any]]:
|
|
884
|
+
with self._lock:
|
|
885
|
+
connection = self._connect()
|
|
886
|
+
try:
|
|
887
|
+
rows = connection.execute(
|
|
888
|
+
"""SELECT * FROM secure_peer_deliveries
|
|
889
|
+
WHERE state IN ('prepared','authorized','queued','running')
|
|
890
|
+
ORDER BY created_at,envelope_id"""
|
|
891
|
+
).fetchall()
|
|
892
|
+
return [self._public(row) for row in rows if row is not None]
|
|
893
|
+
finally:
|
|
894
|
+
connection.close()
|
|
895
|
+
|
|
896
|
+
def for_run(self, run_id: str) -> dict[str, Any] | None:
|
|
897
|
+
with self._lock:
|
|
898
|
+
connection = self._connect()
|
|
899
|
+
try:
|
|
900
|
+
return self._public(connection.execute(
|
|
901
|
+
"SELECT * FROM secure_peer_deliveries WHERE run_id=?",
|
|
902
|
+
(run_id,),
|
|
903
|
+
).fetchone())
|
|
904
|
+
finally:
|
|
905
|
+
connection.close()
|
|
906
|
+
|
|
907
|
+
@staticmethod
|
|
908
|
+
def _public_outbound(row: sqlite3.Row | None) -> dict[str, Any] | None:
|
|
909
|
+
if row is None:
|
|
910
|
+
return None
|
|
911
|
+
item = dict(row)
|
|
912
|
+
item["snapshot"] = json.loads(item.pop("snapshot_json"))
|
|
913
|
+
response = item.pop("response_json")
|
|
914
|
+
item["response"] = json.loads(response) if response else None
|
|
915
|
+
return item
|
|
916
|
+
|
|
917
|
+
def prepare_outbound(
|
|
918
|
+
self,
|
|
919
|
+
*,
|
|
920
|
+
request_id: str,
|
|
921
|
+
source_session_id: str,
|
|
922
|
+
source_run_id: str,
|
|
923
|
+
snapshot: Mapping[str, Any],
|
|
924
|
+
body: str,
|
|
925
|
+
action: str,
|
|
926
|
+
expires_at: int,
|
|
927
|
+
) -> tuple[dict[str, Any], bool]:
|
|
928
|
+
connection_id = str(snapshot.get("connection_id") or "")
|
|
929
|
+
source_route_id = str(snapshot.get("source_route_id") or "")
|
|
930
|
+
source_route_revision = str(snapshot.get("source_route_revision") or "")
|
|
931
|
+
if (
|
|
932
|
+
not request_id
|
|
933
|
+
or not source_session_id
|
|
934
|
+
or not source_run_id
|
|
935
|
+
or not connection_id
|
|
936
|
+
or not source_route_id
|
|
937
|
+
or not source_route_revision
|
|
938
|
+
or str(snapshot.get("source_chat_id") or "") != source_session_id
|
|
939
|
+
or str(snapshot.get("action") or "") != action
|
|
940
|
+
or action not in {"instruction", "request_reply"}
|
|
941
|
+
or not isinstance(body, str)
|
|
942
|
+
or not body.strip()
|
|
943
|
+
or len(body) > 100_000
|
|
944
|
+
or not isinstance(snapshot, Mapping)
|
|
945
|
+
):
|
|
946
|
+
raise ValueError("secure peer outbound intent is invalid")
|
|
947
|
+
exact = {
|
|
948
|
+
"request_id": request_id,
|
|
949
|
+
"source_session_id": source_session_id,
|
|
950
|
+
"source_run_id": source_run_id,
|
|
951
|
+
"snapshot": dict(snapshot),
|
|
952
|
+
"body": body,
|
|
953
|
+
"action": action,
|
|
954
|
+
"expires_at": int(expires_at),
|
|
955
|
+
}
|
|
956
|
+
digest = hashlib.sha256(canonical_json(exact)).hexdigest()
|
|
957
|
+
now = int(time.time())
|
|
958
|
+
with self._transaction() as connection:
|
|
959
|
+
row = connection.execute(
|
|
960
|
+
"SELECT * FROM secure_peer_outbound_intents WHERE request_id=?",
|
|
961
|
+
(request_id,),
|
|
962
|
+
).fetchone()
|
|
963
|
+
if row is not None:
|
|
964
|
+
if row["intent_digest"] != digest:
|
|
965
|
+
raise RuntimeError("secure peer outbound intent changed")
|
|
966
|
+
public = self._public_outbound(row)
|
|
967
|
+
assert public is not None
|
|
968
|
+
return public, False
|
|
969
|
+
connection.execute(
|
|
970
|
+
"""INSERT INTO secure_peer_outbound_intents(
|
|
971
|
+
request_id,intent_digest,source_session_id,source_run_id,
|
|
972
|
+
connection_id,source_route_id,source_route_revision,
|
|
973
|
+
snapshot_json,body,action,expires_at,state,retry_at,
|
|
974
|
+
created_at,updated_at
|
|
975
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,'pending',?,?,?)""",
|
|
976
|
+
(
|
|
977
|
+
request_id,
|
|
978
|
+
digest,
|
|
979
|
+
source_session_id,
|
|
980
|
+
source_run_id,
|
|
981
|
+
connection_id,
|
|
982
|
+
source_route_id,
|
|
983
|
+
source_route_revision,
|
|
984
|
+
canonical_json(dict(snapshot)).decode("utf-8"),
|
|
985
|
+
body,
|
|
986
|
+
action,
|
|
987
|
+
int(expires_at),
|
|
988
|
+
now,
|
|
989
|
+
now,
|
|
990
|
+
now,
|
|
991
|
+
),
|
|
992
|
+
)
|
|
993
|
+
public = self._public_outbound(connection.execute(
|
|
994
|
+
"SELECT * FROM secure_peer_outbound_intents WHERE request_id=?",
|
|
995
|
+
(request_id,),
|
|
996
|
+
).fetchone())
|
|
997
|
+
assert public is not None
|
|
998
|
+
return public, True
|
|
999
|
+
|
|
1000
|
+
@staticmethod
|
|
1001
|
+
def _expire_pending_outbound(
|
|
1002
|
+
connection: sqlite3.Connection,
|
|
1003
|
+
timestamp: int,
|
|
1004
|
+
) -> None:
|
|
1005
|
+
connection.execute(
|
|
1006
|
+
"""UPDATE secure_peer_outbound_intents SET
|
|
1007
|
+
state='failed',retry_at=NULL,last_error='secure peer exchange expired',
|
|
1008
|
+
updated_at=? WHERE state='pending' AND expires_at<=?""",
|
|
1009
|
+
(timestamp, timestamp),
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
def pending_outbound_for_chat(
|
|
1013
|
+
self,
|
|
1014
|
+
source_session_id: str,
|
|
1015
|
+
*,
|
|
1016
|
+
now: int | None = None,
|
|
1017
|
+
) -> list[dict[str, Any]]:
|
|
1018
|
+
timestamp = int(time.time()) if now is None else int(now)
|
|
1019
|
+
with self._transaction() as connection:
|
|
1020
|
+
self._expire_pending_outbound(connection, timestamp)
|
|
1021
|
+
rows = connection.execute(
|
|
1022
|
+
"""SELECT * FROM secure_peer_outbound_intents
|
|
1023
|
+
WHERE state='pending' AND source_session_id=?
|
|
1024
|
+
ORDER BY created_at,request_id""",
|
|
1025
|
+
(str(source_session_id),),
|
|
1026
|
+
).fetchall()
|
|
1027
|
+
return [
|
|
1028
|
+
item
|
|
1029
|
+
for row in rows
|
|
1030
|
+
if (item := self._public_outbound(row)) is not None
|
|
1031
|
+
]
|
|
1032
|
+
|
|
1033
|
+
def pending_outbound_for_route(
|
|
1034
|
+
self,
|
|
1035
|
+
connection_id: str,
|
|
1036
|
+
source_route_id: str,
|
|
1037
|
+
source_route_revision: str,
|
|
1038
|
+
*,
|
|
1039
|
+
now: int | None = None,
|
|
1040
|
+
) -> list[dict[str, Any]]:
|
|
1041
|
+
timestamp = int(time.time()) if now is None else int(now)
|
|
1042
|
+
with self._transaction() as connection:
|
|
1043
|
+
self._expire_pending_outbound(connection, timestamp)
|
|
1044
|
+
rows = connection.execute(
|
|
1045
|
+
"""SELECT * FROM secure_peer_outbound_intents
|
|
1046
|
+
WHERE state='pending' AND connection_id=? AND source_route_id=?
|
|
1047
|
+
AND source_route_revision=? ORDER BY created_at,request_id""",
|
|
1048
|
+
(connection_id, source_route_id, source_route_revision),
|
|
1049
|
+
).fetchall()
|
|
1050
|
+
return [
|
|
1051
|
+
item
|
|
1052
|
+
for row in rows
|
|
1053
|
+
if (item := self._public_outbound(row)) is not None
|
|
1054
|
+
]
|
|
1055
|
+
|
|
1056
|
+
def pending_outbound_for_connection(
|
|
1057
|
+
self,
|
|
1058
|
+
connection_id: str,
|
|
1059
|
+
*,
|
|
1060
|
+
now: int | None = None,
|
|
1061
|
+
) -> list[dict[str, Any]]:
|
|
1062
|
+
timestamp = int(time.time()) if now is None else int(now)
|
|
1063
|
+
with self._transaction() as connection:
|
|
1064
|
+
self._expire_pending_outbound(connection, timestamp)
|
|
1065
|
+
rows = connection.execute(
|
|
1066
|
+
"""SELECT * FROM secure_peer_outbound_intents
|
|
1067
|
+
WHERE state='pending' AND connection_id=?
|
|
1068
|
+
ORDER BY created_at,request_id""",
|
|
1069
|
+
(connection_id,),
|
|
1070
|
+
).fetchall()
|
|
1071
|
+
return [
|
|
1072
|
+
item
|
|
1073
|
+
for row in rows
|
|
1074
|
+
if (item := self._public_outbound(row)) is not None
|
|
1075
|
+
]
|
|
1076
|
+
|
|
1077
|
+
def nonterminal_for_connection(
|
|
1078
|
+
self,
|
|
1079
|
+
connection_id: str,
|
|
1080
|
+
) -> list[dict[str, Any]]:
|
|
1081
|
+
with self._lock:
|
|
1082
|
+
connection = self._connect()
|
|
1083
|
+
try:
|
|
1084
|
+
rows = connection.execute(
|
|
1085
|
+
"""SELECT * FROM secure_peer_deliveries
|
|
1086
|
+
WHERE connection_id=?
|
|
1087
|
+
AND state IN ('prepared','authorized','queued','running')
|
|
1088
|
+
ORDER BY created_at,envelope_id""",
|
|
1089
|
+
(connection_id,),
|
|
1090
|
+
).fetchall()
|
|
1091
|
+
return [
|
|
1092
|
+
item
|
|
1093
|
+
for row in rows
|
|
1094
|
+
if (item := self._public(row)) is not None
|
|
1095
|
+
]
|
|
1096
|
+
finally:
|
|
1097
|
+
connection.close()
|
|
1098
|
+
|
|
1099
|
+
def outbound(self, request_id: str) -> dict[str, Any] | None:
|
|
1100
|
+
with self._lock:
|
|
1101
|
+
connection = self._connect()
|
|
1102
|
+
try:
|
|
1103
|
+
return self._public_outbound(connection.execute(
|
|
1104
|
+
"SELECT * FROM secure_peer_outbound_intents WHERE request_id=?",
|
|
1105
|
+
(request_id,),
|
|
1106
|
+
).fetchone())
|
|
1107
|
+
finally:
|
|
1108
|
+
connection.close()
|
|
1109
|
+
|
|
1110
|
+
def commit_outbound(
|
|
1111
|
+
self,
|
|
1112
|
+
request_id: str,
|
|
1113
|
+
response: Mapping[str, Any],
|
|
1114
|
+
) -> dict[str, Any] | None:
|
|
1115
|
+
response_text = canonical_json(dict(response)).decode("utf-8")
|
|
1116
|
+
now = int(time.time())
|
|
1117
|
+
with self._transaction() as connection:
|
|
1118
|
+
row = connection.execute(
|
|
1119
|
+
"SELECT * FROM secure_peer_outbound_intents WHERE request_id=?",
|
|
1120
|
+
(request_id,),
|
|
1121
|
+
).fetchone()
|
|
1122
|
+
if row is None:
|
|
1123
|
+
return None
|
|
1124
|
+
if row["state"] == "committed":
|
|
1125
|
+
if str(row["response_json"] or "") != response_text:
|
|
1126
|
+
raise RuntimeError("secure peer outbound result changed")
|
|
1127
|
+
return self._public_outbound(row)
|
|
1128
|
+
if row["state"] != "pending":
|
|
1129
|
+
return self._public_outbound(row)
|
|
1130
|
+
connection.execute(
|
|
1131
|
+
"""UPDATE secure_peer_outbound_intents SET
|
|
1132
|
+
state='committed',response_json=?,retry_at=NULL,last_error=NULL,updated_at=?
|
|
1133
|
+
WHERE request_id=? AND state='pending'""",
|
|
1134
|
+
(response_text, now, request_id),
|
|
1135
|
+
)
|
|
1136
|
+
return self._public_outbound(connection.execute(
|
|
1137
|
+
"SELECT * FROM secure_peer_outbound_intents WHERE request_id=?",
|
|
1138
|
+
(request_id,),
|
|
1139
|
+
).fetchone())
|
|
1140
|
+
|
|
1141
|
+
def defer_outbound(self, request_id: str, error: str) -> dict[str, Any] | None:
|
|
1142
|
+
now = int(time.time())
|
|
1143
|
+
with self._transaction() as connection:
|
|
1144
|
+
row = connection.execute(
|
|
1145
|
+
"SELECT * FROM secure_peer_outbound_intents WHERE request_id=?",
|
|
1146
|
+
(request_id,),
|
|
1147
|
+
).fetchone()
|
|
1148
|
+
if row is None or row["state"] != "pending":
|
|
1149
|
+
return self._public_outbound(row)
|
|
1150
|
+
attempts = int(row["attempts"] or 0) + 1
|
|
1151
|
+
delay = min(300, 2 ** min(attempts, 8))
|
|
1152
|
+
connection.execute(
|
|
1153
|
+
"""UPDATE secure_peer_outbound_intents SET
|
|
1154
|
+
attempts=?,retry_at=?,last_error=?,updated_at=?
|
|
1155
|
+
WHERE request_id=? AND state='pending'""",
|
|
1156
|
+
(attempts, now + delay, str(error)[:400], now, request_id),
|
|
1157
|
+
)
|
|
1158
|
+
return self._public_outbound(connection.execute(
|
|
1159
|
+
"SELECT * FROM secure_peer_outbound_intents WHERE request_id=?",
|
|
1160
|
+
(request_id,),
|
|
1161
|
+
).fetchone())
|
|
1162
|
+
|
|
1163
|
+
def fail_outbound(self, request_id: str, error: str) -> dict[str, Any] | None:
|
|
1164
|
+
now = int(time.time())
|
|
1165
|
+
with self._transaction() as connection:
|
|
1166
|
+
connection.execute(
|
|
1167
|
+
"""UPDATE secure_peer_outbound_intents SET
|
|
1168
|
+
state='failed',retry_at=NULL,last_error=?,updated_at=?
|
|
1169
|
+
WHERE request_id=? AND state='pending'""",
|
|
1170
|
+
(str(error)[:400], now, request_id),
|
|
1171
|
+
)
|
|
1172
|
+
return self._public_outbound(connection.execute(
|
|
1173
|
+
"SELECT * FROM secure_peer_outbound_intents WHERE request_id=?",
|
|
1174
|
+
(request_id,),
|
|
1175
|
+
).fetchone())
|
|
1176
|
+
|
|
1177
|
+
def pending_outbound(
|
|
1178
|
+
self,
|
|
1179
|
+
*,
|
|
1180
|
+
limit: int = 8,
|
|
1181
|
+
now: int | None = None,
|
|
1182
|
+
) -> list[dict[str, Any]]:
|
|
1183
|
+
if type(limit) is not int or not 1 <= limit <= 50:
|
|
1184
|
+
raise ValueError("secure peer outbound retry limit is invalid")
|
|
1185
|
+
timestamp = int(time.time()) if now is None else int(now)
|
|
1186
|
+
with self._lock:
|
|
1187
|
+
connection = self._connect()
|
|
1188
|
+
try:
|
|
1189
|
+
rows = connection.execute(
|
|
1190
|
+
"""SELECT * FROM secure_peer_outbound_intents
|
|
1191
|
+
WHERE state='pending' AND COALESCE(retry_at,0)<=?
|
|
1192
|
+
ORDER BY COALESCE(retry_at,0),created_at,request_id LIMIT ?""",
|
|
1193
|
+
(timestamp, limit),
|
|
1194
|
+
).fetchall()
|
|
1195
|
+
return [
|
|
1196
|
+
self._public_outbound(row)
|
|
1197
|
+
for row in rows
|
|
1198
|
+
if row is not None
|
|
1199
|
+
]
|
|
1200
|
+
finally:
|
|
1201
|
+
connection.close()
|
|
1202
|
+
|
|
1203
|
+
def recoverable_outbound(self, *, limit: int = 50) -> list[dict[str, Any]]:
|
|
1204
|
+
"""Return every nonterminal outbound intent, including future retries."""
|
|
1205
|
+
|
|
1206
|
+
if type(limit) is not int or not 1 <= limit <= 50:
|
|
1207
|
+
raise ValueError("secure peer outbound recovery limit is invalid")
|
|
1208
|
+
with self._lock:
|
|
1209
|
+
connection = self._connect()
|
|
1210
|
+
try:
|
|
1211
|
+
rows = connection.execute(
|
|
1212
|
+
"""SELECT * FROM secure_peer_outbound_intents
|
|
1213
|
+
WHERE state='pending'
|
|
1214
|
+
ORDER BY created_at,request_id LIMIT ?""",
|
|
1215
|
+
(limit,),
|
|
1216
|
+
).fetchall()
|
|
1217
|
+
return [
|
|
1218
|
+
self._public_outbound(row)
|
|
1219
|
+
for row in rows
|
|
1220
|
+
if row is not None
|
|
1221
|
+
]
|
|
1222
|
+
finally:
|
|
1223
|
+
connection.close()
|
|
1224
|
+
|
|
1225
|
+
def prune(self, *, retain_seconds: int = 30 * 24 * 60 * 60) -> int:
|
|
1226
|
+
cutoff = int(time.time()) - retain_seconds
|
|
1227
|
+
with self._transaction() as connection:
|
|
1228
|
+
deleted = int(connection.execute(
|
|
1229
|
+
"DELETE FROM secure_peer_deliveries WHERE state IN ('completed','failed') AND updated_at<?",
|
|
1230
|
+
(cutoff,),
|
|
1231
|
+
).rowcount or 0)
|
|
1232
|
+
deleted += int(connection.execute(
|
|
1233
|
+
"DELETE FROM secure_peer_outbound_intents WHERE state IN ('committed','failed') AND updated_at<?",
|
|
1234
|
+
(cutoff,),
|
|
1235
|
+
).rowcount or 0)
|
|
1236
|
+
return deleted
|