@tdk-landscape/tdk-cli-core 1.3.14 → 1.3.22

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 (277) hide show
  1. package/README.md +3 -3
  2. package/Tiltfile +144 -0
  3. package/discovery/AGENTS.md +362 -0
  4. package/discovery/TILT_DISCOVERY.star +424 -0
  5. package/discovery/Tiltfile +58 -0
  6. package/discovery/config.star +258 -0
  7. package/discovery/constants.star +127 -0
  8. package/discovery/deterministic.star +211 -0
  9. package/discovery/discovery_daemon.star +191 -0
  10. package/discovery/discovery_orchestrator.star +460 -0
  11. package/discovery/filtering.star +5 -0
  12. package/discovery/identity-management-backend/service.yaml +26 -0
  13. package/discovery/identity-management-frontend/service.yaml +29 -0
  14. package/discovery/identity-sdk/service.yaml +9 -0
  15. package/discovery/incremental_discovery.star +213 -0
  16. package/discovery/indexing.star +7 -0
  17. package/discovery/json_manifest_scanner.star +51 -0
  18. package/discovery/libraries.star +46 -0
  19. package/discovery/loading.star +23 -0
  20. package/discovery/manifest/constants.star +104 -0
  21. package/discovery/manifest/loading.star +386 -0
  22. package/discovery/manifest/normalize.star +281 -0
  23. package/discovery/manifest/validation.star +108 -0
  24. package/discovery/normalization.star +11 -0
  25. package/discovery/registry.star +544 -0
  26. package/discovery/resource_snapshot.py +382 -0
  27. package/discovery/resource_snapshot.star +143 -0
  28. package/discovery/schemas.star +1 -0
  29. package/discovery/services/platform/cdc/debezium/nats-connector/service.yaml +9 -0
  30. package/discovery/services/platform/cdc/nats-http-bridge/service.yaml +9 -0
  31. package/discovery/services/platform/orchestrator/orchestrator-glue-backend/service.yaml +26 -0
  32. package/discovery/services/platform/synthetic-monitor/service.yaml +9 -0
  33. package/discovery/test_resource_snapshot.py +251 -0
  34. package/discovery/validation.star +78 -0
  35. package/discovery/versioning.star +1 -0
  36. package/dist/commands/config.d.ts.map +1 -1
  37. package/dist/commands/config.js +11 -0
  38. package/dist/commands/config.js.map +1 -1
  39. package/dist/commands/doctor.d.ts.map +1 -1
  40. package/dist/commands/doctor.js +1 -2
  41. package/dist/commands/doctor.js.map +1 -1
  42. package/dist/commands/project.d.ts.map +1 -1
  43. package/dist/commands/project.js +36 -41
  44. package/dist/commands/project.js.map +1 -1
  45. package/dist/commands/resource.d.ts.map +1 -1
  46. package/dist/commands/resource.js +51 -55
  47. package/dist/commands/resource.js.map +1 -1
  48. package/dist/commands/stack.d.ts.map +1 -1
  49. package/dist/commands/stack.js +22 -24
  50. package/dist/commands/stack.js.map +1 -1
  51. package/dist/commands/ui.d.ts.map +1 -1
  52. package/dist/commands/ui.js +1 -2
  53. package/dist/commands/ui.js.map +1 -1
  54. package/dist/commands/up.d.ts.map +1 -1
  55. package/dist/commands/up.js +1 -2
  56. package/dist/commands/up.js.map +1 -1
  57. package/dist/commands/upgrade.d.ts +8 -0
  58. package/dist/commands/upgrade.d.ts.map +1 -1
  59. package/dist/commands/upgrade.js +62 -21
  60. package/dist/commands/upgrade.js.map +1 -1
  61. package/dist/generator/extension-fetch.d.ts +19 -0
  62. package/dist/generator/extension-fetch.d.ts.map +1 -1
  63. package/dist/generator/extension-fetch.js +71 -5
  64. package/dist/generator/extension-fetch.js.map +1 -1
  65. package/dist/generator/template-engine.d.ts +1 -0
  66. package/dist/generator/template-engine.d.ts.map +1 -1
  67. package/dist/generator/template-engine.js +50 -6
  68. package/dist/generator/template-engine.js.map +1 -1
  69. package/dist/utils/command-helpers.d.ts.map +1 -1
  70. package/dist/utils/command-helpers.js +7 -9
  71. package/dist/utils/command-helpers.js.map +1 -1
  72. package/dist/utils/constants.d.ts +1 -0
  73. package/dist/utils/constants.d.ts.map +1 -1
  74. package/dist/utils/constants.js +1 -0
  75. package/dist/utils/constants.js.map +1 -1
  76. package/dist/utils/errors.d.ts +2 -0
  77. package/dist/utils/errors.d.ts.map +1 -1
  78. package/dist/utils/errors.js +6 -1
  79. package/dist/utils/errors.js.map +1 -1
  80. package/dist/utils/paths.js +1 -1
  81. package/dist/utils/paths.js.map +1 -1
  82. package/dist/utils/project-features.d.ts.map +1 -1
  83. package/dist/utils/project-features.js +1 -1
  84. package/dist/utils/project-features.js.map +1 -1
  85. package/dist/utils/resource-features.d.ts.map +1 -1
  86. package/dist/utils/resource-features.js +2 -2
  87. package/dist/utils/resource-features.js.map +1 -1
  88. package/dist/utils/tar.js +4 -4
  89. package/dist/utils/tar.js.map +1 -1
  90. package/dist/utils/validation.d.ts +7 -0
  91. package/dist/utils/validation.d.ts.map +1 -1
  92. package/dist/utils/validation.js +9 -0
  93. package/dist/utils/validation.js.map +1 -1
  94. package/engine/AGENTS.md +705 -0
  95. package/engine/README.md +29 -0
  96. package/engine/REFACTORING_MANIFEST_SYSTEM.md +974 -0
  97. package/engine/Tiltfile +80 -0
  98. package/engine/assets/docker/golden-layers.Dockerfile +221 -0
  99. package/engine/assets/scripts/cleanup-completed-migrator-images.sh +37 -0
  100. package/engine/assets/scripts/cleanup-tilt-images.sh +120 -0
  101. package/engine/assets/scripts/mac-m1-resource-monitor.sh +121 -0
  102. package/engine/config.star +3 -0
  103. package/engine/docs/AUTODISCOVERY_TEST.md +182 -0
  104. package/engine/docs/C4-ARCHITECTURE-operations.md +396 -0
  105. package/engine/docs/C4-ARCHITECTURE-overview.md +284 -0
  106. package/engine/docs/C4-ARCHITECTURE.md +17 -0
  107. package/engine/docs/GOLDEN_IMAGE_INTEGRATION.md +349 -0
  108. package/engine/docs/MANIFEST_DRIVEN_ARCHITECTURE.md +357 -0
  109. package/engine/docs/README.md +284 -0
  110. package/engine/entrypoint.star +97 -0
  111. package/engine/lifecycle/orchestrator.star +260 -0
  112. package/engine/paths.star +717 -0
  113. package/engine/schemas/manifest-schema.json +301 -0
  114. package/engine/schemas/service-schema.json +301 -0
  115. package/engine/scripts/README.md +89 -0
  116. package/engine/scripts/build-and-publish-lib.sh +205 -0
  117. package/engine/scripts/check-lib-publish.sh +85 -0
  118. package/engine/scripts/debug-container.sh +258 -0
  119. package/engine/scripts/generate-manifest.sh +197 -0
  120. package/engine/spec.master +702 -0
  121. package/engine/spec.master.example +341 -0
  122. package/engine/topologies/README.md +34 -0
  123. package/engine/topologies/platform/docker/AGENTS.md +59 -0
  124. package/engine/topologies/platform/docker/auth/auth.star +167 -0
  125. package/engine/topologies/platform/docker/build/golden_image.star +37 -0
  126. package/engine/topologies/platform/docker/build/golden_image_build.star +82 -0
  127. package/engine/topologies/platform/docker/build/golden_image_constants.star +93 -0
  128. package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +291 -0
  129. package/engine/topologies/platform/docker/compose/compose.star +636 -0
  130. package/engine/topologies/platform/docker/compose/compose_helpers.star +52 -0
  131. package/engine/topologies/platform/docker/compose/traefik_standalone.star +53 -0
  132. package/engine/topologies/platform/docker/config/bunignore.star +142 -0
  133. package/engine/topologies/platform/docker/config/dockerignore.star +150 -0
  134. package/engine/topologies/platform/docker/constants.star +170 -0
  135. package/engine/topologies/platform/docker/dockerfile/dockerfile.star +54 -0
  136. package/engine/topologies/platform/docker/generators/golden_docker_generator_v2.star +73 -0
  137. package/engine/topologies/platform/docker/index.star +27 -0
  138. package/engine/topologies/platform/docker/layers/bun_helpers.star +23 -0
  139. package/engine/topologies/platform/docker/layers/infisical/index.star +40 -0
  140. package/engine/topologies/platform/docker/layers/infisical/infisical_docker.star +262 -0
  141. package/engine/topologies/platform/docker/layers/l1_base_layers.star +87 -0
  142. package/engine/topologies/platform/docker/layers/l2_dependency_layers.star +125 -0
  143. package/engine/topologies/platform/docker/layers/l3_builder_layers.star +209 -0
  144. package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +214 -0
  145. package/engine/topologies/platform/docker/layers/prisma/constants.star +135 -0
  146. package/engine/topologies/platform/docker/layers/prisma/db_readiness.star +96 -0
  147. package/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +109 -0
  148. package/engine/topologies/platform/docker/layers/prisma/migrate_script.star +98 -0
  149. package/engine/topologies/platform/docker/layers/prisma/prisma_build.star +126 -0
  150. package/engine/topologies/platform/docker/layers/prisma/prisma_runtime.star +161 -0
  151. package/engine/topologies/platform/docker/layers/prisma/secret_resolution.star +119 -0
  152. package/engine/topologies/platform/docker/networking/api_path_constants.star +230 -0
  153. package/engine/topologies/platform/docker/networking/sablier_container_cycle.star +27 -0
  154. package/engine/topologies/platform/docker/networking/traefik.star +232 -0
  155. package/engine/topologies/platform/docker/networking/traefik_constants.star +146 -0
  156. package/engine/topologies/platform/docker/networking/traefik_helpers.star +160 -0
  157. package/engine/topologies/platform/docker/secrets/infisical.star +135 -0
  158. package/engine/topologies/platform/net/proxy.star +4 -0
  159. package/engine/topologies/platform/registries/local.star +4 -0
  160. package/engine/topologies/platform/security/secrets.star +239 -0
  161. package/engine/topologies/platform/state/relational.star +4 -0
  162. package/engine/topologies/tilt/common/AGENTS.md +59 -0
  163. package/engine/topologies/tilt/common/utils.star +364 -0
  164. package/engine/topologies/tilt/common/utils_debug.star +69 -0
  165. package/engine/topologies/tilt/common/utils_deterministic.star +168 -0
  166. package/engine/topologies/tilt/common/utils_docker_networks.star +46 -0
  167. package/engine/topologies/tilt/common/utils_env.star +43 -0
  168. package/engine/topologies/tilt/common/utils_output.star +97 -0
  169. package/engine/topologies/tilt/config/AGENTS.md +68 -0
  170. package/engine/topologies/tilt/config/defaults.star +3 -0
  171. package/engine/topologies/tilt/config/feature_flags.star +10 -0
  172. package/engine/topologies/tilt/config/global.star +55 -0
  173. package/engine/topologies/tilt/config/profiles.star +382 -0
  174. package/engine/topologies/tilt/generators/agents_md.star +62 -0
  175. package/engine/topologies/tilt/generators/api_gateway.star +145 -0
  176. package/engine/topologies/tilt/generators/index.star +124 -0
  177. package/engine/topologies/tilt/generators/infisical/README.md +424 -0
  178. package/engine/topologies/tilt/generators/infisical/examples.star +312 -0
  179. package/engine/topologies/tilt/generators/infisical/index.star +57 -0
  180. package/engine/topologies/tilt/generators/infisical/machine_identity.star +420 -0
  181. package/engine/topologies/tilt/generators/infisical/organization.star +534 -0
  182. package/engine/topologies/tilt/generators/infisical/path_manager.star +476 -0
  183. package/engine/topologies/tilt/generators/infisical/secrets_generator.star +420 -0
  184. package/engine/topologies/tilt/generators/nginx_conf.star +143 -0
  185. package/engine/topologies/tilt/generators/npmrc.star +111 -0
  186. package/engine/topologies/tilt/generators/tsconfig.star +23 -0
  187. package/engine/topologies/tilt/generators/typescript/backend_library_tsconfig.star +71 -0
  188. package/engine/topologies/tilt/generators/typescript/backend_tsconfig.star +124 -0
  189. package/engine/topologies/tilt/generators/typescript/common.star +7 -0
  190. package/engine/topologies/tilt/generators/typescript/frontend_library_tsconfig.star +76 -0
  191. package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +110 -0
  192. package/engine/topologies/tilt/generators/typescript/include.star +23 -0
  193. package/engine/topologies/tilt/generators/typescript/playwright_config.star +40 -0
  194. package/engine/topologies/tilt/generators/typescript/prisma_config.star +49 -0
  195. package/engine/topologies/tilt/generators/validators.star +853 -0
  196. package/engine/topologies/tilt/generators/vite/backend.star +78 -0
  197. package/engine/topologies/tilt/generators/vite/frontend.star +98 -0
  198. package/engine/topologies/tilt/generators/vite/generators.star +43 -0
  199. package/engine/topologies/tilt/generators/vite/helpers.star +244 -0
  200. package/engine/topologies/tilt/generators/vite/library.star +65 -0
  201. package/engine/topologies/tilt/generators/vite/sdk.star +73 -0
  202. package/engine/topologies/tilt/generators/vite/templates.star +737 -0
  203. package/engine/topologies/tilt/generators/vite_config.star +22 -0
  204. package/engine/topologies/tilt/manifest/AGENTS.md +61 -0
  205. package/engine/topologies/tilt/manifest/README.md +693 -0
  206. package/engine/topologies/tilt/manifest/__init__.star +218 -0
  207. package/engine/topologies/tilt/manifest/constants.star +252 -0
  208. package/engine/topologies/tilt/manifest/errors.star +266 -0
  209. package/engine/topologies/tilt/manifest/integration.star +339 -0
  210. package/engine/topologies/tilt/manifest/json_to_yaml.py +54 -0
  211. package/engine/topologies/tilt/manifest/loader.star +214 -0
  212. package/engine/topologies/tilt/manifest/mapper.star +227 -0
  213. package/engine/topologies/tilt/manifest/parser.star +400 -0
  214. package/engine/topologies/tilt/manifest/schema.star +462 -0
  215. package/engine/topologies/tilt/manifest/tests/test_integration.star +276 -0
  216. package/engine/topologies/tilt/manifest/tests/test_loader_parser.star +397 -0
  217. package/engine/topologies/tilt/manifest/tests/test_manifest.star +316 -0
  218. package/engine/topologies/tilt/manifest/tests/test_validator.star +470 -0
  219. package/engine/topologies/tilt/manifest/validator.star +717 -0
  220. package/engine/topologies/tilt/manifest/yaml_to_json.py +18 -0
  221. package/engine/topologies/tilt/resources/AGENTS.md +71 -0
  222. package/engine/topologies/tilt/resources/conditions.star +3 -0
  223. package/engine/topologies/tilt/resources/databases.star +244 -0
  224. package/engine/topologies/tilt/resources/declaration.star +3 -0
  225. package/engine/topologies/tilt/resources/deps.star +3 -0
  226. package/engine/topologies/tilt/resources/infra-loader.star +437 -0
  227. package/engine/topologies/tilt/resources/libs.star +30 -0
  228. package/engine/topologies/tilt/resources/libs_discovery.star +102 -0
  229. package/engine/topologies/tilt/resources/libs_paths.star +79 -0
  230. package/engine/topologies/tilt/resources/libs_resource_definition.star +151 -0
  231. package/engine/topologies/tilt/resources/libs_setup.star +164 -0
  232. package/engine/topologies/tilt/resources/libs_tsconfig.star +37 -0
  233. package/engine/topologies/tilt/resources/orchestrator/apply.star +77 -0
  234. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +431 -0
  235. package/engine/topologies/tilt/resources/orchestrator/apply_manifest_orchestration.star +81 -0
  236. package/engine/topologies/tilt/resources/orchestrator/apply_migrator_orchestration.star +148 -0
  237. package/engine/topologies/tilt/resources/orchestrator/apply_resource_validation.star +36 -0
  238. package/engine/topologies/tilt/resources/orchestrator/apply_runtime_flags.star +44 -0
  239. package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +164 -0
  240. package/engine/topologies/tilt/resources/orchestrator/generators/dependencies/AGENTS.md +75 -0
  241. package/engine/topologies/tilt/resources/orchestrator/generators/dependencies/index.star +21 -0
  242. package/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +134 -0
  243. package/engine/topologies/tilt/resources/orchestrator/generators/env.star +258 -0
  244. package/engine/topologies/tilt/resources/orchestrator/generators/frontend.star +169 -0
  245. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +524 -0
  246. package/engine/topologies/tilt/resources/orchestrator/helpers.star +33 -0
  247. package/engine/topologies/tilt/resources/orchestrator.star +54 -0
  248. package/engine/topologies/tilt/resources/ordering.star +3 -0
  249. package/engine/topologies/tilt/resources/triggers.star +3 -0
  250. package/engine/topologies/tilt/resources/watchers.star +299 -0
  251. package/engine/where-things-live.star +717 -0
  252. package/ext/Tiltfile +66 -0
  253. package/ext/ide-components/README.md +219 -0
  254. package/ext/ide-components/code_executor/server.py +268 -0
  255. package/ext/ide-components/code_viewer/server.py +206 -0
  256. package/ext/ide-components/config_inspector/server.py +1050 -0
  257. package/ext/ide-components/file_browser/server.py +285 -0
  258. package/ext/ide-components/shared/config_service.py +469 -0
  259. package/ext/ide-components/shared/file_utils.py +298 -0
  260. package/ext/ide-components/shared/http_utils.py +232 -0
  261. package/ext/ide-components/shared/static/styles.css +1334 -0
  262. package/ext/ide-components/shared/syntax_highlighter.py +140 -0
  263. package/ext/ide-components/shared/template_engine.py +123 -0
  264. package/ext/ide-components/shared/templates/base.html +121 -0
  265. package/ext/ide-components/shared/test_config_service.py +326 -0
  266. package/ext/ide-components/shared/tilt_integration.py +158 -0
  267. package/ext/ui-enhancements/cron-status-server.sh +49 -0
  268. package/ext/ui-enhancements/cron-status-tracker.sh +146 -0
  269. package/ext/ui-enhancements/extension.yaml +36 -0
  270. package/ext/ui-enhancements/tiltfile.py +99 -0
  271. package/ext/ui-enhancements/ui/cron-jobs-tab.css +396 -0
  272. package/ext/ui-enhancements/ui/help-panel.css +279 -0
  273. package/ext/ui-enhancements/ui/icons.css +125 -0
  274. package/ext/ui-enhancements/ui/tooltips.css +114 -0
  275. package/package.json +13 -9
  276. package/specs/specs/TILT_SERVICE_DEFAULTS.star +205 -0
  277. package/specs/specs/TILT_TECH_STACK.star +159 -0
