mdb-engine 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. mdb_engine/__init__.py +38 -6
  2. mdb_engine/auth/README.md +534 -11
  3. mdb_engine/auth/__init__.py +129 -28
  4. mdb_engine/auth/audit.py +592 -0
  5. mdb_engine/auth/casbin_factory.py +10 -14
  6. mdb_engine/auth/config_helpers.py +7 -6
  7. mdb_engine/auth/cookie_utils.py +3 -7
  8. mdb_engine/auth/csrf.py +373 -0
  9. mdb_engine/auth/decorators.py +3 -10
  10. mdb_engine/auth/dependencies.py +37 -45
  11. mdb_engine/auth/helpers.py +3 -3
  12. mdb_engine/auth/integration.py +30 -73
  13. mdb_engine/auth/jwt.py +2 -6
  14. mdb_engine/auth/middleware.py +77 -34
  15. mdb_engine/auth/oso_factory.py +16 -36
  16. mdb_engine/auth/provider.py +17 -38
  17. mdb_engine/auth/rate_limiter.py +504 -0
  18. mdb_engine/auth/restrictions.py +8 -24
  19. mdb_engine/auth/session_manager.py +14 -29
  20. mdb_engine/auth/shared_middleware.py +600 -0
  21. mdb_engine/auth/shared_users.py +759 -0
  22. mdb_engine/auth/token_store.py +14 -28
  23. mdb_engine/auth/users.py +54 -113
  24. mdb_engine/auth/utils.py +213 -15
  25. mdb_engine/cli/commands/generate.py +545 -9
  26. mdb_engine/cli/commands/validate.py +3 -7
  27. mdb_engine/cli/utils.py +3 -3
  28. mdb_engine/config.py +7 -21
  29. mdb_engine/constants.py +65 -0
  30. mdb_engine/core/README.md +117 -6
  31. mdb_engine/core/__init__.py +39 -7
  32. mdb_engine/core/app_registration.py +22 -41
  33. mdb_engine/core/app_secrets.py +290 -0
  34. mdb_engine/core/connection.py +18 -9
  35. mdb_engine/core/encryption.py +223 -0
  36. mdb_engine/core/engine.py +758 -95
  37. mdb_engine/core/index_management.py +12 -16
  38. mdb_engine/core/manifest.py +424 -135
  39. mdb_engine/core/ray_integration.py +435 -0
  40. mdb_engine/core/seeding.py +10 -18
  41. mdb_engine/core/service_initialization.py +12 -23
  42. mdb_engine/core/types.py +2 -5
  43. mdb_engine/database/README.md +112 -16
  44. mdb_engine/database/__init__.py +17 -6
  45. mdb_engine/database/abstraction.py +25 -37
  46. mdb_engine/database/connection.py +11 -18
  47. mdb_engine/database/query_validator.py +367 -0
  48. mdb_engine/database/resource_limiter.py +204 -0
  49. mdb_engine/database/scoped_wrapper.py +713 -196
  50. mdb_engine/embeddings/__init__.py +17 -9
  51. mdb_engine/embeddings/dependencies.py +1 -3
  52. mdb_engine/embeddings/service.py +11 -25
  53. mdb_engine/exceptions.py +92 -0
  54. mdb_engine/indexes/README.md +30 -13
  55. mdb_engine/indexes/__init__.py +1 -0
  56. mdb_engine/indexes/helpers.py +1 -1
  57. mdb_engine/indexes/manager.py +50 -114
  58. mdb_engine/memory/README.md +2 -2
  59. mdb_engine/memory/__init__.py +1 -2
  60. mdb_engine/memory/service.py +30 -87
  61. mdb_engine/observability/README.md +4 -2
  62. mdb_engine/observability/__init__.py +26 -9
  63. mdb_engine/observability/health.py +8 -9
  64. mdb_engine/observability/metrics.py +32 -12
  65. mdb_engine/routing/README.md +1 -1
  66. mdb_engine/routing/__init__.py +1 -3
  67. mdb_engine/routing/websockets.py +25 -60
  68. mdb_engine-0.1.7.dist-info/METADATA +285 -0
  69. mdb_engine-0.1.7.dist-info/RECORD +85 -0
  70. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  71. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  72. {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/WHEEL +0 -0
  73. {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/entry_points.txt +0 -0
  74. {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/licenses/LICENSE +0 -0
  75. {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/top_level.txt +0 -0
@@ -36,9 +36,14 @@ from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
36
36
 
37
37
  from jsonschema import SchemaError, ValidationError, validate
38
38
 
39
- from ..constants import (CURRENT_SCHEMA_VERSION, DEFAULT_SCHEMA_VERSION,
40
- MAX_TTL_SECONDS, MAX_VECTOR_DIMENSIONS,
41
- MIN_TTL_SECONDS, MIN_VECTOR_DIMENSIONS)
39
+ from ..constants import (
40
+ CURRENT_SCHEMA_VERSION,
41
+ DEFAULT_SCHEMA_VERSION,
42
+ MAX_TTL_SECONDS,
43
+ MAX_VECTOR_DIMENSIONS,
44
+ MIN_TTL_SECONDS,
45
+ MIN_VECTOR_DIMENSIONS,
46
+ )
42
47
 
43
48
  logger = logging.getLogger(__name__)
44
49
 
@@ -87,9 +92,7 @@ def _get_manifest_hash(manifest_data: Dict[str, Any]) -> str:
87
92
 
88
93
  # Normalize manifest by removing metadata fields that don't affect validation
89
94
  normalized = {
90
- k: v
91
- for k, v in manifest_data.items()
92
- if k not in ["_id", "_updated", "_created", "url"]
95
+ k: v for k, v in manifest_data.items() if k not in ["_id", "_updated", "_created", "url"]
93
96
  }
94
97
  normalized_str = json.dumps(normalized, sort_keys=True)
95
98
  return hashlib.sha256(normalized_str.encode()).hexdigest()[:16]
@@ -137,6 +140,49 @@ MANIFEST_SCHEMA_V2 = {
137
140
  "auth": {
138
141
  "type": "object",
139
142
  "properties": {
143
+ "mode": {
144
+ "type": "string",
145
+ "enum": ["app", "shared"],
146
+ "default": "app",
147
+ "description": (
148
+ "Authentication mode: 'app' for per-app tokens "
149
+ "(isolated auth per app, default), 'shared' for "
150
+ "shared user pool with SSO across all apps. "
151
+ "Modes are mutually exclusive."
152
+ ),
153
+ },
154
+ "roles": {
155
+ "type": "array",
156
+ "items": {"type": "string"},
157
+ "description": (
158
+ "Available roles for this app (shared mode only). "
159
+ "Example: ['viewer', 'editor', 'admin']"
160
+ ),
161
+ },
162
+ "default_role": {
163
+ "type": "string",
164
+ "description": (
165
+ "Default role assigned to new users for this app "
166
+ "(shared mode only). Must be one of the defined roles."
167
+ ),
168
+ },
169
+ "require_role": {
170
+ "type": "string",
171
+ "description": (
172
+ "Minimum role required to access this app "
173
+ "(shared mode only). Users without this role "
174
+ "will be denied access."
175
+ ),
176
+ },
177
+ "public_routes": {
178
+ "type": "array",
179
+ "items": {"type": "string"},
180
+ "description": (
181
+ "Routes that don't require authentication. "
182
+ "Supports wildcards, e.g., ['/health', '/api/public/*']. "
183
+ "Works in both auth modes."
184
+ ),
185
+ },
140
186
  "policy": {
141
187
  "type": "object",
142
188
  "properties": {
@@ -484,8 +530,7 @@ MANIFEST_SCHEMA_V2 = {
484
530
  "type": "string",
485
531
  "default": "user",
486
532
  "description": (
487
- "Role for demo user in app "
488
- "(default: 'user')"
533
+ "Role for demo user in app " "(default: 'user')"
489
534
  ),
490
535
  },
491
536
  "auto_create": {
@@ -567,6 +612,292 @@ MANIFEST_SCHEMA_V2 = {
567
612
  "independent of platform authentication."
568
613
  ),
569
614
  },
615
+ "rate_limits": {
616
+ "type": "object",
617
+ "additionalProperties": {
618
+ "type": "object",
619
+ "properties": {
620
+ "max_attempts": {
621
+ "type": "integer",
622
+ "minimum": 1,
623
+ "default": 5,
624
+ "description": (
625
+ "Maximum attempts allowed in the time window " "(default: 5)."
626
+ ),
627
+ },
628
+ "window_seconds": {
629
+ "type": "integer",
630
+ "minimum": 1,
631
+ "default": 300,
632
+ "description": (
633
+ "Time window in seconds for rate limiting "
634
+ "(default: 300 = 5 minutes)."
635
+ ),
636
+ },
637
+ },
638
+ "additionalProperties": False,
639
+ },
640
+ "description": (
641
+ "Rate limiting configuration for auth endpoints. "
642
+ "Keys are endpoint paths (e.g., '/login', '/register'). "
643
+ "Example: {'/login': {'max_attempts': 5, 'window_seconds': 300}}"
644
+ ),
645
+ },
646
+ "audit": {
647
+ "type": "object",
648
+ "properties": {
649
+ "enabled": {
650
+ "type": "boolean",
651
+ "default": True,
652
+ "description": (
653
+ "Enable audit logging for authentication events "
654
+ "(default: true for shared auth mode)."
655
+ ),
656
+ },
657
+ "retention_days": {
658
+ "type": "integer",
659
+ "minimum": 1,
660
+ "default": 90,
661
+ "description": (
662
+ "Number of days to retain audit logs " "(default: 90 days)."
663
+ ),
664
+ },
665
+ },
666
+ "additionalProperties": False,
667
+ "description": (
668
+ "Audit logging configuration for authentication events. "
669
+ "Logs are stored in MongoDB with automatic TTL cleanup."
670
+ ),
671
+ },
672
+ "csrf_protection": {
673
+ "oneOf": [
674
+ {"type": "boolean"},
675
+ {
676
+ "type": "object",
677
+ "properties": {
678
+ "enabled": {
679
+ "type": "boolean",
680
+ "default": True,
681
+ "description": (
682
+ "Enable CSRF protection "
683
+ "(default: true for shared auth mode)."
684
+ ),
685
+ },
686
+ "exempt_routes": {
687
+ "type": "array",
688
+ "items": {"type": "string"},
689
+ "description": (
690
+ "Routes exempt from CSRF validation. "
691
+ "Supports wildcards (e.g., '/api/*'). "
692
+ "Defaults to public_routes if not specified."
693
+ ),
694
+ },
695
+ "rotate_tokens": {
696
+ "type": "boolean",
697
+ "default": False,
698
+ "description": (
699
+ "Rotate CSRF token on each request "
700
+ "(more secure, less convenient). Default: false."
701
+ ),
702
+ },
703
+ "token_ttl": {
704
+ "type": "integer",
705
+ "minimum": 60,
706
+ "default": 3600,
707
+ "description": (
708
+ "CSRF token TTL in seconds " "(default: 3600 = 1 hour)."
709
+ ),
710
+ },
711
+ },
712
+ "additionalProperties": False,
713
+ },
714
+ ],
715
+ "default": True,
716
+ "description": (
717
+ "CSRF protection configuration. Auto-enabled for shared "
718
+ "auth mode. Set to false to disable, or provide object "
719
+ "for detailed configuration. Uses double-submit cookie "
720
+ "pattern with SameSite=Lax cookies."
721
+ ),
722
+ },
723
+ "security": {
724
+ "type": "object",
725
+ "properties": {
726
+ "hsts": {
727
+ "type": "object",
728
+ "properties": {
729
+ "enabled": {
730
+ "type": "boolean",
731
+ "default": True,
732
+ "description": (
733
+ "Enable HSTS header in production " "(default: true)."
734
+ ),
735
+ },
736
+ "max_age": {
737
+ "type": "integer",
738
+ "minimum": 0,
739
+ "default": 31536000,
740
+ "description": (
741
+ "HSTS max-age in seconds " "(default: 31536000 = 1 year)."
742
+ ),
743
+ },
744
+ "include_subdomains": {
745
+ "type": "boolean",
746
+ "default": True,
747
+ "description": (
748
+ "Include subdomains in HSTS policy " "(default: true)."
749
+ ),
750
+ },
751
+ "preload": {
752
+ "type": "boolean",
753
+ "default": False,
754
+ "description": (
755
+ "Add preload directive for HSTS preload "
756
+ "list submission (default: false). Only "
757
+ "enable if you're ready for permanent HTTPS."
758
+ ),
759
+ },
760
+ },
761
+ "additionalProperties": False,
762
+ "description": (
763
+ "HTTP Strict Transport Security configuration. "
764
+ "Forces HTTPS connections in production."
765
+ ),
766
+ },
767
+ },
768
+ "additionalProperties": False,
769
+ "description": (
770
+ "Security settings including HSTS, headers, and other " "security controls."
771
+ ),
772
+ },
773
+ "jwt": {
774
+ "type": "object",
775
+ "properties": {
776
+ "algorithm": {
777
+ "type": "string",
778
+ "enum": ["HS256", "RS256", "ES256"],
779
+ "default": "HS256",
780
+ "description": (
781
+ "JWT signing algorithm. HS256 (HMAC, default) "
782
+ "uses symmetric secret. RS256 (RSA) and ES256 "
783
+ "(ECDSA) use asymmetric key pairs for better "
784
+ "security in distributed systems."
785
+ ),
786
+ },
787
+ "token_expiry_hours": {
788
+ "type": "integer",
789
+ "minimum": 1,
790
+ "default": 24,
791
+ "description": ("JWT token expiry in hours (default: 24)."),
792
+ },
793
+ },
794
+ "additionalProperties": False,
795
+ "description": (
796
+ "JWT configuration for shared auth mode. "
797
+ "Controls algorithm and token lifetime."
798
+ ),
799
+ },
800
+ "password_policy": {
801
+ "type": "object",
802
+ "properties": {
803
+ "min_length": {
804
+ "type": "integer",
805
+ "minimum": 6,
806
+ "default": 12,
807
+ "description": ("Minimum password length (default: 12)."),
808
+ },
809
+ "min_entropy_bits": {
810
+ "type": "integer",
811
+ "minimum": 0,
812
+ "default": 50,
813
+ "description": (
814
+ "Minimum password entropy in bits "
815
+ "(default: 50). Set to 0 to disable."
816
+ ),
817
+ },
818
+ "require_uppercase": {
819
+ "type": "boolean",
820
+ "default": True,
821
+ "description": (
822
+ "Require at least one uppercase letter " "(default: true)."
823
+ ),
824
+ },
825
+ "require_lowercase": {
826
+ "type": "boolean",
827
+ "default": True,
828
+ "description": (
829
+ "Require at least one lowercase letter " "(default: true)."
830
+ ),
831
+ },
832
+ "require_numbers": {
833
+ "type": "boolean",
834
+ "default": True,
835
+ "description": ("Require at least one number " "(default: true)."),
836
+ },
837
+ "require_special": {
838
+ "type": "boolean",
839
+ "default": False,
840
+ "description": (
841
+ "Require at least one special character " "(default: false)."
842
+ ),
843
+ },
844
+ "check_common_passwords": {
845
+ "type": "boolean",
846
+ "default": True,
847
+ "description": (
848
+ "Check against common password list " "(default: true)."
849
+ ),
850
+ },
851
+ "check_breaches": {
852
+ "type": "boolean",
853
+ "default": False,
854
+ "description": (
855
+ "Check against HaveIBeenPwned breach database "
856
+ "(default: false). Requires network access."
857
+ ),
858
+ },
859
+ },
860
+ "additionalProperties": False,
861
+ "description": (
862
+ "Password policy configuration for shared auth mode. "
863
+ "Enforces password strength requirements."
864
+ ),
865
+ },
866
+ "session_binding": {
867
+ "type": "object",
868
+ "properties": {
869
+ "bind_ip": {
870
+ "type": "boolean",
871
+ "default": False,
872
+ "description": (
873
+ "Bind sessions to IP address. Strict mode: "
874
+ "reject if IP changes (default: false)."
875
+ ),
876
+ },
877
+ "bind_fingerprint": {
878
+ "type": "boolean",
879
+ "default": True,
880
+ "description": (
881
+ "Bind sessions to device fingerprint. Soft mode: "
882
+ "log warning if fingerprint changes (default: true)."
883
+ ),
884
+ },
885
+ "allow_ip_change_with_reauth": {
886
+ "type": "boolean",
887
+ "default": True,
888
+ "description": (
889
+ "Allow IP change if user re-authenticates "
890
+ "(default: true). Only applies when bind_ip=true."
891
+ ),
892
+ },
893
+ },
894
+ "additionalProperties": False,
895
+ "description": (
896
+ "Session binding configuration. Ties sessions to client "
897
+ "characteristics for additional security against session "
898
+ "hijacking."
899
+ ),
900
+ },
570
901
  },
571
902
  "additionalProperties": False,
572
903
  "description": (
@@ -590,17 +921,13 @@ MANIFEST_SCHEMA_V2 = {
590
921
  "type": "integer",
591
922
  "minimum": 60,
592
923
  "default": 900,
593
- "description": (
594
- "Access token TTL in seconds " "(default: 900 = 15 minutes)."
595
- ),
924
+ "description": ("Access token TTL in seconds " "(default: 900 = 15 minutes)."),
596
925
  },
597
926
  "refresh_token_ttl": {
598
927
  "type": "integer",
599
928
  "minimum": 3600,
600
929
  "default": 604800,
601
- "description": (
602
- "Refresh token TTL in seconds " "(default: 604800 = 7 days)."
603
- ),
930
+ "description": ("Refresh token TTL in seconds " "(default: 604800 = 7 days)."),
604
931
  },
605
932
  "token_rotation": {
606
933
  "type": "boolean",
@@ -615,8 +942,7 @@ MANIFEST_SCHEMA_V2 = {
615
942
  "minimum": 1,
616
943
  "default": 10,
617
944
  "description": (
618
- "Maximum number of concurrent sessions per user "
619
- "(default: 10)."
945
+ "Maximum number of concurrent sessions per user " "(default: 10)."
620
946
  ),
621
947
  },
622
948
  "session_inactivity_timeout": {
@@ -635,8 +961,7 @@ MANIFEST_SCHEMA_V2 = {
635
961
  "type": "boolean",
636
962
  "default": False,
637
963
  "description": (
638
- "Require HTTPS in production "
639
- "(default: false, auto-detected)."
964
+ "Require HTTPS in production " "(default: false, auto-detected)."
640
965
  ),
641
966
  },
642
967
  "cookie_secure": {
@@ -718,9 +1043,7 @@ MANIFEST_SCHEMA_V2 = {
718
1043
  },
719
1044
  },
720
1045
  "additionalProperties": False,
721
- "description": (
722
- "Rate limiting configuration per endpoint type."
723
- ),
1046
+ "description": ("Rate limiting configuration per endpoint type."),
724
1047
  },
725
1048
  "password_policy": {
726
1049
  "type": "object",
@@ -747,9 +1070,7 @@ MANIFEST_SCHEMA_V2 = {
747
1070
  "require_lowercase": {
748
1071
  "type": "boolean",
749
1072
  "default": True,
750
- "description": (
751
- "Require lowercase letters " "(default: true)"
752
- ),
1073
+ "description": ("Require lowercase letters " "(default: true)"),
753
1074
  },
754
1075
  "require_numbers": {
755
1076
  "type": "boolean",
@@ -774,24 +1095,21 @@ MANIFEST_SCHEMA_V2 = {
774
1095
  "type": "boolean",
775
1096
  "default": True,
776
1097
  "description": (
777
- "Enable session fingerprinting "
778
- "(default: true)"
1098
+ "Enable session fingerprinting " "(default: true)"
779
1099
  ),
780
1100
  },
781
1101
  "validate_on_login": {
782
1102
  "type": "boolean",
783
1103
  "default": True,
784
1104
  "description": (
785
- "Validate fingerprint on login "
786
- "(default: true)"
1105
+ "Validate fingerprint on login " "(default: true)"
787
1106
  ),
788
1107
  },
789
1108
  "validate_on_refresh": {
790
1109
  "type": "boolean",
791
1110
  "default": True,
792
1111
  "description": (
793
- "Validate fingerprint on token refresh "
794
- "(default: true)"
1112
+ "Validate fingerprint on token refresh " "(default: true)"
795
1113
  ),
796
1114
  },
797
1115
  "validate_on_request": {
@@ -837,8 +1155,7 @@ MANIFEST_SCHEMA_V2 = {
837
1155
  "minimum": 1,
838
1156
  "default": 900,
839
1157
  "description": (
840
- "Lockout duration in seconds "
841
- "(default: 900 = 15 minutes)"
1158
+ "Lockout duration in seconds " "(default: 900 = 15 minutes)"
842
1159
  ),
843
1160
  },
844
1161
  "reset_on_success": {
@@ -860,8 +1177,7 @@ MANIFEST_SCHEMA_V2 = {
860
1177
  "type": "boolean",
861
1178
  "default": False,
862
1179
  "description": (
863
- "Enable IP address validation "
864
- "(default: false)"
1180
+ "Enable IP address validation " "(default: false)"
865
1181
  ),
866
1182
  },
867
1183
  "strict": {
@@ -876,8 +1192,7 @@ MANIFEST_SCHEMA_V2 = {
876
1192
  "type": "boolean",
877
1193
  "default": True,
878
1194
  "description": (
879
- "Allow IP address changes during session "
880
- "(default: true)"
1195
+ "Allow IP address changes during session " "(default: true)"
881
1196
  ),
882
1197
  },
883
1198
  },
@@ -897,9 +1212,7 @@ MANIFEST_SCHEMA_V2 = {
897
1212
  "bind_to_device": {
898
1213
  "type": "boolean",
899
1214
  "default": True,
900
- "description": (
901
- "Bind tokens to device ID " "(default: true)"
902
- ),
1215
+ "description": ("Bind tokens to device ID " "(default: true)"),
903
1216
  },
904
1217
  },
905
1218
  "additionalProperties": False,
@@ -913,8 +1226,7 @@ MANIFEST_SCHEMA_V2 = {
913
1226
  "type": "boolean",
914
1227
  "default": True,
915
1228
  "description": (
916
- "Automatically set up token management on app startup "
917
- "(default: true)."
1229
+ "Automatically set up token management on app startup " "(default: true)."
918
1230
  ),
919
1231
  },
920
1232
  },
@@ -946,9 +1258,7 @@ MANIFEST_SCHEMA_V2 = {
946
1258
  },
947
1259
  "collection_settings": {
948
1260
  "type": "object",
949
- "patternProperties": {
950
- "^[a-zA-Z0-9_]+$": {"$ref": "#/definitions/collectionSettings"}
951
- },
1261
+ "patternProperties": {"^[a-zA-Z0-9_]+$": {"$ref": "#/definitions/collectionSettings"}},
952
1262
  "description": "Collection name -> collection settings",
953
1263
  },
954
1264
  "websockets": {
@@ -996,8 +1306,7 @@ MANIFEST_SCHEMA_V2 = {
996
1306
  "description": {
997
1307
  "type": "string",
998
1308
  "description": (
999
- "Description of what this WebSocket endpoint "
1000
- "is used for"
1309
+ "Description of what this WebSocket endpoint " "is used for"
1001
1310
  ),
1002
1311
  },
1003
1312
  "ping_interval": {
@@ -1210,8 +1519,7 @@ MANIFEST_SCHEMA_V2 = {
1210
1519
  "type": "boolean",
1211
1520
  "default": False,
1212
1521
  "description": (
1213
- "Allow credentials (cookies, authorization headers) "
1214
- "in CORS requests"
1522
+ "Allow credentials (cookies, authorization headers) " "in CORS requests"
1215
1523
  ),
1216
1524
  },
1217
1525
  "allow_methods": {
@@ -1256,6 +1564,40 @@ MANIFEST_SCHEMA_V2 = {
1256
1564
  "additionalProperties": False,
1257
1565
  "description": "CORS (Cross-Origin Resource Sharing) configuration for web apps",
1258
1566
  },
1567
+ "data_access": {
1568
+ "type": "object",
1569
+ "properties": {
1570
+ "read_scopes": {
1571
+ "type": "array",
1572
+ "items": {"type": "string"},
1573
+ "description": (
1574
+ "List of app slugs this app can read from. "
1575
+ "Defaults to [app_slug] if not specified."
1576
+ ),
1577
+ },
1578
+ "write_scope": {
1579
+ "type": "string",
1580
+ "description": (
1581
+ "App slug this app writes to. " "Defaults to app_slug if not specified."
1582
+ ),
1583
+ },
1584
+ "cross_app_policy": {
1585
+ "type": "string",
1586
+ "enum": ["explicit", "deny_all"],
1587
+ "default": "explicit",
1588
+ "description": (
1589
+ "Policy for cross-app access. 'explicit' allows access "
1590
+ "to apps listed in read_scopes. 'deny_all' blocks all "
1591
+ "cross-app access regardless of read_scopes."
1592
+ ),
1593
+ },
1594
+ },
1595
+ "additionalProperties": False,
1596
+ "description": (
1597
+ "Data access configuration defining which apps this app can "
1598
+ "read from and write to. Used for cross-app data access control."
1599
+ ),
1600
+ },
1259
1601
  "observability": {
1260
1602
  "type": "object",
1261
1603
  "properties": {
@@ -1295,8 +1637,7 @@ MANIFEST_SCHEMA_V2 = {
1295
1637
  "type": "boolean",
1296
1638
  "default": True,
1297
1639
  "description": (
1298
- "Collect operation-level metrics "
1299
- "(duration, errors, etc.)"
1640
+ "Collect operation-level metrics " "(duration, errors, etc.)"
1300
1641
  ),
1301
1642
  },
1302
1643
  "collect_performance_metrics": {
@@ -1456,8 +1797,7 @@ MANIFEST_SCHEMA_V2 = {
1456
1797
  "definition": {
1457
1798
  "type": "object",
1458
1799
  "description": (
1459
- "Index definition (required for vectorSearch and "
1460
- "search indexes)"
1800
+ "Index definition (required for vectorSearch and " "search indexes)"
1461
1801
  ),
1462
1802
  },
1463
1803
  "hybrid": {
@@ -1470,8 +1810,7 @@ MANIFEST_SCHEMA_V2 = {
1470
1810
  "type": "string",
1471
1811
  "pattern": "^[a-zA-Z0-9_]+$",
1472
1812
  "description": (
1473
- "Name for the vector index "
1474
- "(defaults to '{name}_vector')"
1813
+ "Name for the vector index " "(defaults to '{name}_vector')"
1475
1814
  ),
1476
1815
  },
1477
1816
  "definition": {
@@ -1493,8 +1832,7 @@ MANIFEST_SCHEMA_V2 = {
1493
1832
  "type": "string",
1494
1833
  "pattern": "^[a-zA-Z0-9_]+$",
1495
1834
  "description": (
1496
- "Name for the text index "
1497
- "(defaults to '{name}_text')"
1835
+ "Name for the text index " "(defaults to '{name}_text')"
1498
1836
  ),
1499
1837
  },
1500
1838
  "definition": {
@@ -1575,9 +1913,7 @@ MANIFEST_SCHEMA_V2 = {
1575
1913
  "then": {"required": ["keys", "options"]},
1576
1914
  "else": {
1577
1915
  "properties": {
1578
- "options": {
1579
- "not": {"required": ["partialFilterExpression"]}
1580
- }
1916
+ "options": {"not": {"required": ["partialFilterExpression"]}}
1581
1917
  }
1582
1918
  },
1583
1919
  },
@@ -1803,9 +2139,7 @@ def migrate_manifest(
1803
2139
 
1804
2140
  # No data transformation needed - V2.0 is backward compatible
1805
2141
  # New fields (auth, etc.) are optional
1806
- logger.debug(
1807
- f"Migrated manifest from 1.0 to 2.0: {migrated.get('slug', 'unknown')}"
1808
- )
2142
+ logger.debug(f"Migrated manifest from 1.0 to 2.0: {migrated.get('slug', 'unknown')}")
1809
2143
 
1810
2144
  # Future: Add more migration paths as needed
1811
2145
  # Example: 2.0 -> 3.0, etc.
@@ -1841,8 +2175,7 @@ def get_schema_for_version(version: str) -> Dict[str, Any]:
1841
2175
 
1842
2176
  # Fallback to current
1843
2177
  logger.warning(
1844
- f"Schema version {version} not found, using current version "
1845
- f"{CURRENT_SCHEMA_VERSION}"
2178
+ f"Schema version {version} not found, using current version " f"{CURRENT_SCHEMA_VERSION}"
1846
2179
  )
1847
2180
  return SCHEMA_REGISTRY[CURRENT_SCHEMA_VERSION]
1848
2181
 
@@ -1873,12 +2206,11 @@ async def _validate_manifest_async(
1873
2206
  Use validate_manifest_with_db() for database validation.
1874
2207
  """
1875
2208
  # Check cache first
2209
+ cache_key = None
1876
2210
  if use_cache:
1877
- cache_key = (
1878
- _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
1879
- )
1880
- if cache_key in _validation_cache:
1881
- return _validation_cache[cache_key]
2211
+ cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
2212
+ if cache_key in _validation_cache:
2213
+ return _validation_cache[cache_key]
1882
2214
 
1883
2215
  try:
1884
2216
  # Get schema version
@@ -1935,11 +2267,7 @@ async def _validate_manifest_async(
1935
2267
  error_message = f"Invalid schema definition: {e.message}"
1936
2268
  result = (False, error_message, ["schema"])
1937
2269
  if use_cache:
1938
- cache_key = (
1939
- _get_manifest_hash(manifest_data)
1940
- + "_"
1941
- + get_schema_version(manifest_data)
1942
- )
2270
+ cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
1943
2271
  _validation_cache[cache_key] = result
1944
2272
 
1945
2273
  return result
@@ -1949,9 +2277,7 @@ async def _validate_manifest_async(
1949
2277
  error_paths = []
1950
2278
  error_messages = []
1951
2279
  if isinstance(e, ValidationError):
1952
- error_paths = [
1953
- f".{'.'.join(str(p) for p in error.path)}" for error in e.context or [e]
1954
- ]
2280
+ error_paths = [f".{'.'.join(str(p) for p in error.path)}" for error in e.context or [e]]
1955
2281
  error_messages = [error.message for error in e.context or [e]]
1956
2282
  else:
1957
2283
  error_messages = [str(e)]
@@ -1959,11 +2285,7 @@ async def _validate_manifest_async(
1959
2285
  error_message = "; ".join(error_messages) if error_messages else str(e)
1960
2286
  result = (False, error_message, error_paths if error_paths else None)
1961
2287
  if use_cache:
1962
- cache_key = (
1963
- _get_manifest_hash(manifest_data)
1964
- + "_"
1965
- + get_schema_version(manifest_data)
1966
- )
2288
+ cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
1967
2289
  _validation_cache[cache_key] = result
1968
2290
 
1969
2291
  return result
@@ -1973,11 +2295,7 @@ async def _validate_manifest_async(
1973
2295
  logger.exception("Unexpected error during manifest validation")
1974
2296
  result = (False, error_message, None)
1975
2297
  if use_cache:
1976
- cache_key = (
1977
- _get_manifest_hash(manifest_data)
1978
- + "_"
1979
- + get_schema_version(manifest_data)
1980
- )
2298
+ cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
1981
2299
  _validation_cache[cache_key] = result
1982
2300
 
1983
2301
  return result
@@ -2006,27 +2324,21 @@ async def validate_manifests_parallel(
2006
2324
  """
2007
2325
 
2008
2326
  async def validate_one(
2009
- manifest: Dict[str, Any]
2327
+ manifest: Dict[str, Any],
2010
2328
  ) -> Tuple[bool, Optional[str], Optional[List[str]], Optional[str]]:
2011
2329
  slug = manifest.get("slug", "unknown")
2012
- is_valid, error, paths = await _validate_manifest_async(
2013
- manifest, use_cache=use_cache
2014
- )
2330
+ is_valid, error, paths = await _validate_manifest_async(manifest, use_cache=use_cache)
2015
2331
  return (is_valid, error, paths, slug)
2016
2332
 
2017
2333
  # Run validations in parallel
2018
- results = await asyncio.gather(
2019
- *[validate_one(m) for m in manifests], return_exceptions=True
2020
- )
2334
+ results = await asyncio.gather(*[validate_one(m) for m in manifests], return_exceptions=True)
2021
2335
 
2022
2336
  # Handle exceptions
2023
2337
  validated_results = []
2024
2338
  for i, result in enumerate(results):
2025
2339
  if isinstance(result, Exception):
2026
2340
  slug = manifests[i].get("slug", "unknown")
2027
- validated_results.append(
2028
- (False, f"Validation error: {str(result)}", None, slug)
2029
- )
2341
+ validated_results.append((False, f"Validation error: {str(result)}", None, slug))
2030
2342
  else:
2031
2343
  validated_results.append(result)
2032
2344
 
@@ -2072,9 +2384,7 @@ async def validate_developer_id(
2072
2384
  f"developer_id '{developer_id}' does not exist or does not have developer role",
2073
2385
  )
2074
2386
  except (ValueError, TypeError, AttributeError) as e:
2075
- logger.exception(
2076
- f"Validation error validating developer_id '{developer_id}'"
2077
- )
2387
+ logger.exception(f"Validation error validating developer_id '{developer_id}'")
2078
2388
  return False, f"Error validating developer_id: {e}"
2079
2389
 
2080
2390
  return True, None
@@ -2154,15 +2464,11 @@ def validate_manifest(
2154
2464
 
2155
2465
  with concurrent.futures.ThreadPoolExecutor() as executor:
2156
2466
  future = executor.submit(
2157
- lambda: asyncio.run(
2158
- _validate_manifest_async(manifest_data, use_cache)
2159
- )
2467
+ lambda: asyncio.run(_validate_manifest_async(manifest_data, use_cache))
2160
2468
  )
2161
2469
  return future.result()
2162
2470
  else:
2163
- return loop.run_until_complete(
2164
- _validate_manifest_async(manifest_data, use_cache)
2165
- )
2471
+ return loop.run_until_complete(_validate_manifest_async(manifest_data, use_cache))
2166
2472
  except RuntimeError:
2167
2473
  # No event loop, create one
2168
2474
  return asyncio.run(_validate_manifest_async(manifest_data, use_cache))
@@ -2186,8 +2492,7 @@ def _validate_regular_index(
2186
2492
  ):
2187
2493
  return (
2188
2494
  False,
2189
- f"Regular index '{index_name}' in collection "
2190
- f"'{collection_name}' has empty 'keys'",
2495
+ f"Regular index '{index_name}' in collection " f"'{collection_name}' has empty 'keys'",
2191
2496
  )
2192
2497
 
2193
2498
  # Check for _id index
@@ -2214,8 +2519,7 @@ def _validate_ttl_index(
2214
2519
  if "keys" not in index_def:
2215
2520
  return (
2216
2521
  False,
2217
- f"TTL index '{index_name}' in collection '{collection_name}' "
2218
- f"requires 'keys' field",
2522
+ f"TTL index '{index_name}' in collection '{collection_name}' " f"requires 'keys' field",
2219
2523
  )
2220
2524
  options = index_def.get("options", {})
2221
2525
  if "expireAfterSeconds" not in options:
@@ -2305,9 +2609,7 @@ def _validate_geospatial_index(
2305
2609
  if isinstance(keys, dict):
2306
2610
  has_geo = any(v in ["2dsphere", "2d", "geoHaystack"] for v in keys.values())
2307
2611
  elif isinstance(keys, list):
2308
- has_geo = any(
2309
- len(k) >= 2 and k[1] in ["2dsphere", "2d", "geoHaystack"] for k in keys
2310
- )
2612
+ has_geo = any(len(k) >= 2 and k[1] in ["2dsphere", "2d", "geoHaystack"] for k in keys)
2311
2613
  if not has_geo:
2312
2614
  return (
2313
2615
  False,
@@ -2448,8 +2750,7 @@ def validate_index_definition(
2448
2750
  if not index_type:
2449
2751
  return (
2450
2752
  False,
2451
- f"Index '{index_name}' in collection '{collection_name}' "
2452
- f"is missing 'type' field",
2753
+ f"Index '{index_name}' in collection '{collection_name}' " f"is missing 'type' field",
2453
2754
  )
2454
2755
 
2455
2756
  # Type-specific validation
@@ -2464,9 +2765,7 @@ def validate_index_definition(
2464
2765
  elif index_type == "geospatial":
2465
2766
  return _validate_geospatial_index(index_def, collection_name, index_name)
2466
2767
  elif index_type in ("vectorSearch", "search"):
2467
- return _validate_vector_search_index(
2468
- index_def, collection_name, index_name, index_type
2469
- )
2768
+ return _validate_vector_search_index(index_def, collection_name, index_name, index_type)
2470
2769
  elif index_type == "hybrid":
2471
2770
  return _validate_hybrid_index(index_def, collection_name, index_name)
2472
2771
  else:
@@ -2478,7 +2777,7 @@ def validate_index_definition(
2478
2777
 
2479
2778
 
2480
2779
  def validate_managed_indexes(
2481
- managed_indexes: Dict[str, List[Dict[str, Any]]]
2780
+ managed_indexes: Dict[str, List[Dict[str, Any]]],
2482
2781
  ) -> Tuple[bool, Optional[str]]:
2483
2782
  """
2484
2783
  Validate all managed indexes with collection and index context.
@@ -2516,9 +2815,7 @@ def validate_managed_indexes(
2516
2815
  )
2517
2816
 
2518
2817
  index_name = index_def.get("name", f"index_{idx}")
2519
- is_valid, error_msg = validate_index_definition(
2520
- index_def, collection_name, index_name
2521
- )
2818
+ is_valid, error_msg = validate_index_definition(index_def, collection_name, index_name)
2522
2819
  if not is_valid:
2523
2820
  return False, error_msg
2524
2821
 
@@ -2600,13 +2897,11 @@ class ManifestValidator:
2600
2897
  Returns:
2601
2898
  Tuple of (is_valid, error_message, error_paths)
2602
2899
  """
2603
- return await validate_manifest_with_db(
2604
- manifest, db_validator, use_cache=use_cache
2605
- )
2900
+ return await validate_manifest_with_db(manifest, db_validator, use_cache=use_cache)
2606
2901
 
2607
2902
  @staticmethod
2608
2903
  def validate_managed_indexes(
2609
- managed_indexes: Dict[str, List[Dict[str, Any]]]
2904
+ managed_indexes: Dict[str, List[Dict[str, Any]]],
2610
2905
  ) -> Tuple[bool, Optional[str]]:
2611
2906
  """
2612
2907
  Validate managed indexes configuration.
@@ -2720,17 +3015,13 @@ class ManifestParser:
2720
3015
  if validate:
2721
3016
  is_valid, error, paths = ManifestValidator.validate(manifest_data)
2722
3017
  if not is_valid:
2723
- error_path_str = (
2724
- f" (errors in: {', '.join(paths[:3])})" if paths else ""
2725
- )
3018
+ error_path_str = f" (errors in: {', '.join(paths[:3])})" if paths else ""
2726
3019
  raise ValueError(f"Manifest validation failed: {error}{error_path_str}")
2727
3020
 
2728
3021
  return manifest_data
2729
3022
 
2730
3023
  @staticmethod
2731
- async def load_from_dict(
2732
- data: Dict[str, Any], validate: bool = True
2733
- ) -> Dict[str, Any]:
3024
+ async def load_from_dict(data: Dict[str, Any], validate: bool = True) -> Dict[str, Any]:
2734
3025
  """
2735
3026
  Load and validate manifest from dictionary.
2736
3027
 
@@ -2748,9 +3039,7 @@ class ManifestParser:
2748
3039
  if validate:
2749
3040
  is_valid, error, paths = ManifestValidator.validate(data)
2750
3041
  if not is_valid:
2751
- error_path_str = (
2752
- f" (errors in: {', '.join(paths[:3])})" if paths else ""
2753
- )
3042
+ error_path_str = f" (errors in: {', '.join(paths[:3])})" if paths else ""
2754
3043
  raise ValueError(f"Manifest validation failed: {error}{error_path_str}")
2755
3044
 
2756
3045
  return data.copy()