-
Notifications
You must be signed in to change notification settings - Fork 103
Expand file tree
/
Copy pathdatabase.py
More file actions
2367 lines (1762 loc) · 109 KB
/
Copy pathdatabase.py
File metadata and controls
2367 lines (1762 loc) · 109 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
SQLite persistence layer for Trinity platform.
This module provides the DatabaseManager class - a facade for all database operations.
The actual implementations are organized in submodules under db/:
- db/migrations.py: Schema migrations
- db/schema.py: Table and index definitions
- db/users.py: User management
- db/agents.py: Agent ownership and sharing
- db/mcp_keys.py: MCP API key management
- db/schedules.py: Schedule and execution management
- db/chat.py: Chat session and message persistence
- db/activities.py: Activity stream logging
For backward compatibility, all models and the global `db` instance are
re-exported from this module.
Redis is still used for:
- Credential secrets (fast access)
- OAuth state (ephemeral, TTL-based)
- Sessions/cache
"""
import os
from datetime import datetime
from pathlib import Path
# Re-export models for backward compatibility
from db_models import (
UserCreate,
User,
SessionMessageInsert,
AgentShare,
AgentOperatorAccess,
AgentShareRequest,
McpApiKeyCreate,
McpApiKey,
McpApiKeyWithSecret,
ScheduleCreate,
Schedule,
ScheduleExecution,
AgentGitConfig,
GitSyncResult,
ChatSession,
ChatMessage,
AgentPermission,
SharedFolderConfig,
SharedFolderMount,
SystemSetting,
SystemSettingUpdate,
# Public Agent Links (Phase 12.2)
PublicLinkCreate,
PublicLink,
PublicLinkUpdate,
PublicLinkWithUrl,
PublicLinkInfo,
VerificationRequest,
VerificationConfirm,
VerificationResponse,
PublicChatRequest,
PublicChatResponse,
# Public Chat Persistence (Phase 12.2.5)
PublicChatSession,
PublicChatMessage,
# Email Authentication (Phase 12.4)
EmailWhitelistAdd,
EmailLoginRequest,
EmailLoginVerify,
EmailLoginResponse,
# Agent Notifications (NOTIF-001)
NotificationCreate,
Notification,
NotificationList,
NotificationAcknowledge,
# Subscription Credential Models (SUB-001)
SubscriptionCredentialCreate,
SubscriptionCredential,
SubscriptionWithAgents,
AgentAuthStatus,
# Agent Event Subscriptions (EVT-001)
EventSubscriptionCreate,
EventSubscriptionUpdate,
EventSubscription,
EventSubscriptionList,
AgentEvent,
AgentEventList,
# Monitoring Models (MON-001)
AgentHealthStatus,
DockerHealthCheck,
NetworkHealthCheck,
BusinessHealthCheck,
AgentHealthDetail,
AgentHealthSummary,
FleetHealthSummary,
FleetHealthStatus,
MonitoringConfig,
)
# Re-export connection utilities
from db.connection import get_db_connection, DB_PATH
from utils.helpers import utc_now_iso
# Import schema and migration utilities
from db.migrations import run_all_migrations
from db.schema import init_schema
# Import operation classes
from db.users import UserOperations
from db.agents import AgentOperations
from db.mcp_keys import McpKeyOperations
from db.schedules import ScheduleOperations
from db.chat import ChatOperations
from db.sessions import SessionOperations
from db.activities import ActivityOperations
from db.reports import ReportOperations
from db.permissions import PermissionOperations
from db.shared_folders import SharedFolderOperations
from db.agent_shared_files import AgentSharedFilesOperations
from db.settings import SettingsOperations
from db.public_links import PublicLinkOperations
from db.email_auth import EmailAuthOperations
from db.skills import SkillsOperations
from db.public_chat import PublicChatOperations
from db.tags import TagOperations
from db.system_views import SystemViewOperations
from db.notifications import NotificationOperations
from db.subscriptions import SubscriptionOperations
from db.monitoring import MonitoringOperations
from db.dashboard_history import DashboardHistoryOperations
from db.slack import SlackOperations
from db.slack_channels import SlackChannelOperations
from db.nevermined import NeverminedOperations
from db.operator_queue import OperatorQueueOperations
from db.event_subscriptions import EventSubscriptionOperations
from db.telegram_channels import TelegramChannelOperations
from db.whatsapp_channels import WhatsAppChannelOperations
from db.voip import VoipOperations
from db.access_requests import AccessRequestOperations
from db.audit import PlatformAuditOperations
from db.canary import CanaryOperations
from db.compatibility import CompatibilityOperations
from db.sync_state import SyncStateOperations
from db.idempotency import IdempotencyOperations
from db.loops import LoopOperations
def init_database():
"""Initialize the SQLite database with all required tables.
1. Creates database directory if needed
2. Runs all migrations (idempotent)
3. Creates schema (tables and indexes)
4. Ensures admin user exists
"""
# PostgreSQL path (#300/#1183): schema is owned by Alembic — a fresh DB is
# built by `alembic upgrade head` (the baseline revision reuses the same
# head DDL that init_schema_postgres emitted), and an existing DB is
# migrated in place. The sqlite-only PRAGMA migrations below are skipped;
# SQLite keeps the legacy bespoke path (the two coexist during the Postgres
# transition).
from db.engine import is_sqlite
if not is_sqlite():
from db.alembic_runner import upgrade_to_head
upgrade_to_head()
_ensure_admin_user_engine()
return
db_path = Path(DB_PATH)
db_path.parent.mkdir(parents=True, exist_ok=True)
# Hold the cross-process lock across BOTH migration passes AND init_schema
# (#1160): init_schema's CREATE TABLE IF NOT EXISTS could otherwise race a
# concurrent worker mid-rebuild and recreate an empty table over its data.
from db.migration_lock import migration_lock
with migration_lock(DB_PATH):
with get_db_connection() as conn:
cursor = conn.cursor()
# Run migrations first (upgrade existing DB; skips if tables don't exist yet)
run_all_migrations(cursor, conn)
# Create schema (tables and indexes)
init_schema(cursor, conn)
# Second pass: record any migrations skipped on fresh install. On first
# startup the target tables don't exist yet so those migrations are
# skipped-but-not-recorded. init_schema has now created them with the
# correct current schema, so re-running is a no-op — but it records
# them, keeping the health check accurate.
run_all_migrations(cursor, conn)
# Create default admin user if not exists
_ensure_admin_user(cursor, conn)
def _ensure_admin_user_engine():
"""Ensure the admin user exists — engine-based path for PostgreSQL (#300).
Reuses the dialect-agnostic ``UserOperations`` (already on SQLAlchemy Core)
instead of the raw-cursor sqlite path. Creates the admin on a fresh DB;
updates the password when the env password no longer verifies.
"""
admin_password = os.getenv("ADMIN_PASSWORD", "")
admin_username = os.getenv("ADMIN_USERNAME", "admin")
if not admin_password:
print("WARNING: ADMIN_PASSWORD not set - skipping admin user creation")
return
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
user_ops = UserOperations()
existing = user_ops.get_user_by_username(admin_username)
if existing is None:
user_ops.update_user_password(admin_username, pwd_context.hash(admin_password))
print(f"Created admin user '{admin_username}' with hashed password")
return
existing_hash = existing.get("password")
needs_update = False
if existing_hash and not existing_hash.startswith("$2"):
needs_update = existing_hash == admin_password # plaintext → bcrypt
elif existing_hash:
try:
needs_update = not pwd_context.verify(admin_password, existing_hash)
except Exception:
needs_update = True
else:
needs_update = True
if needs_update:
user_ops.update_user_password(admin_username, pwd_context.hash(admin_password))
print(f"Updated admin user '{admin_username}' password")
def _ensure_admin_user(cursor, conn):
"""Ensure the admin user exists with properly hashed password."""
admin_password = os.getenv("ADMIN_PASSWORD", "")
admin_username = os.getenv("ADMIN_USERNAME", "admin")
# Warn operators if ADMIN_PASSWORD doesn't meet complexity requirements
if admin_password:
try:
from utils.password_validation import validate_password_strength
pw_errors = validate_password_strength(admin_password)
if pw_errors:
print(f"WARNING: ADMIN_PASSWORD does not meet complexity requirements:")
for err in pw_errors:
print(f" - {err}")
print(" Recommended: 12+ chars with uppercase, lowercase, digits, and special characters")
except ImportError:
pass # password_validation module not available during early init
cursor.execute("SELECT id, password_hash FROM users WHERE username = ?", (admin_username,))
existing = cursor.fetchone()
if existing is None:
# Create admin user
if not admin_password:
print("WARNING: ADMIN_PASSWORD not set - skipping admin user creation")
print(" Set ADMIN_PASSWORD environment variable to create admin user")
return
now = utc_now_iso()
# Hash password using bcrypt
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
hashed = pwd_context.hash(admin_password)
cursor.execute("""
INSERT INTO users (username, password_hash, role, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
""", (admin_username, hashed, "admin", now, now))
conn.commit()
print(f"Created admin user '{admin_username}' with hashed password")
else:
# Check if existing password needs update (migration or change)
existing_hash = existing[1]
should_update = False
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
if existing_hash and not existing_hash.startswith("$2"):
# Password is likely plaintext (bcrypt hashes start with $2)
if admin_password and existing_hash == admin_password:
should_update = True
print(f"Migrating admin user '{admin_username}' password from plaintext to bcrypt")
elif admin_password:
# Check if environment password matches DB hash
try:
if not pwd_context.verify(admin_password, existing_hash):
should_update = True
print(f"Environment password changed - updating admin user '{admin_username}'")
except Exception as e:
print(f"Error verifying password hash: {e}")
should_update = True
if should_update and admin_password:
hashed = pwd_context.hash(admin_password)
cursor.execute("""
UPDATE users SET password_hash = ?, updated_at = ?
WHERE username = ?
""", (hashed, utc_now_iso(), admin_username))
conn.commit()
class DatabaseManager:
"""
Manages SQLite database operations for Trinity platform.
This class composes operations from specialized submodules:
- User management: db/users.py
- Agent ownership/sharing: db/agents.py
- MCP API keys: db/mcp_keys.py
- Schedules/executions: db/schedules.py
- Chat sessions/messages: db/chat.py
- Activity stream: db/activities.py
All methods are delegated to the appropriate submodule while
maintaining the same interface for backward compatibility.
"""
def __init__(self):
init_database()
# Initialize operation handlers
self._user_ops = UserOperations()
self._agent_ops = AgentOperations(self._user_ops)
self._mcp_key_ops = McpKeyOperations(self._user_ops)
self._schedule_ops = ScheduleOperations(self._user_ops, self._agent_ops)
self._chat_ops = ChatOperations()
self._session_ops = SessionOperations()
self._activity_ops = ActivityOperations()
self._report_ops = ReportOperations()
self._permission_ops = PermissionOperations(self._user_ops, self._agent_ops)
self._shared_folder_ops = SharedFolderOperations(self._permission_ops)
self._agent_shared_files_ops = AgentSharedFilesOperations()
self._settings_ops = SettingsOperations()
self._public_link_ops = PublicLinkOperations(self._user_ops, self._agent_ops)
self._email_auth_ops = EmailAuthOperations(self._user_ops)
self._skills_ops = SkillsOperations()
self._public_chat_ops = PublicChatOperations()
self._tag_ops = TagOperations()
self._system_view_ops = SystemViewOperations()
self._notification_ops = NotificationOperations()
self._subscription_ops = SubscriptionOperations()
self._monitoring_ops = MonitoringOperations()
self._dashboard_history_ops = DashboardHistoryOperations()
self._slack_ops = SlackOperations()
self._slack_channel_ops = SlackChannelOperations()
self._nevermined_ops = NeverminedOperations()
self._operator_queue_ops = OperatorQueueOperations()
self._event_subscription_ops = EventSubscriptionOperations()
self._telegram_channel_ops = TelegramChannelOperations()
self._whatsapp_channel_ops = WhatsAppChannelOperations()
self._voip_ops = VoipOperations()
self._access_request_ops = AccessRequestOperations()
self._audit_ops = PlatformAuditOperations()
self._canary_ops = CanaryOperations()
self._compatibility_ops = CompatibilityOperations() # #668 agent compatibility
self._sync_state_ops = SyncStateOperations() # #389 sync health
self._idempotency_ops = IdempotencyOperations() # RELIABILITY-006, #525
self._loop_ops = LoopOperations() # #740 sequential agent loops
# =========================================================================
# User Management (delegated to db/users.py)
# =========================================================================
def get_user_by_username(self, username: str):
return self._user_ops.get_user_by_username(username)
def get_user_by_auth0_sub(self, auth0_sub: str):
return self._user_ops.get_user_by_auth0_sub(auth0_sub)
def get_user_by_id(self, user_id: int):
return self._user_ops.get_user_by_id(user_id)
def get_user_by_email(self, email: str):
return self._user_ops.get_user_by_email(email)
def create_user(self, user_data: UserCreate):
return self._user_ops.create_user(user_data)
def update_user(self, username: str, updates: dict):
return self._user_ops.update_user(username, updates)
def update_last_login(self, username: str):
return self._user_ops.update_last_login(username)
def update_user_password(self, username: str, hashed_password: str):
return self._user_ops.update_user_password(username, hashed_password)
def get_or_create_auth0_user(self, auth0_sub: str, email: str, name: str = None, picture: str = None):
return self._user_ops.get_or_create_auth0_user(auth0_sub, email, name, picture)
def list_users(self):
return self._user_ops.list_users()
def update_user_role(self, username: str, role: str):
return self._user_ops.update_user_role(username, role)
# =========================================================================
# Agent Ownership Management (delegated to db/agents.py)
# =========================================================================
def register_agent_owner(self, agent_name: str, owner_username: str, is_system: bool = False, require_email: bool = False):
return self._agent_ops.register_agent_owner(agent_name, owner_username, is_system, require_email)
def get_agent_owner(self, agent_name: str):
return self._agent_ops.get_agent_owner(agent_name)
def get_agents_by_owner(self, owner_username: str):
return self._agent_ops.get_agents_by_owner(owner_username)
def delete_agent_ownership(self, agent_name: str):
return self._agent_ops.delete_agent_ownership(agent_name)
def purge_agent_ownership(self, agent_name: str):
return self._agent_ops.purge_agent_ownership(agent_name)
def find_soft_deleted_agents_past_retention(self, retention_days: int, limit: int = 5000):
return self._agent_ops.find_soft_deleted_agents_past_retention(retention_days, limit)
def is_agent_name_reserved(self, agent_name: str):
return self._agent_ops.is_agent_name_reserved(agent_name)
def recover_agent_ownership(self, agent_name: str):
return self._agent_ops.recover_agent_ownership(agent_name)
def list_soft_deleted_agents(self, limit: int = 200):
return self._agent_ops.list_soft_deleted_agents(limit)
def rename_agent(self, old_name: str, new_name: str):
return self._agent_ops.rename_agent(old_name, new_name)
def can_user_access_agent(self, username: str, agent_name: str):
return self._agent_ops.can_user_access_agent(username, agent_name)
def can_user_delete_agent(self, username: str, agent_name: str):
return self._agent_ops.can_user_delete_agent(username, agent_name)
def can_user_rename_agent(self, username: str, agent_name: str):
return self._agent_ops.can_user_rename_agent(username, agent_name)
def is_system_agent(self, agent_name: str):
return self._agent_ops.is_system_agent(agent_name)
# Access policy (issue #311)
def get_access_policy(self, agent_name: str):
return self._agent_ops.get_access_policy(agent_name)
def set_access_policy(
self, agent_name: str, require_email: bool, open_access: bool, group_auth_mode: str = "none"
):
return self._agent_ops.set_access_policy(agent_name, require_email, open_access, group_auth_mode)
def email_has_agent_access(self, agent_name: str, email: str):
return self._agent_ops.email_has_agent_access(agent_name, email)
# =========================================================================
# Agent Sharing Management (delegated to db/agents.py)
# =========================================================================
def share_agent(self, agent_name: str, owner_username: str, share_with_email: str):
return self._agent_ops.share_agent(agent_name, owner_username, share_with_email)
def unshare_agent(self, agent_name: str, owner_username: str, share_with_email: str):
return self._agent_ops.unshare_agent(agent_name, owner_username, share_with_email)
def get_agent_shares(self, agent_name: str):
return self._agent_ops.get_agent_shares(agent_name)
def get_agent_operator_access(self, agent_name: str):
return self._agent_ops.get_agent_operator_access(agent_name)
def get_shared_agents(self, username: str):
return self._agent_ops.get_shared_agents(username)
def is_agent_shared_with_user(self, agent_name: str, username: str):
return self._agent_ops.is_agent_shared_with_user(agent_name, username)
def can_user_share_agent(self, username: str, agent_name: str):
return self._agent_ops.can_user_share_agent(username, agent_name)
def delete_agent_shares(self, agent_name: str):
return self._agent_ops.delete_agent_shares(agent_name)
# =========================================================================
# Proactive Messaging (Issue #321) - delegated to db/agents.py
# =========================================================================
def can_agent_message_email(self, agent_name: str, email: str):
"""Check if agent can send proactive messages to this email."""
return self._agent_ops.can_agent_message_email(agent_name, email)
def set_allow_proactive(self, agent_name: str, email: str, allow: bool, setter_username: str):
"""Update allow_proactive flag for a sharing record."""
return self._agent_ops.set_allow_proactive(agent_name, email, allow, setter_username)
def get_proactive_enabled_shares(self, agent_name: str):
"""Get all emails that have opted in to proactive messages from this agent."""
return self._agent_ops.get_proactive_enabled_shares(agent_name)
# =========================================================================
# Agent API Key Settings (delegated to db/agents.py)
# =========================================================================
def get_use_platform_api_key(self, agent_name: str):
return self._agent_ops.get_use_platform_api_key(agent_name)
def set_use_platform_api_key(self, agent_name: str, use_platform_key: bool):
return self._agent_ops.set_use_platform_api_key(agent_name, use_platform_key)
# =========================================================================
# Agent Autonomy Mode (delegated to db/agents.py)
# =========================================================================
def get_autonomy_enabled(self, agent_name: str):
return self._agent_ops.get_autonomy_enabled(agent_name)
def set_autonomy_enabled(self, agent_name: str, enabled: bool):
return self._agent_ops.set_autonomy_enabled(agent_name, enabled)
def get_all_agents_autonomy_status(self):
return self._agent_ops.get_all_agents_autonomy_status()
# =========================================================================
# Agent File Sharing (outbound — FILES-001)
# =========================================================================
def get_file_sharing_enabled(self, agent_name: str):
return self._agent_ops.get_file_sharing_enabled(agent_name)
def set_file_sharing_enabled(self, agent_name: str, enabled: bool):
return self._agent_ops.set_file_sharing_enabled(agent_name, enabled)
def get_public_volume_name(self, agent_name: str):
return self._agent_ops.get_public_volume_name(agent_name)
def get_public_mount_path(self):
return self._agent_ops.get_public_mount_path()
# =========================================================================
# Agent Shared Files (outbound file URLs — FILES-001 Step 3)
# =========================================================================
def create_agent_shared_file(self, **kwargs):
return self._agent_shared_files_ops.create(**kwargs)
def get_agent_shared_file(self, file_id: str):
return self._agent_shared_files_ops.get_by_id(file_id)
def get_agent_shared_file_by_token(self, download_token: str):
return self._agent_shared_files_ops.get_by_token(download_token)
def total_shared_file_bytes_for_agent(self, agent_name: str) -> int:
return self._agent_shared_files_ops.total_bytes_for_agent(agent_name)
def list_active_shared_files_for_agent(self, agent_name: str) -> list:
return self._agent_shared_files_ops.list_active_for_agent(agent_name)
def mark_shared_file_downloaded(self, file_id: str) -> None:
return self._agent_shared_files_ops.mark_downloaded(file_id)
def revoke_agent_shared_file(self, file_id: str) -> bool:
return self._agent_shared_files_ops.revoke(file_id)
def validate_agent_session(self, agent_name: str, session_token: str):
return self._public_link_ops.validate_agent_session(agent_name, session_token)
def delete_shared_files_for_agent(self, agent_name: str) -> list:
return self._agent_shared_files_ops.delete_for_agent(agent_name)
def delete_expired_and_revoked_shared_files(self, revoke_grace_hours: int = 24) -> list:
return self._agent_shared_files_ops.delete_expired_and_revoked(revoke_grace_hours=revoke_grace_hours)
# =========================================================================
# Batch Metadata Query (N+1 Fix) - delegated to db/agents.py
# =========================================================================
def get_all_agent_metadata(self, user_email: str = None):
return self._agent_ops.get_all_agent_metadata(user_email)
def get_accessible_agent_names(self, user_email: str, is_admin: bool = False):
"""Get list of agent names the user can access (owned + shared, or all if admin)."""
return self._agent_ops.get_accessible_agent_names(user_email, is_admin)
# =========================================================================
# Agent Resource Limits (delegated to db/agents.py)
# =========================================================================
def get_resource_limits(self, agent_name: str):
return self._agent_ops.get_resource_limits(agent_name)
def set_resource_limits(self, agent_name: str, memory: str = None, cpu: str = None):
return self._agent_ops.set_resource_limits(agent_name, memory, cpu)
# =========================================================================
# Agent Read-Only Mode (delegated to db/agents.py)
# =========================================================================
def get_read_only_mode(self, agent_name: str):
return self._agent_ops.get_read_only_mode(agent_name)
def set_read_only_mode(self, agent_name: str, enabled: bool, config: dict = None):
return self._agent_ops.set_read_only_mode(agent_name, enabled, config)
def get_full_capabilities(self, agent_name: str) -> bool:
return self._agent_ops.get_full_capabilities(agent_name)
def set_full_capabilities(self, agent_name: str, enabled: bool) -> bool:
return self._agent_ops.set_full_capabilities(agent_name, enabled)
# =========================================================================
# Agent Guardrails (GUARD-001)
# =========================================================================
def get_guardrails_config(self, agent_name: str) -> dict:
return self._agent_ops.get_guardrails_config(agent_name)
def set_guardrails_config(self, agent_name: str, config: dict = None) -> bool:
return self._agent_ops.set_guardrails_config(agent_name, config)
# =========================================================================
# Parallel Capacity (delegated to db/agents.py) - CAPACITY-001
# =========================================================================
def get_max_parallel_tasks(self, agent_name: str):
return self._agent_ops.get_max_parallel_tasks(agent_name)
def set_max_parallel_tasks(self, agent_name: str, max_tasks: int):
return self._agent_ops.set_max_parallel_tasks(agent_name, max_tasks)
def get_all_agents_parallel_capacity(self):
return self._agent_ops.get_all_agents_parallel_capacity()
# =========================================================================
# Dispatch Circuit Breaker opt-in (delegated to db/agents.py) - #526
# =========================================================================
def get_circuit_breaker_enabled(self, agent_name: str) -> bool:
return self._agent_ops.get_circuit_breaker_enabled(agent_name)
def set_circuit_breaker_enabled(self, agent_name: str, enabled: bool) -> bool:
return self._agent_ops.set_circuit_breaker_enabled(agent_name, enabled)
def get_all_circuit_breaker_enabled(self):
return self._agent_ops.get_all_circuit_breaker_enabled()
# =========================================================================
# Execution Timeout (delegated to db/agents.py) - TIMEOUT-001
# =========================================================================
def get_execution_timeout(self, agent_name: str) -> int:
return self._agent_ops.get_execution_timeout(agent_name)
def get_all_execution_timeouts(self) -> dict:
return self._agent_ops.get_all_execution_timeouts()
def set_execution_timeout(self, agent_name: str, timeout_seconds: int) -> bool:
return self._agent_ops.set_execution_timeout(agent_name, timeout_seconds)
# =========================================================================
# Backlog Depth (delegated to db/agent_settings/resources.py) - BACKLOG-001
# =========================================================================
def get_max_backlog_depth(self, agent_name: str) -> int:
return self._agent_ops.get_max_backlog_depth(agent_name)
def set_max_backlog_depth(self, agent_name: str, depth: int) -> bool:
return self._agent_ops.set_max_backlog_depth(agent_name, depth)
# =========================================================================
# Backlog Execution Queries (delegated to db/schedules.py) - BACKLOG-001
# =========================================================================
def update_execution_to_queued(self, execution_id: str, backlog_metadata: str, queued_at: str) -> bool:
return self._schedule_ops.update_execution_to_queued(execution_id, backlog_metadata, queued_at)
def claim_next_queued(self, agent_name: str):
return self._schedule_ops.claim_next_queued(agent_name)
def release_claim_to_queued(self, execution_id: str) -> bool:
return self._schedule_ops.release_claim_to_queued(execution_id)
def get_queued_count(self, agent_name: str) -> int:
return self._schedule_ops.get_queued_count(agent_name)
def cancel_queued_execution(self, execution_id: str, reason: str = "cancelled") -> bool:
return self._schedule_ops.cancel_queued_execution(execution_id, reason)
def cancel_queued_for_agent(self, agent_name: str, reason: str = "agent_deleted") -> int:
return self._schedule_ops.cancel_queued_for_agent(agent_name, reason)
def fail_queued_for_agent(self, agent_name: str, reason: str = "circuit_open") -> int:
return self._schedule_ops.fail_queued_for_agent(agent_name, reason)
def expire_stale_queued(self, max_age_hours: float = 24) -> int:
return self._schedule_ops.expire_stale_queued(max_age_hours)
def list_agents_with_queued(self):
return self._schedule_ops.list_agents_with_queued()
# =========================================================================
# Avatar Identity (delegated to db/agents.py) - AVATAR-001
# =========================================================================
def set_avatar_identity(self, agent_name: str, prompt: str, updated_at: str):
return self._agent_ops.set_avatar_identity(agent_name, prompt, updated_at)
def get_avatar_identity(self, agent_name: str):
return self._agent_ops.get_avatar_identity(agent_name)
def clear_avatar_identity(self, agent_name: str):
return self._agent_ops.clear_avatar_identity(agent_name)
def get_agents_without_custom_avatar(self):
return self._agent_ops.get_agents_without_custom_avatar()
def set_default_avatar(self, agent_name: str, identity_prompt: str, updated_at: str):
return self._agent_ops.set_default_avatar(agent_name, identity_prompt, updated_at)
# =========================================================================
# GitHub PAT (delegated to db/agents.py) - #347
# =========================================================================
def get_agent_github_pat(self, agent_name: str):
return self._agent_ops.get_agent_github_pat(agent_name)
def set_agent_github_pat(self, agent_name: str, pat: str) -> bool:
return self._agent_ops.set_agent_github_pat(agent_name, pat)
def clear_agent_github_pat(self, agent_name: str) -> bool:
return self._agent_ops.clear_agent_github_pat(agent_name)
def has_agent_github_pat(self, agent_name: str) -> bool:
return self._agent_ops.has_agent_github_pat(agent_name)
# =========================================================================
# Voice System Prompt (delegated to db/agents.py) - VOICE-005
# =========================================================================
def get_voice_system_prompt(self, agent_name: str):
return self._agent_ops.get_voice_system_prompt(agent_name)
def set_voice_system_prompt(self, agent_name: str, prompt: str):
return self._agent_ops.set_voice_system_prompt(agent_name, prompt)
def get_voice_name(self, agent_name: str):
return self._agent_ops.get_voice_name(agent_name)
def set_voice_name(self, agent_name: str, voice_name):
return self._agent_ops.set_voice_name(agent_name, voice_name)
# =========================================================================
# MCP API Key Management (delegated to db/mcp_keys.py)
# =========================================================================
def create_mcp_api_key(self, username: str, key_data: McpApiKeyCreate):
return self._mcp_key_ops.create_mcp_api_key(username, key_data)
def create_agent_mcp_api_key(self, agent_name: str, owner_username: str, description: str = None):
return self._mcp_key_ops.create_agent_mcp_api_key(agent_name, owner_username, description)
def get_agent_mcp_api_key(self, agent_name: str):
return self._mcp_key_ops.get_agent_mcp_api_key(agent_name)
def delete_agent_mcp_api_key(self, agent_name: str):
return self._mcp_key_ops.delete_agent_mcp_api_key(agent_name)
def validate_mcp_api_key(self, api_key: str, *, track_usage: bool = True):
return self._mcp_key_ops.validate_mcp_api_key(api_key, track_usage=track_usage)
def get_mcp_api_key(self, key_id: str, username: str):
return self._mcp_key_ops.get_mcp_api_key(key_id, username)
def list_mcp_api_keys(self, username: str):
return self._mcp_key_ops.list_mcp_api_keys(username)
def list_all_mcp_api_keys(self):
return self._mcp_key_ops.list_all_mcp_api_keys()
def revoke_mcp_api_key(self, key_id: str, username: str):
return self._mcp_key_ops.revoke_mcp_api_key(key_id, username)
def delete_mcp_api_key(self, key_id: str, username: str):
return self._mcp_key_ops.delete_mcp_api_key(key_id, username)
# =========================================================================
# Schedule Management (delegated to db/schedules.py)
# =========================================================================
def create_schedule(self, agent_name: str, username: str, schedule_data: ScheduleCreate):
return self._schedule_ops.create_schedule(agent_name, username, schedule_data)
def get_schedule(self, schedule_id: str):
return self._schedule_ops.get_schedule(schedule_id)
def list_agent_schedules(self, agent_name: str):
return self._schedule_ops.list_agent_schedules(agent_name)
def find_active_schedules_exceeding_timeout(self, agent_name: str, ceiling_seconds: int):
return self._schedule_ops.find_active_schedules_exceeding_timeout(
agent_name, ceiling_seconds
)
def list_all_enabled_schedules(self):
return self._schedule_ops.list_all_enabled_schedules()
def list_all_disabled_schedules(self):
return self._schedule_ops.list_all_disabled_schedules()
def list_all_schedules(self):
"""List all schedules across all agents."""
return self._schedule_ops.list_all_schedules()
def update_schedule(self, schedule_id: str, username: str, updates: dict):
return self._schedule_ops.update_schedule(schedule_id, username, updates)
def delete_schedule(self, schedule_id: str, username: str):
return self._schedule_ops.delete_schedule(schedule_id, username)
def purge_schedule(self, schedule_id: str):
return self._schedule_ops.purge_schedule(schedule_id)
def find_soft_deleted_schedules_past_retention(self, retention_days: int, limit: int = 5000):
return self._schedule_ops.find_soft_deleted_schedules_past_retention(retention_days, limit)
def recover_schedule(self, schedule_id: str):
return self._schedule_ops.recover_schedule(schedule_id)
def list_soft_deleted_schedules(self, agent_name=None, limit: int = 200):
return self._schedule_ops.list_soft_deleted_schedules(agent_name, limit)
# Webhook token management (WEBHOOK-001, #291)
def generate_webhook_token(self, schedule_id: str):
return self._schedule_ops.generate_webhook_token(schedule_id)
def get_schedule_by_webhook_token(self, token: str):
return self._schedule_ops.get_schedule_by_webhook_token(token)
def revoke_webhook_token(self, schedule_id: str):
return self._schedule_ops.revoke_webhook_token(schedule_id)
def get_webhook_status(self, schedule_id: str):
return self._schedule_ops.get_webhook_status(schedule_id)
def set_schedule_enabled(self, schedule_id: str, enabled: bool):
return self._schedule_ops.set_schedule_enabled(schedule_id, enabled)
def update_schedule_run_times(self, schedule_id: str, last_run_at=None, next_run_at=None):
return self._schedule_ops.update_schedule_run_times(schedule_id, last_run_at, next_run_at)
def delete_agent_schedules(self, agent_name: str):
return self._schedule_ops.delete_agent_schedules(agent_name)
# =========================================================================
# Schedule Execution Management (delegated to db/schedules.py)
# =========================================================================
def create_task_execution(
self,
agent_name: str,
message: str,
triggered_by: str = "manual",
source_user_id: int = None,
source_user_email: str = None,
source_agent_name: str = None,
source_mcp_key_id: str = None,
source_mcp_key_name: str = None,
model_used: str = None,
fan_out_id: str = None,
loop_id: str = None,
subscription_id: str = None,
):
"""Create an execution record for a manual/API-triggered task (no schedule)."""
return self._schedule_ops.create_task_execution(
agent_name, message, triggered_by,
source_user_id=source_user_id,
source_user_email=source_user_email,
source_agent_name=source_agent_name,
source_mcp_key_id=source_mcp_key_id,
source_mcp_key_name=source_mcp_key_name,
model_used=model_used,
fan_out_id=fan_out_id,
loop_id=loop_id,
subscription_id=subscription_id,
)
def create_schedule_execution(
self,
schedule_id: str,
agent_name: str,
message: str,
triggered_by: str = "schedule",
source_user_id: int = None,
source_user_email: str = None,
source_agent_name: str = None,
source_mcp_key_id: str = None,
source_mcp_key_name: str = None,
subscription_id: str = None,
):
return self._schedule_ops.create_schedule_execution(
schedule_id, agent_name, message, triggered_by,
source_user_id=source_user_id,
source_user_email=source_user_email,
source_agent_name=source_agent_name,
source_mcp_key_id=source_mcp_key_id,
source_mcp_key_name=source_mcp_key_name,
subscription_id=subscription_id,
)
def update_execution_status(self, execution_id: str, status: str, response: str = None, error: str = None,
context_used: int = None, context_max: int = None, cost: float = None, tool_calls: str = None, execution_log: str = None,
claude_session_id: str = None, compact_metadata: str = None, retry_count: int = None):
return self._schedule_ops.update_execution_status(execution_id, status, response, error,
context_used, context_max, cost, tool_calls, execution_log, claude_session_id,
compact_metadata, retry_count)
def mark_execution_dispatched(self, execution_id: str, async_dispatch: bool = False) -> bool:
return self._schedule_ops.mark_execution_dispatched(execution_id, async_dispatch)
def get_schedule_executions(self, schedule_id: str, limit: int = 50):
return self._schedule_ops.get_schedule_executions(schedule_id, limit)
def get_latest_execution_per_schedule(self, schedule_ids: list):
return self._schedule_ops.get_latest_execution_per_schedule(schedule_ids)
def get_agent_executions(self, agent_name: str, limit: int = 50):
return self._schedule_ops.get_agent_executions(agent_name, limit)
def get_agent_executions_summary(self, agent_name: str, limit: int = 50):
"""Get execution summaries for list view - excludes large text fields.
PERF-001: Task List Performance Optimization
"""
return self._schedule_ops.get_agent_executions_summary(agent_name, limit)
def get_execution(self, execution_id: str):
return self._schedule_ops.get_execution(execution_id)
def get_all_agents_execution_stats(self, hours: int = 24):
"""Get execution statistics for all agents."""
return self._schedule_ops.get_all_agents_execution_stats(hours)
def get_all_agents_execution_stats_dual(self):
"""Get execution statistics for all agents with both 24h and 7d windows."""
return self._schedule_ops.get_all_agents_execution_stats_dual()
def get_all_agents_schedule_counts(self):
"""Get schedule counts (total and enabled) for all agents."""
return self._schedule_ops.get_all_agents_schedule_counts()
def get_fleet_executions(self, agent_names, **kwargs):
"""Cross-fleet execution list (EXEC-022 / Issue #18)."""
return self._schedule_ops.get_fleet_executions(agent_names, **kwargs)
def get_fleet_execution_stats(self, agent_names, hours: int = 24):
"""Aggregate stats for the fleet executions stat cards (EXEC-022 / Issue #18)."""
return self._schedule_ops.get_fleet_execution_stats(agent_names, hours)
# =========================================================================
# Git Configuration Management (delegated to db/schedules.py)
# =========================================================================
def create_git_config(
self,
agent_name: str,
github_repo: str,
working_branch: str,
instance_id: str,
sync_paths=None,
source_branch: str = "main",
source_mode: bool = False
):
return self._schedule_ops.create_git_config(
agent_name, github_repo, working_branch, instance_id, sync_paths,
source_branch=source_branch, source_mode=source_mode
)
def get_git_config(self, agent_name: str):
return self._schedule_ops.get_git_config(agent_name)
def update_git_sync(self, agent_name: str, commit_sha: str):
return self._schedule_ops.update_git_sync(agent_name, commit_sha)
def set_git_sync_enabled(self, agent_name: str, enabled: bool):
return self._schedule_ops.set_git_sync_enabled(agent_name, enabled)
# #389 sync health observability
def set_git_auto_sync_enabled(self, agent_name: str, enabled: bool):
return self._schedule_ops.set_git_auto_sync_enabled(agent_name, enabled)
def set_freeze_schedules_if_sync_failing(self, agent_name: str, enabled: bool):
return self._schedule_ops.set_freeze_schedules_if_sync_failing(agent_name, enabled)
def get_git_auto_sync_enabled(self, agent_name: str):
return self._schedule_ops.get_git_auto_sync_enabled(agent_name)