@@ -0,0 +1,469 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ConfigService - File operations for Config Inspector
4
+ Handles safe file reading/writing with validation, backups, and conflict detection.
5
+ """
6
+
7
+ import os
8
+ import hashlib
9
+ import tempfile
10
+ from pathlib import Path
11
+ from datetime import datetime
12
+ from typing import Optional, Tuple, NamedTuple
13
+
14
+ from file_utils import PROJECT_ROOT, FileUtils
15
+
16
+
17
+ class SaveResult(NamedTuple):
18
+ """Result of a save operation"""
19
+ success: bool
20
+ message: str
21
+ backup_path: Optional[Path] = None
22
+ error_details: Optional[str] = None
23
+
24
+
25
+ class ConfigService:
26
+ """
27
+ Service layer for config file operations.
28
+ Separates file operations from HTTP handling.
29
+ """
30
+
31
+ # Allowed paths for write operations
32
+ # NOTE: These are paths within the TDK CLI installation
33
+ ALLOWED_PATH_PREFIXES = [
34
+ 'engine/topologies/',
35
+ ]
36
+
37
+ # Master configs allowed at project root
38
+ ALLOWED_ROOT_CONFIGS = [
39
+ 'TILT_RESOURCE_DEFAULTS.star',
40
+ 'spec.master',
41
+ 'engine/spec.master',
42
+ ]
43
+
44
+ # Allowed file extensions
45
+ ALLOWED_EXTENSIONS = ['.star']
46
+
47
+ # Special filenames that are allowed without extension check
48
+ ALLOWED_FILENAMES = ['spec.master']
49
+
50
+ # File size limits
51
+ MAX_FILE_SIZE = 1024 * 1024 # 1MB
52
+ MAX_CONTENT_LENGTH = 1024 * 1024 # 1MB for request body
53
+
54
+ # Backup settings
55
+ BACKUP_DIR_NAME = '.backups'
56
+ MAX_BACKUPS = 3
57
+
58
+ @staticmethod
59
+ def validate_write_path(path: str) -> Optional[Path]:
60
+ """
61
+ Validate that a file path is safe for writing.
62
+
63
+ Security: Prevents path traversal attacks by using os.path.realpath()
64
+ and ensuring the resolved path is within allowed directories.
65
+
66
+ Allowed paths:
67
+ - Within engine/topologies/
68
+ - Project root master configs: TILT_RESOURCE_DEFAULTS.star, spec.master, engine/spec.master
69
+
70
+ Returns: Absolute Path if valid, None if invalid.
71
+ """
72
+ try:
73
+ # Handle relative paths
74
+ if not path.startswith('/'):
75
+ target = PROJECT_ROOT / path
76
+ else:
77
+ target = Path(path)
78
+
79
+ # Resolve to absolute path using realpath (follows symlinks, removes ..)
80
+ resolved = Path(os.path.realpath(target))
81
+ project_root_real = Path(os.path.realpath(PROJECT_ROOT))
82
+
83
+ # Check if path is within project root (prevent path traversal)
84
+ try:
85
+ resolved.relative_to(project_root_real)
86
+ except ValueError:
87
+ return None
88
+
89
+ # Get the relative path from project root
90
+ rel_path = resolved.relative_to(project_root_real)
91
+ rel_path_str = str(rel_path)
92
+
93
+ # Check if it's one of the allowed root configs
94
+ if rel_path_str in ConfigService.ALLOWED_ROOT_CONFIGS:
95
+ return resolved
96
+
97
+ # Check if it's within allowed prefixes
98
+ for prefix in ConfigService.ALLOWED_PATH_PREFIXES:
99
+ if rel_path_str.startswith(prefix):
100
+ return resolved
101
+
102
+ # Path is not in any allowed location
103
+ return None
104
+
105
+ except (ValueError, OSError, RuntimeError):
106
+ return None
107
+
108
+ @staticmethod
109
+ def validate_file_type(path: str) -> Tuple[bool, str]:
110
+ """
111
+ Validate that a file has an allowed extension or filename.
112
+
113
+ Allowed:
114
+ - Files with .star extension
115
+ - Files named exactly 'spec.master'
116
+
117
+ Returns: (is_valid, message)
118
+ """
119
+ path_obj = Path(path)
120
+ filename = path_obj.name
121
+ extension = path_obj.suffix.lower()
122
+
123
+ # Check if filename is in allowed list (spec.master)
124
+ if filename in ConfigService.ALLOWED_FILENAMES:
125
+ return True, "Valid"
126
+
127
+ # Check extension
128
+ if extension in ConfigService.ALLOWED_EXTENSIONS:
129
+ return True, "Valid"
130
+
131
+ return False, f"Invalid file type: only {', '.join(ConfigService.ALLOWED_EXTENSIONS)} and {', '.join(ConfigService.ALLOWED_FILENAMES)} are allowed"
132
+
133
+ @staticmethod
134
+ def check_file_size(path: Path) -> Tuple[bool, str]:
135
+ """
136
+ Check if a file size is within the allowed limit.
137
+
138
+ Returns: (is_valid, message)
139
+ """
140
+ try:
141
+ if not path.exists():
142
+ # For new files, this is ok
143
+ return True, "Valid"
144
+
145
+ size = path.stat().st_size
146
+ if size > ConfigService.MAX_FILE_SIZE:
147
+ return False, f"File too large: {size / (1024*1024):.1f}MB (max {ConfigService.MAX_FILE_SIZE / (1024*1024):.1f}MB)"
148
+
149
+ return True, "Valid"
150
+ except (OSError, IOError) as e:
151
+ return False, f"Cannot check file size: {e}"
152
+
153
+ @staticmethod
154
+ def compute_content_hash(content: str) -> str:
155
+ """
156
+ Compute SHA-256 hash of content for conflict detection.
157
+
158
+ Returns: Hex digest of the hash.
159
+ """
160
+ return hashlib.sha256(content.encode('utf-8')).hexdigest()
161
+
162
+ @staticmethod
163
+ def check_conflict(path: Path, original_hash: str) -> Tuple[bool, str]:
164
+ """
165
+ Check if file has been modified since it was loaded (conflict detection).
166
+
167
+ Uses SHA-256 content hash comparison instead of timestamps to avoid TOCTOU race conditions.
168
+
169
+ Args:
170
+ path: Path to the file
171
+ original_hash: SHA-256 hash of the content when it was loaded
172
+
173
+ Returns: (has_conflict, message)
174
+ - has_conflict: True if file has changed, False if it matches original_hash
175
+ - message: Human-readable description
176
+ """
177
+ try:
178
+ if not path.exists():
179
+ return True, "File has been deleted"
180
+
181
+ with open(path, 'r', encoding='utf-8', errors='replace') as f:
182
+ current_content = f.read()
183
+
184
+ current_hash = ConfigService.compute_content_hash(current_content)
185
+
186
+ if current_hash != original_hash:
187
+ return True, "File has been modified by another process"
188
+
189
+ return False, "No conflict detected"
190
+
191
+ except Exception as e:
192
+ return True, f"Cannot check for conflicts: {e}"
193
+
194
+ @staticmethod
195
+ def create_backup_with_rotation(file_path: Path, max_backups: int = MAX_BACKUPS) -> Tuple[Optional[Path], str]:
196
+ """
197
+ Create a backup of a file with rotation (keep only last N backups).
198
+
199
+ Backups are stored in a .backups/ subdirectory next to the file.
200
+ Old backups are automatically deleted when exceeding max_backups.
201
+
202
+ Args:
203
+ file_path: Path to the file to backup
204
+ max_backups: Maximum number of backups to keep (default: 3)
205
+
206
+ Returns: (backup_path, message)
207
+ - backup_path: Path to the created backup, or None if backup failed
208
+ - message: Description of what happened
209
+ """
210
+ try:
211
+ if not file_path.exists():
212
+ return None, "File does not exist, no backup needed"
213
+
214
+ # Create backup directory
215
+ backup_dir = file_path.parent / ConfigService.BACKUP_DIR_NAME
216
+ backup_dir.mkdir(exist_ok=True)
217
+
218
+ # Generate backup filename with timestamp
219
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
220
+ backup_name = f"{file_path.name}.{timestamp}.backup"
221
+ backup_path = backup_dir / backup_name
222
+
223
+ # Copy file to backup
224
+ import shutil
225
+ shutil.copy2(file_path, backup_path)
226
+
227
+ # Clean up old backups
228
+ existing_backups = sorted(
229
+ backup_dir.glob(f"{file_path.name}.*.backup"),
230
+ key=lambda p: p.stat().st_mtime,
231
+ reverse=True # Newest first
232
+ )
233
+
234
+ # Remove excess backups
235
+ removed_count = 0
236
+ for old_backup in existing_backups[max_backups:]:
237
+ old_backup.unlink()
238
+ removed_count += 1
239
+
240
+ msg = f"Backup created: {backup_path.name}"
241
+ if removed_count > 0:
242
+ msg += f" (removed {removed_count} old backup(s))"
243
+
244
+ return backup_path, msg
245
+
246
+ except Exception as e:
247
+ return None, f"Failed to create backup: {e}"
248
+
249
+ @staticmethod
250
+ def atomic_write(file_path: Path, content: str, original_hash: str = None,
251
+ create_backup: bool = True) -> SaveResult:
252
+ """
253
+ Atomically write content to a file with backup and conflict detection.
254
+
255
+ Uses write-to-temp-then-rename pattern for atomic writes.
256
+ If the write fails, the temp file is preserved for recovery.
257
+
258
+ Args:
259
+ file_path: Path to write to
260
+ content: Content to write
261
+ original_hash: If provided, check for conflicts before writing (SHA-256 hash)
262
+ create_backup: Whether to create a backup before writing (default: True)
263
+
264
+ Returns: SaveResult with success status, message, and details
265
+ """
266
+ temp_file = None
267
+
268
+ try:
269
+ # Validate file size
270
+ content_bytes = content.encode('utf-8')
271
+ if len(content_bytes) > ConfigService.MAX_FILE_SIZE:
272
+ return SaveResult(
273
+ success=False,
274
+ message=f"Content too large: {len(content_bytes) / (1024*1024):.1f}MB",
275
+ error_details=f"Maximum file size is {ConfigService.MAX_FILE_SIZE / (1024*1024):.1f}MB"
276
+ )
277
+
278
+ # Check for conflicts if hash provided
279
+ if original_hash and file_path.exists():
280
+ has_conflict, conflict_msg = ConfigService.check_conflict(file_path, original_hash)
281
+ if has_conflict:
282
+ return SaveResult(
283
+ success=False,
284
+ message=f"Conflict detected: {conflict_msg}",
285
+ error_details="The file has been modified since you loaded it. Please reload and try again."
286
+ )
287
+
288
+ # Create backup if requested and file exists
289
+ backup_path = None
290
+ backup_msg = ""
291
+ if create_backup and file_path.exists():
292
+ backup_path, backup_msg = ConfigService.create_backup_with_rotation(file_path)
293
+ if backup_path is None:
294
+ # Backup failed but we can still try to write
295
+ backup_msg = "Warning: Backup creation failed, but continuing with write"
296
+
297
+ # Atomic write: write to temp file, then rename
298
+ temp_fd, temp_path = tempfile.mkstemp(
299
+ dir=file_path.parent,
300
+ prefix=f".tmp_{file_path.name}_",
301
+ suffix='.tmp'
302
+ )
303
+ temp_file = Path(temp_path)
304
+
305
+ try:
306
+ # Write content to temp file
307
+ with os.fdopen(temp_fd, 'w', encoding='utf-8') as f:
308
+ f.write(content)
309
+
310
+ # Ensure data is written to disk
311
+ os.fsync(temp_fd)
312
+
313
+ # Atomic rename (POSIX guarantees this is atomic)
314
+ temp_file.rename(file_path)
315
+
316
+ # Sync parent directory to ensure rename is persisted
317
+ dir_fd = os.open(file_path.parent, os.O_RDONLY | os.O_DIRECTORY)
318
+ try:
319
+ os.fsync(dir_fd)
320
+ finally:
321
+ os.close(dir_fd)
322
+
323
+ success_msg = "File saved successfully"
324
+ if backup_msg:
325
+ success_msg += f". {backup_msg}"
326
+
327
+ return SaveResult(
328
+ success=True,
329
+ message=success_msg,
330
+ backup_path=backup_path
331
+ )
332
+
333
+ except Exception as e:
334
+ # If rename failed, temp file still exists for recovery
335
+ return SaveResult(
336
+ success=False,
337
+ message=f"Failed to write file: {e}",
338
+ error_details=f"Temp file preserved at: {temp_file}" if temp_file.exists() else None
339
+ )
340
+
341
+ except Exception as e:
342
+ return SaveResult(
343
+ success=False,
344
+ message=f"Unexpected error during save: {e}",
345
+ error_details=str(e)
346
+ )
347
+
348
+ finally:
349
+ # Clean up temp file if it still exists (shouldn't happen on success)
350
+ if temp_file and temp_file.exists():
351
+ try:
352
+ temp_file.unlink()
353
+ except (OSError, PermissionError):
354
+ pass # Best effort cleanup
355
+
356
+ @staticmethod
357
+ def validate_request_content_length(content_length: Optional[str]) -> Tuple[bool, str]:
358
+ """
359
+ Validate Content-Length header to prevent DoS attacks.
360
+
361
+ Returns: (is_valid, message)
362
+ """
363
+ if content_length is None:
364
+ return False, "Missing Content-Length header"
365
+
366
+ try:
367
+ length = int(content_length)
368
+ if length < 0:
369
+ return False, "Invalid Content-Length (negative)"
370
+ if length > ConfigService.MAX_CONTENT_LENGTH:
371
+ return False, f"Content too large: {length} bytes (max {ConfigService.MAX_CONTENT_LENGTH})"
372
+ return True, "Valid"
373
+ except ValueError:
374
+ return False, "Invalid Content-Length header"
375
+
376
+ @staticmethod
377
+ def validate_csrf_token(token: str, referer: Optional[str], origin: Optional[str]) -> Tuple[bool, str]:
378
+ """
379
+ Validate CSRF protection token.
380
+
381
+ Checks:
382
+ - Token is non-empty
383
+ - Referer or Origin header is from localhost
384
+
385
+ Returns: (is_valid, message)
386
+ """
387
+ if not token or len(token) < 8:
388
+ return False, "Invalid or missing CSRF token"
389
+
390
+ # Check that request comes from localhost
391
+ allowed_hosts = ['localhost', '127.0.0.1', '[::1]']
392
+
393
+ referer_valid = False
394
+ if referer:
395
+ referer_lower = referer.lower()
396
+ if any(host in referer_lower for host in allowed_hosts):
397
+ referer_valid = True
398
+
399
+ origin_valid = False
400
+ if origin:
401
+ origin_lower = origin.lower()
402
+ if any(host in origin_lower for host in allowed_hosts):
403
+ origin_valid = True
404
+
405
+ if not (referer_valid or origin_valid):
406
+ return False, "Invalid origin - request must come from localhost"
407
+
408
+ return True, "Valid"
409
+
410
+ @staticmethod
411
+ def generate_csrf_token() -> str:
412
+ """Generate a new CSRF token."""
413
+ import secrets
414
+ return secrets.token_urlsafe(32)
415
+
416
+
417
+ # Backwards compatibility: extend FileUtils with ConfigService methods
418
+ FileUtils.validate_write_path = staticmethod(ConfigService.validate_write_path)
419
+ FileUtils.validate_file_type = staticmethod(ConfigService.validate_file_type)
420
+ FileUtils.atomic_write = staticmethod(ConfigService.atomic_write)
421
+ FileUtils.create_backup_with_rotation = staticmethod(ConfigService.create_backup_with_rotation)
422
+ FileUtils.check_conflict = staticmethod(ConfigService.check_conflict)
423
+ FileUtils.compute_content_hash = staticmethod(ConfigService.compute_content_hash)
424
+
425
+
426
+ if __name__ == '__main__':
427
+ # Quick tests
428
+ print(f"Project root: {PROJECT_ROOT}")
429
+
430
+ # Test validate_write_path
431
+ test_paths = [
432
+ 'engine/topologies/tilt/discovery/registry.star',
433
+ 'TILT_RESOURCE_DEFAULTS.star',
434
+ 'spec.master',
435
+ 'engine/spec.master',
436
+ '../outside', # Should fail
437
+ '/etc/passwd', # Should fail
438
+ 'some/random/file.star', # Should fail - not in allowed paths
439
+ 'README.md', # Should fail - wrong extension
440
+ ]
441
+
442
+ print("\n=== Testing validate_write_path ===")
443
+ for path in test_paths:
444
+ result = ConfigService.validate_write_path(path)
445
+ status = "✅ VALID" if result else "❌ INVALID"
446
+ print(f"{status}: {path}")
447
+
448
+ # Test validate_file_type
449
+ print("\n=== Testing validate_file_type ===")
450
+ test_files = [
451
+ 'engine/topologies/tilt.star',
452
+ 'spec.master',
453
+ 'README.md',
454
+ 'file.txt',
455
+ ]
456
+ for path in test_files:
457
+ is_valid, msg = ConfigService.validate_file_type(path)
458
+ status = "✅" if is_valid else "❌"
459
+ print(f"{status}: {path} - {msg}")
460
+
461
+ # Test hash computation
462
+ print("\n=== Testing compute_content_hash ===")
463
+ test_content = "Hello, World!"
464
+ hash1 = ConfigService.compute_content_hash(test_content)
465
+ hash2 = ConfigService.compute_content_hash(test_content)
466
+ print(f"Content: '{test_content}'")
467
+ print(f"Hash 1: {hash1[:16]}...")
468
+ print(f"Hash 2: {hash2[:16]}...")
469
+ print(f"Hashes match: {hash1 == hash2}")