cassetta 0.30.0__tar.gz

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 (275) hide show
  1. cassetta-0.30.0/.dockerignore +51 -0
  2. cassetta-0.30.0/.env.example +131 -0
  3. cassetta-0.30.0/.github/workflows/ci.yml +66 -0
  4. cassetta-0.30.0/.github/workflows/release.yml +85 -0
  5. cassetta-0.30.0/.github/workflows/smoke.yml +132 -0
  6. cassetta-0.30.0/.github/workflows/tag.yml +132 -0
  7. cassetta-0.30.0/.gitignore +41 -0
  8. cassetta-0.30.0/CHANGELOG.md +592 -0
  9. cassetta-0.30.0/CLA.md +125 -0
  10. cassetta-0.30.0/CONTRIBUTING.md +115 -0
  11. cassetta-0.30.0/Dockerfile +94 -0
  12. cassetta-0.30.0/LICENSE +105 -0
  13. cassetta-0.30.0/Makefile +80 -0
  14. cassetta-0.30.0/PKG-INFO +435 -0
  15. cassetta-0.30.0/README.md +384 -0
  16. cassetta-0.30.0/SECURITY.md +55 -0
  17. cassetta-0.30.0/docker-compose.yml +39 -0
  18. cassetta-0.30.0/docs/AGENT_SETUP.md +461 -0
  19. cassetta-0.30.0/docs/CLIENT_SETUP.md +605 -0
  20. cassetta-0.30.0/docs/CONFIG.md +201 -0
  21. cassetta-0.30.0/docs/GLOSSARY.md +51 -0
  22. cassetta-0.30.0/docs/LICENSE_FAQ.md +65 -0
  23. cassetta-0.30.0/docs/REST_API.md +289 -0
  24. cassetta-0.30.0/docs/adr/001-three-layer-architecture.md +53 -0
  25. cassetta-0.30.0/docs/adr/002-backendconfig-public-api.md +50 -0
  26. cassetta-0.30.0/docs/adr/003-protocol-self-identification.md +49 -0
  27. cassetta-0.30.0/docs/adr/004-configuration-as-a-layer-1-protocol.md +91 -0
  28. cassetta-0.30.0/pyproject.toml +211 -0
  29. cassetta-0.30.0/scripts/docker-entrypoint.sh +147 -0
  30. cassetta-0.30.0/scripts/smoke.sh +384 -0
  31. cassetta-0.30.0/scripts/sync-docs-version.py +184 -0
  32. cassetta-0.30.0/src/cassetta/__init__.py +16 -0
  33. cassetta-0.30.0/src/cassetta/app.py +392 -0
  34. cassetta-0.30.0/src/cassetta/auth/__init__.py +52 -0
  35. cassetta-0.30.0/src/cassetta/auth/dependencies.py +146 -0
  36. cassetta-0.30.0/src/cassetta/auth/jwt_hot_reload.py +264 -0
  37. cassetta-0.30.0/src/cassetta/auth/jwt_tokens.py +147 -0
  38. cassetta-0.30.0/src/cassetta/auth/manifest_validation.py +74 -0
  39. cassetta-0.30.0/src/cassetta/auth/models.py +28 -0
  40. cassetta-0.30.0/src/cassetta/auth/observability.py +81 -0
  41. cassetta-0.30.0/src/cassetta/backends/__init__.py +0 -0
  42. cassetta-0.30.0/src/cassetta/backends/filesystem/__init__.py +0 -0
  43. cassetta-0.30.0/src/cassetta/backends/filesystem/claim_storage.py +215 -0
  44. cassetta-0.30.0/src/cassetta/backends/filesystem/keystore.py +157 -0
  45. cassetta-0.30.0/src/cassetta/backends/filesystem/storage.py +399 -0
  46. cassetta-0.30.0/src/cassetta/capabilities.py +76 -0
  47. cassetta-0.30.0/src/cassetta/claims.py +73 -0
  48. cassetta-0.30.0/src/cassetta/cli/__init__.py +32 -0
  49. cassetta-0.30.0/src/cassetta/cli/capabilities.py +107 -0
  50. cassetta-0.30.0/src/cassetta/cli/download.py +191 -0
  51. cassetta-0.30.0/src/cassetta/cli/send.py +154 -0
  52. cassetta-0.30.0/src/cassetta/cli/upload.py +125 -0
  53. cassetta-0.30.0/src/cassetta/config.py +381 -0
  54. cassetta-0.30.0/src/cassetta/defaults/__init__.py +0 -0
  55. cassetta-0.30.0/src/cassetta/defaults/default_access.py +33 -0
  56. cassetta-0.30.0/src/cassetta/defaults/default_alias.py +40 -0
  57. cassetta-0.30.0/src/cassetta/defaults/default_identity.py +21 -0
  58. cassetta-0.30.0/src/cassetta/defaults/default_limits.py +149 -0
  59. cassetta-0.30.0/src/cassetta/defaults/default_metrics.py +40 -0
  60. cassetta-0.30.0/src/cassetta/defaults/default_transport.py +37 -0
  61. cassetta-0.30.0/src/cassetta/defaults/factory.py +74 -0
  62. cassetta-0.30.0/src/cassetta/dependencies.py +42 -0
  63. cassetta-0.30.0/src/cassetta/downloads.py +212 -0
  64. cassetta-0.30.0/src/cassetta/envelopes.py +173 -0
  65. cassetta-0.30.0/src/cassetta/gc.py +282 -0
  66. cassetta-0.30.0/src/cassetta/limits.py +100 -0
  67. cassetta-0.30.0/src/cassetta/mcp_auth.py +116 -0
  68. cassetta-0.30.0/src/cassetta/mcp_server.py +1451 -0
  69. cassetta-0.30.0/src/cassetta/middleware.py +99 -0
  70. cassetta-0.30.0/src/cassetta/mime.py +43 -0
  71. cassetta-0.30.0/src/cassetta/models.py +226 -0
  72. cassetta-0.30.0/src/cassetta/path_validation.py +36 -0
  73. cassetta-0.30.0/src/cassetta/protocols/__init__.py +0 -0
  74. cassetta-0.30.0/src/cassetta/protocols/access.py +54 -0
  75. cassetta-0.30.0/src/cassetta/protocols/alias.py +45 -0
  76. cassetta-0.30.0/src/cassetta/protocols/claim_storage.py +76 -0
  77. cassetta-0.30.0/src/cassetta/protocols/config.py +103 -0
  78. cassetta-0.30.0/src/cassetta/protocols/identity.py +34 -0
  79. cassetta-0.30.0/src/cassetta/protocols/keystore.py +41 -0
  80. cassetta-0.30.0/src/cassetta/protocols/limits.py +119 -0
  81. cassetta-0.30.0/src/cassetta/protocols/metrics.py +52 -0
  82. cassetta-0.30.0/src/cassetta/protocols/reference_transport.py +47 -0
  83. cassetta-0.30.0/src/cassetta/protocols/storage.py +75 -0
  84. cassetta-0.30.0/src/cassetta/py.typed +0 -0
  85. cassetta-0.30.0/src/cassetta/rate_limit/__init__.py +23 -0
  86. cassetta-0.30.0/src/cassetta/rate_limit/limiter.py +161 -0
  87. cassetta-0.30.0/src/cassetta/routes/__init__.py +0 -0
  88. cassetta-0.30.0/src/cassetta/routes/agents.py +378 -0
  89. cassetta-0.30.0/src/cassetta/routes/capabilities.py +42 -0
  90. cassetta-0.30.0/src/cassetta/routes/download.py +432 -0
  91. cassetta-0.30.0/src/cassetta/routes/files.py +650 -0
  92. cassetta-0.30.0/src/cassetta/routes/inbox.py +577 -0
  93. cassetta-0.30.0/src/cassetta/routes/keys.py +246 -0
  94. cassetta-0.30.0/src/cassetta/routes/upload.py +428 -0
  95. cassetta-0.30.0/src/cassetta/routes/uploads.py +107 -0
  96. cassetta-0.30.0/src/cassetta/send_init.py +329 -0
  97. cassetta-0.30.0/src/cassetta/streaming.py +68 -0
  98. cassetta-0.30.0/src/cassetta/structured_log.py +290 -0
  99. cassetta-0.30.0/tests/auth/__init__.py +0 -0
  100. cassetta-0.30.0/tests/auth/conftest.py +114 -0
  101. cassetta-0.30.0/tests/auth/test_dependencies.py +154 -0
  102. cassetta-0.30.0/tests/auth/test_jwt_aud_placeholder.py +39 -0
  103. cassetta-0.30.0/tests/auth/test_jwt_hot_reload.py +494 -0
  104. cassetta-0.30.0/tests/auth/test_observability.py +235 -0
  105. cassetta-0.30.0/tests/conftest.py +358 -0
  106. cassetta-0.30.0/tests/integration/bundle_layout/__init__.py +0 -0
  107. cassetta-0.30.0/tests/integration/bundle_layout/test_atomic_visibility.py +103 -0
  108. cassetta-0.30.0/tests/integration/bundle_layout/test_mime_inference_roundtrip.py +167 -0
  109. cassetta-0.30.0/tests/integration/bundle_layout/test_multicast_fresh_bundle_id.py +45 -0
  110. cassetta-0.30.0/tests/integration/bundle_layout/test_rest_roundtrip.py +256 -0
  111. cassetta-0.30.0/tests/integration/conftest.py +349 -0
  112. cassetta-0.30.0/tests/integration/test_capabilities_mcp.py +328 -0
  113. cassetta-0.30.0/tests/integration/test_capabilities_mcp_rest_parity.py +75 -0
  114. cassetta-0.30.0/tests/integration/test_capabilities_no_side_effects.py +102 -0
  115. cassetta-0.30.0/tests/integration/test_capabilities_rest.py +90 -0
  116. cassetta-0.30.0/tests/integration/test_claim_expiry.py +123 -0
  117. cassetta-0.30.0/tests/integration/test_cli_capabilities.py +139 -0
  118. cassetta-0.30.0/tests/integration/test_cli_download.py +224 -0
  119. cassetta-0.30.0/tests/integration/test_cli_send_contract.py +171 -0
  120. cassetta-0.30.0/tests/integration/test_cli_upload_contract.py +170 -0
  121. cassetta-0.30.0/tests/integration/test_client_precheck_pattern.py +138 -0
  122. cassetta-0.30.0/tests/integration/test_concurrent_pick.py +90 -0
  123. cassetta-0.30.0/tests/integration/test_disk_pressure_1gb.py +301 -0
  124. cassetta-0.30.0/tests/integration/test_download_endpoint_matrix.py +491 -0
  125. cassetta-0.30.0/tests/integration/test_download_identity_mismatch.py +132 -0
  126. cassetta-0.30.0/tests/integration/test_get_inbox_rest_reference.py +74 -0
  127. cassetta-0.30.0/tests/integration/test_get_store_reference.py +88 -0
  128. cassetta-0.30.0/tests/integration/test_inbox_listing_hides_claimed.py +125 -0
  129. cassetta-0.30.0/tests/integration/test_jwt_rotation.py +64 -0
  130. cassetta-0.30.0/tests/integration/test_legacy_send_removed.py +79 -0
  131. cassetta-0.30.0/tests/integration/test_manifest_pre_bytes_rejection.py +278 -0
  132. cassetta-0.30.0/tests/integration/test_mcp_description_hygiene.py +63 -0
  133. cassetta-0.30.0/tests/integration/test_mcp_send_init_contract.py +154 -0
  134. cassetta-0.30.0/tests/integration/test_mcp_send_inline_contract.py +313 -0
  135. cassetta-0.30.0/tests/integration/test_mcp_server_version.py +39 -0
  136. cassetta-0.30.0/tests/integration/test_observability_contract.py +509 -0
  137. cassetta-0.30.0/tests/integration/test_pick_inline.py +143 -0
  138. cassetta-0.30.0/tests/integration/test_pick_reference.py +128 -0
  139. cassetta-0.30.0/tests/integration/test_read_surface_migration.py +140 -0
  140. cassetta-0.30.0/tests/integration/test_reaper_runs_in_lifespan.py +34 -0
  141. cassetta-0.30.0/tests/integration/test_reaper_sweeps_orphans.py +46 -0
  142. cassetta-0.30.0/tests/integration/test_rest_upload_contract.py +132 -0
  143. cassetta-0.30.0/tests/integration/test_rest_upload_init.py +243 -0
  144. cassetta-0.30.0/tests/integration/test_scenario_01_full_lifecycle.py +83 -0
  145. cassetta-0.30.0/tests/integration/test_scenario_02_two_agents.py +54 -0
  146. cassetta-0.30.0/tests/integration/test_scenario_03_discovery_broadcast.py +61 -0
  147. cassetta-0.30.0/tests/integration/test_scenario_04_multi_file_bundle.py +95 -0
  148. cassetta-0.30.0/tests/integration/test_scenario_05_send_validation.py +37 -0
  149. cassetta-0.30.0/tests/integration/test_scenario_06_mcp_tools.py +90 -0
  150. cassetta-0.30.0/tests/integration/test_scenario_07_key_rotation.py +63 -0
  151. cassetta-0.30.0/tests/integration/test_scenario_08_ttl_expiry.py +50 -0
  152. cassetta-0.30.0/tests/integration/test_send_init_batch.py +148 -0
  153. cassetta-0.30.0/tests/integration/test_send_init_inline.py +121 -0
  154. cassetta-0.30.0/tests/integration/test_send_roundtrip.py +141 -0
  155. cassetta-0.30.0/tests/integration/test_server_restart_claim_survival.py +138 -0
  156. cassetta-0.30.0/tests/integration/test_single_use_passive.py +197 -0
  157. cassetta-0.30.0/tests/integration/test_stateless_across_restart.py +141 -0
  158. cassetta-0.30.0/tests/integration/test_upload_rollback.py +140 -0
  159. cassetta-0.30.0/tests/observability/_obs_helpers.py +103 -0
  160. cassetta-0.30.0/tests/observability/conftest.py +179 -0
  161. cassetta-0.30.0/tests/observability/test_active_keys_rotate.py +38 -0
  162. cassetta-0.30.0/tests/observability/test_audit_no_raw_metric_calls.py +56 -0
  163. cassetta-0.30.0/tests/observability/test_auth_logger_isolation.py +173 -0
  164. cassetta-0.30.0/tests/observability/test_capabilities_metric.py +63 -0
  165. cassetta-0.30.0/tests/observability/test_download_metrics.py +258 -0
  166. cassetta-0.30.0/tests/observability/test_inbox_metrics.py +203 -0
  167. cassetta-0.30.0/tests/observability/test_mcp_enforce_metric.py +142 -0
  168. cassetta-0.30.0/tests/observability/test_policy_kind_tag.py +155 -0
  169. cassetta-0.30.0/tests/observability/test_safe_emit.py +249 -0
  170. cassetta-0.30.0/tests/observability/test_upload_metrics.py +199 -0
  171. cassetta-0.30.0/tests/rate_limit/__init__.py +0 -0
  172. cassetta-0.30.0/tests/rate_limit/conftest.py +66 -0
  173. cassetta-0.30.0/tests/rate_limit/test_broadcast_mcp.py +203 -0
  174. cassetta-0.30.0/tests/rate_limit/test_broadcast_rest.py +201 -0
  175. cassetta-0.30.0/tests/rate_limit/test_limiter_instance.py +104 -0
  176. cassetta-0.30.0/tests/rate_limit/test_shared_budget.py +183 -0
  177. cassetta-0.30.0/tests/rate_limit/test_trusted_proxies.py +119 -0
  178. cassetta-0.30.0/tests/test_access.py +65 -0
  179. cassetta-0.30.0/tests/test_access_denial.py +146 -0
  180. cassetta-0.30.0/tests/test_access_wiring.py +463 -0
  181. cassetta-0.30.0/tests/test_agents.py +341 -0
  182. cassetta-0.30.0/tests/test_alias_protocol.py +48 -0
  183. cassetta-0.30.0/tests/test_app_has_no_concrete_default_imports.py +74 -0
  184. cassetta-0.30.0/tests/test_auth.py +114 -0
  185. cassetta-0.30.0/tests/test_broadcast.py +661 -0
  186. cassetta-0.30.0/tests/test_broadcast_url.py +113 -0
  187. cassetta-0.30.0/tests/test_bundle_listing.py +147 -0
  188. cassetta-0.30.0/tests/test_bundle_rest.py +232 -0
  189. cassetta-0.30.0/tests/test_bundle_writer.py +176 -0
  190. cassetta-0.30.0/tests/test_capabilities_features_policy.py +49 -0
  191. cassetta-0.30.0/tests/test_client_install.py +243 -0
  192. cassetta-0.30.0/tests/test_concurrent_access.py +65 -0
  193. cassetta-0.30.0/tests/test_config.py +267 -0
  194. cassetta-0.30.0/tests/test_config_dead_envvars.py +68 -0
  195. cassetta-0.30.0/tests/test_config_protocol.py +255 -0
  196. cassetta-0.30.0/tests/test_cross_protocol.py +236 -0
  197. cassetta-0.30.0/tests/test_declared_dependencies.py +226 -0
  198. cassetta-0.30.0/tests/test_default_access.py +47 -0
  199. cassetta-0.30.0/tests/test_default_alias.py +94 -0
  200. cassetta-0.30.0/tests/test_default_config.py +13 -0
  201. cassetta-0.30.0/tests/test_dev_mode_observability.py +203 -0
  202. cassetta-0.30.0/tests/test_docs_examples.py +996 -0
  203. cassetta-0.30.0/tests/test_docs_version_pins.py +250 -0
  204. cassetta-0.30.0/tests/test_download_auth_failure.py +200 -0
  205. cassetta-0.30.0/tests/test_files.py +165 -0
  206. cassetta-0.30.0/tests/test_health.py +9 -0
  207. cassetta-0.30.0/tests/test_identity.py +136 -0
  208. cassetta-0.30.0/tests/test_inbox.py +416 -0
  209. cassetta-0.30.0/tests/test_inbox_mcp.py +236 -0
  210. cassetta-0.30.0/tests/test_key_creation.py +129 -0
  211. cassetta-0.30.0/tests/test_keys.py +212 -0
  212. cassetta-0.30.0/tests/test_keys_protocol_no_typeerror.py +50 -0
  213. cassetta-0.30.0/tests/test_layer2_uses_protocol_annotations.py +129 -0
  214. cassetta-0.30.0/tests/test_lease.py +81 -0
  215. cassetta-0.30.0/tests/test_limits_wire.py +357 -0
  216. cassetta-0.30.0/tests/test_listing_reads_only_meta.py +83 -0
  217. cassetta-0.30.0/tests/test_logging.py +456 -0
  218. cassetta-0.30.0/tests/test_mcp_auth.py +245 -0
  219. cassetta-0.30.0/tests/test_mcp_bundles.py +365 -0
  220. cassetta-0.30.0/tests/test_mcp_identity.py +53 -0
  221. cassetta-0.30.0/tests/test_mcp_tools.py +610 -0
  222. cassetta-0.30.0/tests/test_metrics.py +351 -0
  223. cassetta-0.30.0/tests/test_mime_inference.py +69 -0
  224. cassetta-0.30.0/tests/test_models_inventory.py +88 -0
  225. cassetta-0.30.0/tests/test_namespace_isolation.py +76 -0
  226. cassetta-0.30.0/tests/test_no_cloud_imports_in_core.py +30 -0
  227. cassetta-0.30.0/tests/test_openapi_leakage.py +121 -0
  228. cassetta-0.30.0/tests/test_openapi_version.py +40 -0
  229. cassetta-0.30.0/tests/test_path_validation.py +63 -0
  230. cassetta-0.30.0/tests/test_peek_mcp.py +202 -0
  231. cassetta-0.30.0/tests/test_peek_rest.py +177 -0
  232. cassetta-0.30.0/tests/test_protocols_declare_kind.py +98 -0
  233. cassetta-0.30.0/tests/test_public_surface.py +1111 -0
  234. cassetta-0.30.0/tests/test_rate_limit_route_attribution.py +154 -0
  235. cassetta-0.30.0/tests/test_release_metadata.py +518 -0
  236. cassetta-0.30.0/tests/test_request_id.py +107 -0
  237. cassetta-0.30.0/tests/test_send_init_parity.py +108 -0
  238. cassetta-0.30.0/tests/test_send_validation.py +142 -0
  239. cassetta-0.30.0/tests/test_send_with_policy.py +241 -0
  240. cassetta-0.30.0/tests/test_sender_label.py +59 -0
  241. cassetta-0.30.0/tests/test_signing_key_fixtures.py +88 -0
  242. cassetta-0.30.0/tests/test_smoke_script.py +157 -0
  243. cassetta-0.30.0/tests/test_startup_log_claim_backend.py +57 -0
  244. cassetta-0.30.0/tests/test_storage.py +96 -0
  245. cassetta-0.30.0/tests/test_store_path_collision.py +81 -0
  246. cassetta-0.30.0/tests/test_ttl.py +122 -0
  247. cassetta-0.30.0/tests/test_ttl_cleanup.py +160 -0
  248. cassetta-0.30.0/tests/test_unified_identity.py +140 -0
  249. cassetta-0.30.0/tests/test_unlocked_server_resolution.py +92 -0
  250. cassetta-0.30.0/tests/test_upload_per_file_cap.py +192 -0
  251. cassetta-0.30.0/tests/test_workflows.py +525 -0
  252. cassetta-0.30.0/tests/unit/test_app_wiring_limits.py +142 -0
  253. cassetta-0.30.0/tests/unit/test_boot_fails_without_keys.py +68 -0
  254. cassetta-0.30.0/tests/unit/test_capabilities_assembly.py +238 -0
  255. cassetta-0.30.0/tests/unit/test_claim_sidecar.py +308 -0
  256. cassetta-0.30.0/tests/unit/test_claim_storage_protocol_conformance.py +13 -0
  257. cassetta-0.30.0/tests/unit/test_config_loading.py +190 -0
  258. cassetta-0.30.0/tests/unit/test_gc_claims.py +210 -0
  259. cassetta-0.30.0/tests/unit/test_gc_reaper.py +133 -0
  260. cassetta-0.30.0/tests/unit/test_jwt_revocation_hook.py +71 -0
  261. cassetta-0.30.0/tests/unit/test_jwt_tokens.py +153 -0
  262. cassetta-0.30.0/tests/unit/test_key_source_precedence.py +69 -0
  263. cassetta-0.30.0/tests/unit/test_limits_config.py +179 -0
  264. cassetta-0.30.0/tests/unit/test_limits_policy_evaluate.py +173 -0
  265. cassetta-0.30.0/tests/unit/test_limits_policy_introspection.py +334 -0
  266. cassetta-0.30.0/tests/unit/test_limits_policy_skeleton.py +23 -0
  267. cassetta-0.30.0/tests/unit/test_limits_protocol_shape.py +71 -0
  268. cassetta-0.30.0/tests/unit/test_limits_shared_helper.py +276 -0
  269. cassetta-0.30.0/tests/unit/test_manifest_validation.py +76 -0
  270. cassetta-0.30.0/tests/unit/test_no_jti_store.py +42 -0
  271. cassetta-0.30.0/tests/unit/test_peek_never_reads_bytes.py +103 -0
  272. cassetta-0.30.0/tests/unit/test_peek_unit.py +264 -0
  273. cassetta-0.30.0/tests/unit/test_reference_transport.py +77 -0
  274. cassetta-0.30.0/tests/unit/test_streaming_sync_reader.py +107 -0
  275. cassetta-0.30.0/uv.lock +1325 -0
@@ -0,0 +1,51 @@
1
+ # Build context = the repository root. Only `pyproject.toml`, `uv.lock`, `LICENSE`, `src/` and
2
+ # `scripts/docker-entrypoint.sh` are actually COPYed by the Dockerfile, but keeping the context
3
+ # small speeds every build and keeps a stray `.env` from ever reaching the daemon.
4
+ #
5
+ # `scripts/` is therefore not excluded below, and must not become excluded: the entrypoint is the
6
+ # container's first process.
7
+
8
+ # Version control
9
+ .git/
10
+ .gitignore
11
+ .github/
12
+
13
+ # Python build + cache artifacts
14
+ __pycache__/
15
+ *.py[cod]
16
+ *.egg-info/
17
+ dist/
18
+ build/
19
+ .venv/
20
+ venv/
21
+
22
+ # Test, lint, and type-check state
23
+ tests/
24
+ .pytest_cache/
25
+ .mypy_cache/
26
+ .ruff_cache/
27
+ htmlcov/
28
+ .coverage
29
+
30
+ # Docs — not needed to run the service
31
+ docs/
32
+
33
+ # Runtime state: never bake a developer's bus contents into an image
34
+ data/
35
+ data.keys/
36
+
37
+ # Secrets
38
+ .env
39
+ .env.local
40
+
41
+ # The docker files themselves
42
+ Dockerfile
43
+ docker-compose.yml
44
+ .dockerignore
45
+
46
+ # Editor and OS noise
47
+ .idea/
48
+ .vscode/
49
+ *.swp
50
+ .DS_Store
51
+ Thumbs.db
@@ -0,0 +1,131 @@
1
+ # Cassetta configuration — copy to `.env` and edit.
2
+ #
3
+ # cp .env.example .env
4
+ #
5
+ # Every value below is read from the environment at startup. Three are required; the rest show
6
+ # their built-in default, so a line you leave commented out behaves exactly as shown.
7
+ # Full reference: docs/CONFIG.md
8
+ #
9
+ # To change a value later, edit `.env` and run `docker compose up -d` again — no image rebuild.
10
+
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Required
14
+ # ---------------------------------------------------------------------------
15
+
16
+ # Token for the one-time POST /setup call that mints your first API key.
17
+ # Set this to a long random string before exposing the server to anything.
18
+ # Setting it to "" enables dev mode — NO AUTHENTICATION AT ALL. Local experiments only.
19
+ CASSETTA_SETUP_TOKEN=change-me-to-a-long-random-string
20
+
21
+ # Absolute base URL that agents can reach. Used to compose the download / upload URLs
22
+ # returned in batch responses, so it must be the externally reachable address — not the
23
+ # internal bind address, when those differ. Scheme is mandatory (http:// or https://).
24
+ CASSETTA_PUBLIC_BASE_URL=http://localhost:16001
25
+
26
+ # ###########################################################################
27
+ # # DEV ONLY — the key below is a fixed placeholder shipped with this file. #
28
+ # # It decodes to the literal "dev-only-quickstart-key-DO-NOT-USE-IN-PRODUCTION",
29
+ # # so anyone who cloned this repository can mint a valid token against it. #
30
+ # # REPLACE IT before any deployment beyond localhost: #
31
+ # # openssl rand -base64 32 #
32
+ # ###########################################################################
33
+ # Base64-encoded HS256 signing key; must decode to at least 32 bytes.
34
+ CASSETTA_JWT_KEY=ZGV2LW9ubHktcXVpY2tzdGFydC1rZXktRE8tTk9ULVVTRS1JTi1QUk9EVUNUSU9O
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Signing keys (optional)
39
+ # ---------------------------------------------------------------------------
40
+
41
+ # Path to a file whose contents are the base64-encoded primary key. Wins over
42
+ # CASSETTA_JWT_KEY when both are set — the managed-secret setup.
43
+ #CASSETTA_JWT_KEY_FILE=
44
+
45
+ # Verify-only secondary key, used during rotation. Never signs. See docs/CONFIG.md
46
+ # "Key rotation procedure".
47
+ #CASSETTA_JWT_KEY_SECONDARY=
48
+ #CASSETTA_JWT_KEY_SECONDARY_FILE=
49
+
50
+ # Seconds a just-demoted key stays valid after a SIGHUP rotation, so in-flight tokens
51
+ # keep verifying. Must be >= max(CASSETTA_DOWNLOAD_CLAIM_TTL, CASSETTA_UPLOAD_TOKEN_TTL).
52
+ #CASSETTA_JWT_KEY_OVERLAP_TTL=600
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Storage
57
+ # ---------------------------------------------------------------------------
58
+
59
+ # Storage path. /data is the container mount; outside a container the default is ./data.
60
+ CASSETTA_STORAGE_PATH=/data
61
+
62
+ # API key store. Defaults to a sibling of the storage directory —
63
+ # "<storage>.keys/.cassetta-keys.json" — so key files live outside the storage tree and
64
+ # never appear in a /files/ listing.
65
+ #CASSETTA_KEYS_FILE=
66
+
67
+ # Default file TTL in seconds. 0 disables expiration.
68
+ #CASSETTA_DEFAULT_TTL=0
69
+
70
+ # Character class allowed in stored paths.
71
+ #CASSETTA_ALLOWED_PATH_CHARS=a-zA-Z0-9\-_./
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Limits policy
76
+ # ---------------------------------------------------------------------------
77
+ # For the four caps, an empty value means "no cap" — explicitly, so configuration
78
+ # management that cannot distinguish an absent variable from an empty one can still clear one.
79
+
80
+ # Maximum bytes for any single file in a bundle. Empty = uncapped.
81
+ #CASSETTA_PER_FILE_MAX=
82
+
83
+ # Maximum total bytes across a bundle. Empty = uncapped.
84
+ #CASSETTA_PER_BUNDLE_TOTAL_MAX=
85
+
86
+ # Maximum number of files per bundle. Empty = unlimited.
87
+ #CASSETTA_PER_BUNDLE_FILE_COUNT_MAX=25
88
+
89
+ # Payloads at or below this size are accepted inline; larger ones require the batch
90
+ # upload flow. 102400 = 100 KiB. Empty = always inline.
91
+ #CASSETTA_MAX_INLINE_SIZE=102400
92
+
93
+ # TTLs, in seconds. All must be > 0.
94
+ #CASSETTA_UPLOAD_TOKEN_TTL=300
95
+ #CASSETTA_DOWNLOAD_CLAIM_TTL=300
96
+
97
+ # Passive garbage collection of orphaned bundles.
98
+ #CASSETTA_PASSIVE_GC_MIN_AGE=3600
99
+ #CASSETTA_PASSIVE_GC_INTERVAL=600
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Rate limiting and fan-out
104
+ # ---------------------------------------------------------------------------
105
+
106
+ # Shared budget across REST /broadcast and the MCP cassetta_broadcast tool.
107
+ # Format "<int>/<unit>", unit one of sec, second, min, minute, hour, hourly.
108
+ #CASSETTA_RATE_LIMIT_BROADCAST=10/minute
109
+
110
+ # Fan-out cap. A broadcast above this many recipients is rejected before any write.
111
+ #CASSETTA_BROADCAST_MAX_TARGETS=1000
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # MCP and logging
116
+ # ---------------------------------------------------------------------------
117
+
118
+ # Comma-separated Host header allowlist for the /mcp endpoint (DNS-rebinding protection).
119
+ # Empty means localhost only — set your external hostname(s) to reach MCP from another machine.
120
+ #CASSETTA_MCP_ALLOWED_HOSTS=
121
+
122
+ # "text" or "json".
123
+ #CASSETTA_LOG_FORMAT=text
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Compose only
128
+ # ---------------------------------------------------------------------------
129
+ # Read by docker-compose.yml, never by the server. It selects the host port to publish;
130
+ # the port inside the container is fixed at 16001.
131
+ #CASSETTA_PORT=16001
@@ -0,0 +1,66 @@
1
+ # Continuous integration for every pull request, and for the default branch itself.
2
+ #
3
+ # This workflow carries exactly the gates a contributor already runs locally, and nothing more.
4
+ # It is meant to be cheap enough to require on every merge, so anything that builds an image or
5
+ # starts a stack belongs in the weekly smoke workflow instead — never here.
6
+
7
+ name: CI
8
+
9
+ on:
10
+ pull_request:
11
+ branches: [main]
12
+ # So the default branch carries its own status rather than inheriting the last pull request's.
13
+ push:
14
+ branches: [main]
15
+
16
+ # The run reads this repository and writes nothing back. It needs no secret, no registry and no
17
+ # token beyond the read-only one below.
18
+ permissions:
19
+ contents: read
20
+
21
+ # A force-pushed branch should not leave superseded runs burning minutes on a status nobody
22
+ # will read.
23
+ concurrency:
24
+ group: ci-${{ github.ref }}
25
+ cancel-in-progress: true
26
+
27
+ jobs:
28
+ lint:
29
+ name: Format, lint and types
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v6
33
+
34
+ - uses: astral-sh/setup-uv@v7
35
+ with:
36
+ python-version: "3.13"
37
+
38
+ # `--extra dev` is load-bearing. The gate tools live in [project.optional-dependencies],
39
+ # which uv treats as an extra rather than as a default dependency group, so a bare
40
+ # `uv sync` uninstalls pytest, ruff and mypy and every step below then fails.
41
+ - run: uv sync --extra dev
42
+
43
+ # One step per gate, so a failure is named in the run's step list rather than buried.
44
+ - run: uv run ruff format --check
45
+ - run: uv run ruff check
46
+ - run: uv run mypy
47
+
48
+ test:
49
+ name: Test suite
50
+ runs-on: ubuntu-latest
51
+ steps:
52
+ # Default fetch depth is correct: the one test that shells out to git reads `git ls-files`,
53
+ # which is the index and needs no history.
54
+ - uses: actions/checkout@v6
55
+
56
+ - uses: astral-sh/setup-uv@v7
57
+ with:
58
+ python-version: "3.13"
59
+
60
+ - run: uv sync --extra dev
61
+
62
+ - run: uv run pytest
63
+
64
+ # Every test must also pass under a randomised ordering — interdependence between tests is
65
+ # a defect here, not a quirk.
66
+ - run: uv run pytest -p randomly
@@ -0,0 +1,85 @@
1
+ # Build the distribution for a release, and prove it is the release it claims to be.
2
+ #
3
+ # Reusable on purpose, and reusable *only*. What defines a release here is the tag, and the tag is
4
+ # placed by tag.yml after a merge — so this workflow is called from there, in the same run, with the
5
+ # version that was just tagged. There is nothing this could usefully mean on its own, which is why it
6
+ # carries no other trigger.
7
+ #
8
+ # It builds and checks; it does not publish, and it cannot. A called workflow can narrow the token
9
+ # its caller grants but never widen it, so a job here could not obtain the identity token publishing
10
+ # needs, and the index does not support a reusable workflow as a trusted publisher in any case. The
11
+ # publish job is in tag.yml, after the call to this one, and it hands over the artifact uploaded
12
+ # below. tests/test_workflows.py holds publishing to that one place.
13
+
14
+ name: Release
15
+
16
+ on:
17
+ workflow_call:
18
+ inputs:
19
+ version:
20
+ description: The version that was just tagged, without the leading `v`.
21
+ required: true
22
+ type: string
23
+
24
+ # It reads the tree and writes nothing back — the tag already exists by the time this runs.
25
+ permissions:
26
+ contents: read
27
+
28
+ jobs:
29
+ build:
30
+ name: Build the distribution and check it against the tag
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v6
34
+
35
+ - uses: astral-sh/setup-uv@v7
36
+ with:
37
+ python-version: "3.13"
38
+
39
+ # No `--extra dev` here: a build needs the build backend, not the gate tools. CI has already
40
+ # run those against this very commit.
41
+ - run: uv build
42
+
43
+ # The step this whole composition exists for. The version in the built artifact is derived
44
+ # from src/cassetta/__init__.py by the build backend; the version in the tag was read from the
45
+ # same file by tag.yml. Asserting they match is what turns "they are both read from one place"
46
+ # from an argument into an observation.
47
+ #
48
+ # The input reaches the script through the environment rather than being interpolated into the
49
+ # script body. The value comes from this repository's own tree, so nothing hostile can reach
50
+ # it — but a `run:` block with an expression spliced into it is a shape a reviewer has to stop
51
+ # and think about, and this one gets exactly one review.
52
+ - name: The built artifact must carry the version that was tagged
53
+ env:
54
+ EXPECTED: ${{ inputs.version }}
55
+ run: |
56
+ set -euo pipefail
57
+
58
+ wheel="dist/cassetta-${EXPECTED}-py3-none-any.whl"
59
+ sdist="dist/cassetta-${EXPECTED}.tar.gz"
60
+
61
+ missing=false
62
+ for artifact in "$wheel" "$sdist"; do
63
+ if [ ! -f "$artifact" ]; then
64
+ printf 'expected %s, which was not built\n' "$artifact" >&2
65
+ missing=true
66
+ fi
67
+ done
68
+
69
+ if [ "$missing" = true ]; then
70
+ printf '\nthe tag says %s; the build produced:\n' "$EXPECTED" >&2
71
+ ls -1 dist/ >&2
72
+ printf '\nThe tag and the distribution disagree about what this release is.\n' >&2
73
+ exit 1
74
+ fi
75
+
76
+ printf 'dist/ carries %s, which is what was tagged\n' "$EXPECTED"
77
+
78
+ # What tag.yml's publish job hands to the index, once a reviewer approves the run — these bytes,
79
+ # which the step above checked against the tag, and never a second build. It downloads them by
80
+ # this name, so the two are renamed together; tests/test_workflows.py compares them.
81
+ - uses: actions/upload-artifact@v7
82
+ with:
83
+ name: cassetta-${{ inputs.version }}
84
+ path: dist/
85
+ if-no-files-found: error
@@ -0,0 +1,132 @@
1
+ # Weekly proof that the container this repository ships still boots and serves traffic.
2
+ #
3
+ # Deliberately off the merge path. Building an image and starting a stack is far too expensive to
4
+ # run on every pull request; the change flow here is low and history is linear and squash-merged,
5
+ # so a weekly signal is enough to identify which recent merge broke it.
6
+ #
7
+ # The shell lives in scripts/smoke.sh rather than in this file, so a contributor can run exactly
8
+ # what the schedule runs against their own clone.
9
+ #
10
+ # It runs twice, against bind mounts owned two different ways. The second run is the only thing in
11
+ # this repository that exercises the case a hosted runner's own user id conceals; see the comment on
12
+ # that job for why one run was never enough.
13
+
14
+ name: Smoke
15
+
16
+ on:
17
+ schedule:
18
+ # Mondays, off the hour on purpose: scheduled runs at minute zero are heavily contended on
19
+ # the platform and get delayed, sometimes substantially.
20
+ - cron: "17 6 * * 1"
21
+ # So the first proof that any of this works does not have to wait up to a week.
22
+ workflow_dispatch:
23
+
24
+ permissions:
25
+ contents: read
26
+
27
+ concurrency:
28
+ group: smoke-${{ github.ref }}
29
+ cancel-in-progress: false
30
+
31
+ jobs:
32
+ changes:
33
+ name: Anything landed since the last green run?
34
+ runs-on: ubuntu-latest
35
+ permissions:
36
+ contents: read
37
+ # For the Actions API query below.
38
+ actions: read
39
+ outputs:
40
+ should_run: ${{ steps.decide.outputs.should_run }}
41
+ steps:
42
+ - uses: actions/checkout@v6
43
+ with:
44
+ # The commit count below needs history to count over.
45
+ fetch-depth: 0
46
+
47
+ - id: decide
48
+ env:
49
+ GH_TOKEN: ${{ github.token }}
50
+ run: |
51
+ set -euo pipefail
52
+
53
+ decide() {
54
+ printf 'should_run=%s — %s\n' "$1" "$2"
55
+ echo "should_run=$1" >>"$GITHUB_OUTPUT"
56
+ }
57
+
58
+ # Key on the last *successful* run rather than on a calendar window, so a run that
59
+ # fails and is fixed ten days later still measures from the right place. The run
60
+ # executing this query cannot match it: it is in_progress, not success.
61
+ query="repos/${GITHUB_REPOSITORY}/actions/workflows/smoke.yml/runs?status=success&branch=main&per_page=1"
62
+
63
+ if baseline="$(gh api "$query" --jq '.workflow_runs[0].head_sha // ""')"; then
64
+ api_reachable=true
65
+ else
66
+ api_reachable=false
67
+ baseline=""
68
+ fi
69
+
70
+ # Every unknown below resolves to "run". A wrong "run" costs one unnecessary run; a
71
+ # wrong "skip" is silent and can keep skipping forever, which is the failure nobody
72
+ # notices.
73
+ if [ "$api_reachable" = false ]; then
74
+ decide true "the Actions API could not be queried; running rather than skipping blind"
75
+ elif [ -z "$baseline" ]; then
76
+ decide true "no successful run of this workflow exists yet"
77
+ elif ! git cat-file -e "${baseline}^{commit}" 2>/dev/null; then
78
+ decide true "baseline ${baseline} is not in the fetched history (rewritten, or older than the checkout)"
79
+ else
80
+ landed="$(git rev-list --count "${baseline}..HEAD")"
81
+ if [ "$landed" -gt 0 ]; then
82
+ decide true "${landed} commit(s) landed since ${baseline}"
83
+ else
84
+ decide false "nothing has landed since ${baseline}"
85
+ fi
86
+ fi
87
+
88
+ smoke:
89
+ name: Container smoke
90
+ needs: changes
91
+ if: needs.changes.outputs.should_run == 'true'
92
+ runs-on: ubuntu-latest
93
+ steps:
94
+ - uses: actions/checkout@v6
95
+
96
+ # One step, on purpose. Dumping logs on failure and tearing the stack down live in the
97
+ # script's own exit trap, so a contributor running it locally gets exactly this behaviour.
98
+ - run: ./scripts/smoke.sh
99
+
100
+ smoke-foreign-uid:
101
+ name: Container smoke, bind mounts owned by another uid
102
+ needs: changes
103
+ if: needs.changes.outputs.should_run == 'true'
104
+ runs-on: ubuntu-latest
105
+ steps:
106
+ - uses: actions/checkout@v6
107
+
108
+ # The case the job above is structurally unable to see, and the reason this one is permanent.
109
+ #
110
+ # A hosted runner's own user carries uid 1001 — the same id as the account inside the image —
111
+ # so a bind mount the runner creates is already owned by exactly the right user. The job above
112
+ # therefore passes for a reason that has nothing to do with the code, and would keep passing
113
+ # if the container lost the ability to write into a directory it did not create. Handing the
114
+ # two directories to a different uid first is what reproduces the case an ordinary Linux
115
+ # contributor meets: the quickstart's `mkdir -p` leaves them owned by whoever ran it.
116
+ #
117
+ # 1000 is the first ordinary user id on a Debian or Ubuntu desktop, so it stands in for
118
+ # precisely that contributor. Creating the directories here rather than letting the script do
119
+ # it is load-bearing — the script creates them itself when they are absent, and one it creates
120
+ # belongs to the runner again.
121
+ #
122
+ # Both ownership cases are kept because they fail for different reasons. Deleting either loses
123
+ # a signal, and deleting this one returns the weekly run to passing by coincidence.
124
+ - name: Hand the bind-mount sources to a uid that is not the runner's
125
+ run: |
126
+ set -euo pipefail
127
+ mkdir -p data data.keys
128
+ sudo chown 1000:1000 data data.keys
129
+ printf 'runner uid: %s (the container runs as uid 1001)\n' "$(id -u)"
130
+ ls -ld data data.keys
131
+
132
+ - run: ./scripts/smoke.sh
@@ -0,0 +1,132 @@
1
+ # The release tag, as a consequence of the merge rather than a step somebody remembers.
2
+ #
3
+ # This repository is squash-only. A tag made on the branch before the merge names a commit that is
4
+ # not on `main` — the branch commit and the squash commit can have the identical tree and the
5
+ # identical parent and still different hashes, because the squash rewrites the message and the
6
+ # committer. Reading the version *after* the merge and tagging the commit that landed is the only
7
+ # way to get this right by construction.
8
+ #
9
+ # It runs on every push to `main`, and on almost all of them it does nothing. That is the design: the
10
+ # common case must be a quiet, green no-op, because a mechanism that costs something on the ordinary
11
+ # path is a mechanism somebody eventually deletes.
12
+ #
13
+ # The release workflow is *called*, never triggered. A tag pushed with GITHUB_TOKEN does not start
14
+ # other workflow runs — GitHub's loop prevention — so a release workflow listening on `push: tags:`
15
+ # would never fire, and would look correct in review. tests/test_workflows.py forbids that shape
16
+ # repository-wide.
17
+ #
18
+ # Publishing is the last job here, and it waits for a person. It hands the index the artifact the
19
+ # release workflow built and checked against the tag in this same run, and it starts only once a
20
+ # reviewer approves the run in the `pypi` deployment environment. That approval is the gate, and it is
21
+ # deliberately not in this file. The job lives here rather than in release.yml because a called
22
+ # workflow can narrow the token its caller grants but never widen it, and the index does not support a
23
+ # reusable workflow as a trusted publisher. So this file is the workflow registered with the index.
24
+
25
+ name: Tag on merge
26
+
27
+ on:
28
+ push:
29
+ branches: [main]
30
+
31
+ # Read by default; each job that needs more says so for itself.
32
+ permissions:
33
+ contents: read
34
+
35
+ jobs:
36
+ tag:
37
+ name: Tag the declared version, if it is new
38
+ runs-on: ubuntu-latest
39
+ permissions:
40
+ contents: write
41
+ # Not keyed on the ref: every run of this workflow is on `main`, and the point is that two merges
42
+ # landing seconds apart cannot both decide that a tag is missing. Cancelling is wrong here — a
43
+ # cancelled tag job leaves a release untagged with nothing to report it, which is the failure this
44
+ # workflow exists to prevent.
45
+ #
46
+ # On this job rather than on the workflow, because the run ends in a job that waits for a reviewer.
47
+ # At workflow level the group would be held for the whole wait, and GitHub keeps one pending run
48
+ # per group, cancelling it when the next one arrives — so a merge landing during the wait could
49
+ # lose its tag run to the merge after it. Here the group is released before anything waits on a
50
+ # person.
51
+ concurrency:
52
+ group: tag-on-merge
53
+ cancel-in-progress: false
54
+ outputs:
55
+ version: ${{ steps.tag.outputs.version }}
56
+ created: ${{ steps.tag.outputs.created }}
57
+ steps:
58
+ - uses: actions/checkout@v6
59
+ with:
60
+ # Brings the tags with it, so the "does this already exist?" question and the tag object
61
+ # itself are one local repository. It also keeps the push off a shallow clone, which git
62
+ # can refuse.
63
+ fetch-depth: 0
64
+
65
+ - id: tag
66
+ run: |
67
+ set -euo pipefail
68
+
69
+ # The same extraction make release-check performs, for the same reason: this is the one
70
+ # place the version is declared, and reading the file as text needs no environment.
71
+ version="$(sed -n 's/^__version__ = "\(.*\)"$/\1/p' src/cassetta/__init__.py | head -n 1)"
72
+
73
+ if [ -z "$version" ]; then
74
+ echo "src/cassetta/__init__.py declares no __version__ — nothing to tag" >&2
75
+ exit 1
76
+ fi
77
+
78
+ echo "version=${version}" >>"$GITHUB_OUTPUT"
79
+
80
+ if git rev-parse -q --verify "refs/tags/v${version}" >/dev/null; then
81
+ echo "created=false" >>"$GITHUB_OUTPUT"
82
+ printf 'v%s is already tagged — this merge carries no release.\n' "$version"
83
+ exit 0
84
+ fi
85
+
86
+ # Annotated, with the tag's own name as the message: that is what v0.26.0 through v0.26.5
87
+ # carry, and a release history where one entry is shaped differently is a question someone
88
+ # has to answer later.
89
+ git config user.name "github-actions[bot]"
90
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
91
+ git tag -a "v${version}" -m "v${version}"
92
+ git push origin "v${version}"
93
+
94
+ echo "created=true" >>"$GITHUB_OUTPUT"
95
+ printf 'tagged %s as v%s\n' "$(git rev-parse --short HEAD)" "$version"
96
+
97
+ release:
98
+ name: Release
99
+ needs: tag
100
+ if: needs.tag.outputs.created == 'true'
101
+ # A call, in this same run — not something the tag push triggers, because it could not be. A job
102
+ # that carries `uses:` carries no `runs-on` and no `steps` of its own.
103
+ uses: ./.github/workflows/release.yml
104
+ with:
105
+ version: ${{ needs.tag.outputs.version }}
106
+
107
+ publish:
108
+ name: Publish the release, once a reviewer approves
109
+ # `release` is what makes this the checked build. When the merge carried no new version `release`
110
+ # is skipped, and this job is skipped with it — the quiet no-op above, not a gate. `tag` is listed
111
+ # as well because the version is read from its outputs, and the `needs` context carries only the
112
+ # jobs named here.
113
+ needs: [tag, release]
114
+ runs-on: ubuntu-latest
115
+ # The gate: a reviewer on this environment approves the run before this job starts, and the name
116
+ # is also what the index checks in the token's claim. There is no `if:`. A false condition would
117
+ # skip the job and paint the run green, and a release that silently did not happen is the failure
118
+ # this whole composition is built to rule out.
119
+ environment: pypi
120
+ # Only what the OIDC exchange needs, and on this job alone. No token, no secret, no `password:` —
121
+ # the publish action trades a short-lived identity token for an upload credential on its own.
122
+ permissions:
123
+ id-token: write
124
+ steps:
125
+ # The bytes release.yml built and checked against the tag, under the name it uploaded them with.
126
+ # Nothing here checks the repository out, so there is nothing to build a second distribution from.
127
+ - uses: actions/download-artifact@v8
128
+ with:
129
+ name: cassetta-${{ needs.tag.outputs.version }}
130
+ path: dist/
131
+
132
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,41 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+
10
+ # IDE
11
+ .idea/
12
+ .vscode/
13
+ *.swp
14
+
15
+ # Atomic writes: tooling that writes <path>.tmp then renames it into place
16
+ *.tmp
17
+
18
+ # Testing & Linting
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .pytest_cache/
22
+ htmlcov/
23
+ .coverage
24
+
25
+ # Environment
26
+ .env
27
+ .env.local
28
+
29
+ # Claude Code (may contain credentials)
30
+ .claude/settings.local.json
31
+
32
+ # OS
33
+ .DS_Store
34
+ Thumbs.db
35
+
36
+ # Docker
37
+ data/
38
+ data.keys/
39
+
40
+ # Briefs
41
+ inbox/