blun-king-cli 9.1.587 → 9.1.588

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 (436) hide show
  1. package/CHANGELOG.md +11 -185
  2. package/LIESMICH.txt +51 -13
  3. package/README.md +44 -47
  4. package/agent-spine-plugin/.codex-plugin/plugin.json +16 -4
  5. package/agent-spine-plugin/CHANGELOG.md +37 -5
  6. package/agent-spine-plugin/README.md +3 -3
  7. package/agent-spine-plugin/blun.plugin.json +45 -10
  8. package/agent-spine-plugin/docs/artifact-evaluation.md +93 -0
  9. package/agent-spine-plugin/docs/host-integration.md +42 -27
  10. package/agent-spine-plugin/docs/preflight-recall.md +4 -2
  11. package/agent-spine-plugin/docs/session-timeline.md +97 -236
  12. package/agent-spine-plugin/docs/world-model.md +25 -0
  13. package/agent-spine-plugin/hooks/codex.json +1 -1
  14. package/agent-spine-plugin/hooks/hooks.json +1 -1
  15. package/agent-spine-plugin/package.json +1 -3
  16. package/agent-spine-plugin/scripts/check-hosts.js +3 -3
  17. package/agent-spine-plugin/scripts/release-check.js +10 -5
  18. package/agent-spine-plugin/scripts/run-checks.js +4 -1
  19. package/agent-spine-plugin/scripts/run-tests-hermetic.js +32 -6
  20. package/agent-spine-plugin/src/cli-learning.js +15 -0
  21. package/agent-spine-plugin/src/cli.js +2 -0
  22. package/agent-spine-plugin/src/hook.js +32 -32
  23. package/agent-spine-plugin/src/lib/action-lesson-recall.js +73 -8
  24. package/agent-spine-plugin/src/lib/briefing.js +146 -36
  25. package/agent-spine-plugin/src/lib/channel-continuity.js +19 -0
  26. package/agent-spine-plugin/src/lib/delivery-agent-usage.js +14 -7
  27. package/agent-spine-plugin/src/lib/gateway-group-response.js +128 -0
  28. package/agent-spine-plugin/src/lib/gateway-runs.js +24 -15
  29. package/agent-spine-plugin/src/lib/hook-briefing-use.js +13 -3
  30. package/agent-spine-plugin/src/lib/hook-context.js +16 -3
  31. package/agent-spine-plugin/src/lib/hook-output.js +129 -5
  32. package/agent-spine-plugin/src/lib/hook-timeline.js +5 -3
  33. package/agent-spine-plugin/src/lib/indexed-memory.js +2 -2
  34. package/agent-spine-plugin/src/lib/learning-artifact-evaluator.js +114 -0
  35. package/agent-spine-plugin/src/lib/learning-context.js +11 -4
  36. package/agent-spine-plugin/src/lib/learning-measurements.js +2 -2
  37. package/agent-spine-plugin/src/lib/mcp-runtime.js +89 -3
  38. package/agent-spine-plugin/src/lib/mcp-source-context.js +12 -2
  39. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +91 -8
  40. package/agent-spine-plugin/src/lib/mcp-world-tools.js +2 -2
  41. package/agent-spine-plugin/src/lib/owned-file-lock.js +20 -1
  42. package/agent-spine-plugin/src/lib/persona-runtime.js +2 -2
  43. package/agent-spine-plugin/src/lib/preflight-delivery-id.js +27 -0
  44. package/agent-spine-plugin/src/lib/preflight.js +4 -4
  45. package/agent-spine-plugin/src/lib/session-timeline-codex.js +15 -0
  46. package/agent-spine-plugin/src/lib/session-timeline-contract.js +12 -4
  47. package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +36 -7
  48. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +13 -10
  49. package/agent-spine-plugin/src/lib/session-timeline-invocation.js +1 -1
  50. package/agent-spine-plugin/src/lib/session-timeline-king.js +14 -0
  51. package/agent-spine-plugin/src/lib/session-timeline-prior.js +18 -12
  52. package/agent-spine-plugin/src/lib/session-timeline-provider.js +5 -0
  53. package/agent-spine-plugin/src/lib/session-timeline-query.js +2 -0
  54. package/agent-spine-plugin/src/lib/session-timeline-results.js +35 -10
  55. package/agent-spine-plugin/src/lib/session-timeline-source-open.js +30 -0
  56. package/agent-spine-plugin/src/lib/session-timeline.js +122 -75
  57. package/agent-spine-plugin/src/lib/source-roots.js +3 -2
  58. package/agent-spine-plugin/src/lib/task-knowledge-context.js +22 -1
  59. package/agent-spine-plugin/src/lib/timeline-continuation-update.js +100 -0
  60. package/agent-spine-plugin/src/lib/timeline-tool-guard.js +30 -7
  61. package/agent-spine-plugin/src/lib/timeline-user-feedback.js +217 -0
  62. package/agent-spine-plugin/src/lib/timeline-world-capture.js +233 -0
  63. package/agent-spine-plugin/src/lib/world-knowledge.js +59 -2
  64. package/agent-spine-plugin/src/lib/world-model.js +64 -9
  65. package/agent-spine-plugin/src/worker.js +13 -1
  66. package/bin/blun.js +43 -28
  67. package/bin/core-bootstrap.js +5 -4
  68. package/bin/king.js +43 -28
  69. package/bin/launcher-mode.js +1 -10
  70. package/bin/launcher-runtime.js +128 -295
  71. package/bin/managed-node.js +0 -0
  72. package/bin/managed-plugin-selection.cjs +0 -1
  73. package/bin/native-module-repair.js +0 -0
  74. package/bin/node-runtime.js +0 -0
  75. package/bin/node-version.js +0 -0
  76. package/bin/plugin-bootstrap.js +56 -120
  77. package/bin/private-paths.js +11 -34
  78. package/bin/standard-tools-bootstrap.js +34 -114
  79. package/bin/turn-thinking-policy.cjs +3 -11
  80. package/bin/update-copy.js +200 -0
  81. package/bin/update-lease.js +0 -0
  82. package/bin/update-notice.js +136 -289
  83. package/bin/verify-agent-behavior.cjs +122 -0
  84. package/bin/verify-agent-components.cjs +104 -0
  85. package/bin/verify-bundled-agent-sources.cjs +57 -0
  86. package/blun.mjs +143076 -135288
  87. package/bundled-agent-sources.json +701 -0
  88. package/package.json +12 -15
  89. package/standard-skills/translate-native/README.md +1293 -0
  90. package/standard-skills/translate-native/SKILL.md +172 -22
  91. package/standard-skills/translate-native/VERSION +1 -1
  92. package/standard-skills/translate-native/agents/openai.yaml +18 -0
  93. package/standard-skills/translate-native/assets/icon.svg +8 -0
  94. package/standard-skills/translate-native/docs/BLUN_CODE_INTEGRATION.md +76 -0
  95. package/standard-skills/translate-native/docs/PREMORTEM.md +489 -0
  96. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +2035 -0
  97. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1302 -0
  98. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +136 -0
  99. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HEALTH_HTTP.md +130 -0
  100. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +175 -0
  101. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +86 -0
  102. package/standard-skills/translate-native/integrations/AGENT_RULES.md +32 -0
  103. package/standard-skills/translate-native/integrations/adapters/blun-code-language-guard.js +514 -0
  104. package/standard-skills/translate-native/integrations/adapters/node-language-guard.js +230 -0
  105. package/standard-skills/translate-native/integrations/audit_log.py +327 -0
  106. package/standard-skills/translate-native/integrations/claude_language_hook.js +1536 -0
  107. package/standard-skills/translate-native/integrations/commercial_localization_profile.py +42 -0
  108. package/standard-skills/translate-native/integrations/delivery-policy.example.json +28 -0
  109. package/standard-skills/translate-native/integrations/enforced_delivery.py +543 -0
  110. package/standard-skills/translate-native/integrations/guard_service.py +435 -0
  111. package/standard-skills/translate-native/integrations/language_gateway.py +67 -0
  112. package/standard-skills/translate-native/integrations/mcp_auth_headers.py +198 -0
  113. package/standard-skills/translate-native/integrations/mcp_http_gateway.py +429 -0
  114. package/standard-skills/translate-native/integrations/non_language_html_entities.js +1485 -0
  115. package/standard-skills/translate-native/integrations/pre_output_guard.py +65 -0
  116. package/standard-skills/translate-native/integrations/task_router.py +101 -0
  117. package/standard-skills/translate-native/integrations/website_localization.py +401 -0
  118. package/standard-skills/translate-native/integrations/website_localization_api.py +581 -0
  119. package/standard-skills/translate-native/integrations/website_localization_benchmark.py +1885 -0
  120. package/standard-skills/translate-native/integrations/website_localization_benchmark_campaign.py +1772 -0
  121. package/standard-skills/translate-native/integrations/website_localization_benchmark_candidate.py +506 -0
  122. package/standard-skills/translate-native/integrations/website_localization_benchmark_http.py +400 -0
  123. package/standard-skills/translate-native/integrations/website_localization_benchmark_review_store.py +781 -0
  124. package/standard-skills/translate-native/integrations/website_localization_benchmark_reviewer_http.py +500 -0
  125. package/standard-skills/translate-native/integrations/website_localization_benchmark_runtime.py +1107 -0
  126. package/standard-skills/translate-native/integrations/website_localization_benchmark_suite.py +463 -0
  127. package/standard-skills/translate-native/integrations/website_localization_cms.py +2835 -0
  128. package/standard-skills/translate-native/integrations/website_localization_cms_client.py +875 -0
  129. package/standard-skills/translate-native/integrations/website_localization_cms_dispatch.py +805 -0
  130. package/standard-skills/translate-native/integrations/website_localization_cms_http.py +588 -0
  131. package/standard-skills/translate-native/integrations/website_localization_cms_lifecycle_monitor.py +991 -0
  132. package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +1441 -0
  133. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +414 -0
  134. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +1073 -0
  135. package/standard-skills/translate-native/integrations/website_localization_cms_removal_dispatch.py +865 -0
  136. package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +583 -0
  137. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +964 -0
  138. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +665 -0
  139. package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +1153 -0
  140. package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +675 -0
  141. package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +1125 -0
  142. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification.py +674 -0
  143. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +444 -0
  144. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +1469 -0
  145. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +1142 -0
  146. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +634 -0
  147. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +804 -0
  148. package/standard-skills/translate-native/integrations/website_localization_deepl_baseline.py +922 -0
  149. package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +482 -0
  150. package/standard-skills/translate-native/integrations/website_localization_health.py +1541 -0
  151. package/standard-skills/translate-native/integrations/website_localization_health_http.py +372 -0
  152. package/standard-skills/translate-native/integrations/website_localization_http_provider.py +297 -0
  153. package/standard-skills/translate-native/integrations/website_localization_native_reference_http.py +479 -0
  154. package/standard-skills/translate-native/integrations/website_localization_native_reference_intake.py +363 -0
  155. package/standard-skills/translate-native/integrations/website_localization_native_reference_queue.py +1449 -0
  156. package/standard-skills/translate-native/integrations/website_localization_native_reference_store.py +420 -0
  157. package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +235 -0
  158. package/standard-skills/translate-native/integrations/website_localization_queue.py +671 -0
  159. package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +516 -0
  160. package/standard-skills/translate-native/integrations/website_localization_release.py +928 -0
  161. package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +1008 -0
  162. package/standard-skills/translate-native/integrations/website_localization_runner.py +276 -0
  163. package/standard-skills/translate-native/integrations/website_localization_runtime.py +862 -0
  164. package/standard-skills/translate-native/integrations/website_localization_service.py +350 -0
  165. package/standard-skills/translate-native/integrations/website_localization_supervisor.py +511 -0
  166. package/standard-skills/translate-native/integrations/website_localization_worker.py +663 -0
  167. package/standard-skills/translate-native/provenance.json +3 -4
  168. package/standard-skills/translate-native/references/commercial-localization.md +177 -0
  169. package/standard-skills/translate-native/scripts/blun_language_guard.py +7 -1
  170. package/standard-skills/translate-native/scripts/check_commercial_review.py +80 -0
  171. package/standard-skills/translate-native/scripts/commercial_localization_profile.py +333 -0
  172. package/standard-tools/language-guard/LICENSE +21 -0
  173. package/standard-tools/language-guard/VERSION +1 -0
  174. package/standard-tools/language-guard/blun_language_guard.py +7 -1
  175. package/standard-tools/language-guard/check_commercial_review.py +80 -0
  176. package/standard-tools/language-guard/commercial_localization_profile.py +333 -0
  177. package/standard-tools/language-guard/language_gateway.py +62 -0
  178. package/standard-tools/language-guard/pre_output_guard.py +64 -0
  179. package/standard-tools/language-guard/provenance.json +4 -11
  180. package/standard-tools/manifest.json +34 -11
  181. package/telegram-plugin/commands/access.md +2 -10
  182. package/telegram-plugin/dist/bridge.mjs +64041 -687
  183. package/telegram-plugin/dist/mcp-server.mjs +72810 -9027
  184. package/telegram-plugin/dist/noise.mjs +28 -63511
  185. package/agent-spine-plugin/CONTRIBUTING.md +0 -52
  186. package/agent-spine-plugin/SECURITY.md +0 -47
  187. package/agent-spine-plugin/docs/assignment-continuation.md +0 -48
  188. package/agent-spine-plugin/docs/releasing.md +0 -85
  189. package/agent-spine-plugin/docs/structured-completion.md +0 -67
  190. package/bin/abort-listener-policy.cjs +0 -43
  191. package/bin/active-steer-priority-policy.cjs +0 -24
  192. package/bin/agent-api-http-adapter.mjs +0 -446
  193. package/bin/agent-api-private-http-server.mjs +0 -288
  194. package/bin/agent-api-runtime.mjs +0 -252
  195. package/bin/agent-api-service-environment.mjs +0 -236
  196. package/bin/agent-api-service-host.mjs +0 -209
  197. package/bin/agent-api-service-process.mjs +0 -171
  198. package/bin/agent-api-session-registry.mjs +0 -428
  199. package/bin/agent-api-tool-broker.cjs +0 -248
  200. package/bin/agent-api-turn-controller.mjs +0 -461
  201. package/bin/agent-api-usage-journal.cjs +0 -259
  202. package/bin/agent-resume-snapshot.cjs +0 -241
  203. package/bin/agentspine-king-goal-inbox.mjs +0 -111
  204. package/bin/agentspine-king-goal-intake.mjs +0 -106
  205. package/bin/approval-rejection-stop.cjs +0 -15
  206. package/bin/assistant-message-offload-policy.cjs +0 -284
  207. package/bin/baseline-skill-performance-policy.cjs +0 -39
  208. package/bin/bash-search-scope-policy.cjs +0 -49
  209. package/bin/codebase-search-runtime.cjs +0 -23
  210. package/bin/cognitive-action-checkpoint.cjs +0 -1104
  211. package/bin/cognitive-attention-delivery.cjs +0 -76
  212. package/bin/cognitive-attention-policy.cjs +0 -143
  213. package/bin/cognitive-attention-runtime.cjs +0 -91
  214. package/bin/cognitive-context-projection.cjs +0 -73
  215. package/bin/cognitive-cross-portal-acceptance.cjs +0 -443
  216. package/bin/cognitive-effective-view.cjs +0 -77
  217. package/bin/cognitive-focus-projection.cjs +0 -206
  218. package/bin/cognitive-focus-scope.cjs +0 -37
  219. package/bin/cognitive-goal-autostart-policy.cjs +0 -72
  220. package/bin/cognitive-goal-time-trigger-controller.cjs +0 -146
  221. package/bin/cognitive-memory-adapter.cjs +0 -282
  222. package/bin/cognitive-memory-command.cjs +0 -293
  223. package/bin/cognitive-memory-provider.cjs +0 -92
  224. package/bin/cognitive-salience-policy.cjs +0 -159
  225. package/bin/cognitive-state-store.cjs +0 -508
  226. package/bin/cognitive-turn-lifecycle.cjs +0 -624
  227. package/bin/cognitive-work-focus.cjs +0 -180
  228. package/bin/compaction-history-archive.cjs +0 -166
  229. package/bin/compaction-history-startup.cjs +0 -50
  230. package/bin/compaction-model-policy.cjs +0 -31
  231. package/bin/compaction-stage-policy.cjs +0 -21
  232. package/bin/compaction-transaction-policy.cjs +0 -122
  233. package/bin/config-write-dedup-policy.cjs +0 -27
  234. package/bin/context-budget-ledger.cjs +0 -31
  235. package/bin/context-doctor-policy.cjs +0 -70
  236. package/bin/context-insight-policy.cjs +0 -36
  237. package/bin/context-performance-policy.cjs +0 -19
  238. package/bin/context-pressure-policy.cjs +0 -20
  239. package/bin/cron-run-output.cjs +0 -45
  240. package/bin/cron-run-store.cjs +0 -145
  241. package/bin/curiosity-scout-policy.cjs +0 -49
  242. package/bin/default-model-output-budget-policy.cjs +0 -28
  243. package/bin/durable-task-resume-policy.cjs +0 -130
  244. package/bin/durable-task-resume-runtime.cjs +0 -117
  245. package/bin/durable-task-resume-store.cjs +0 -88
  246. package/bin/editable-tool-approval-policy.cjs +0 -540
  247. package/bin/editable-tool-approval-runtime.cjs +0 -99
  248. package/bin/effective-system-prompt-cache-policy.cjs +0 -33
  249. package/bin/error-memory-performance-policy.cjs +0 -113
  250. package/bin/file-observation-policy.cjs +0 -133
  251. package/bin/foreground-output-capture-policy.cjs +0 -41
  252. package/bin/generated-source-health.cjs +0 -142
  253. package/bin/glob-pattern-policy.cjs +0 -13
  254. package/bin/goal-completion-evidence-policy.cjs +0 -120
  255. package/bin/grep-output-limit-policy.cjs +0 -39
  256. package/bin/historical-media-projection-policy.cjs +0 -48
  257. package/bin/history-offload-pressure-policy.cjs +0 -33
  258. package/bin/html-to-research-markdown.cjs +0 -147
  259. package/bin/identity-context-policy.cjs +0 -764
  260. package/bin/identity-journal-policy.cjs +0 -107
  261. package/bin/input-draft-persistence.cjs +0 -77
  262. package/bin/king-tui-function-contract.json +0 -33
  263. package/bin/launcher-restart-policy.cjs +0 -150
  264. package/bin/live-response-repetition-guard.cjs +0 -196
  265. package/bin/llm-config-log-dedup-policy.cjs +0 -76
  266. package/bin/loop-event-record-policy.cjs +0 -174
  267. package/bin/managed-context-startup-policy.cjs +0 -27
  268. package/bin/media-activity-layout-policy.cjs +0 -34
  269. package/bin/media-auto-retrieval-policy.cjs +0 -90
  270. package/bin/media-result-policy.cjs +0 -59
  271. package/bin/micro-compaction-policy.cjs +0 -145
  272. package/bin/mistake-relevance-policy.cjs +0 -319
  273. package/bin/model-retry-progress-policy.cjs +0 -46
  274. package/bin/native-large-file-io.cjs +0 -42
  275. package/bin/native-runtime-cache.cjs +0 -76
  276. package/bin/natural-presence-policy.cjs +0 -28
  277. package/bin/noninteractive-shell-env-policy.cjs +0 -19
  278. package/bin/observer-hooks.cjs +0 -14
  279. package/bin/outbound-claim-provenance.cjs +0 -150
  280. package/bin/oversized-context-offload-policy.cjs +0 -86
  281. package/bin/pending-media-policy.cjs +0 -182
  282. package/bin/pending-token-estimate-policy.cjs +0 -41
  283. package/bin/personal-memory-consent-policy.cjs +0 -72
  284. package/bin/personal-memory-performance-policy.cjs +0 -12
  285. package/bin/personality-choice-policy.cjs +0 -101
  286. package/bin/personality-memory-adapter.cjs +0 -379
  287. package/bin/personality-mode.cjs +0 -46
  288. package/bin/personality-setup-policy.cjs +0 -197
  289. package/bin/proactive-compaction-policy.cjs +0 -25
  290. package/bin/profile-identity-resolution.cjs +0 -136
  291. package/bin/profile-runtime.cjs +0 -318
  292. package/bin/profile-tool-exclusion-policy.cjs +0 -37
  293. package/bin/programmatic-context-isolation.cjs +0 -25
  294. package/bin/programmatic-tool-runtime.mjs +0 -627
  295. package/bin/provider-idle-timeout-policy.cjs +0 -14
  296. package/bin/provider-model-refresh-deadline.cjs +0 -53
  297. package/bin/provider-model-refresh-policy.cjs +0 -107
  298. package/bin/rate-limit-recovery-policy.cjs +0 -47
  299. package/bin/read-batch-policy.cjs +0 -32
  300. package/bin/read-continuation-policy.cjs +0 -59
  301. package/bin/recurring-cron-history-policy.cjs +0 -124
  302. package/bin/relationship-continuity-policy.cjs +0 -143
  303. package/bin/relationship-curiosity-policy.cjs +0 -107
  304. package/bin/relationship-learning-policy.cjs +0 -168
  305. package/bin/release-artifact-freeze-policy.cjs +0 -30
  306. package/bin/reload-plugin-bootstrap.cjs +0 -18
  307. package/bin/reload-queue-policy.cjs +0 -38
  308. package/bin/repeated-assistant-response-policy.cjs +0 -232
  309. package/bin/repeated-injection-projection.cjs +0 -107
  310. package/bin/repeated-user-message-projection.cjs +0 -8
  311. package/bin/research-page-result.cjs +0 -74
  312. package/bin/retry-checkpoint-policy.cjs +0 -13
  313. package/bin/runtime-exit-ledger.cjs +0 -144
  314. package/bin/scoped-cron-run-policy.cjs +0 -358
  315. package/bin/session-checkpoint-policy.cjs +0 -25
  316. package/bin/session-compaction-policy.cjs +0 -84
  317. package/bin/session-replay-policy.cjs +0 -20
  318. package/bin/session-replay-window-policy.cjs +0 -40
  319. package/bin/session-resume-checkpoint.cjs +0 -254
  320. package/bin/session-scrollback-archive.cjs +0 -229
  321. package/bin/skill-activation-performance-policy.cjs +0 -69
  322. package/bin/skill-listing-performance-policy.cjs +0 -92
  323. package/bin/soul-organization-policy.cjs +0 -78
  324. package/bin/soul-preservation-policy.cjs +0 -20
  325. package/bin/startup-preferences.cjs +0 -131
  326. package/bin/streaming-flush-performance-policy.cjs +0 -28
  327. package/bin/structured-agent-swarm-output.cjs +0 -325
  328. package/bin/structured-subagent-output.cjs +0 -252
  329. package/bin/subagent-context-fork-policy.cjs +0 -155
  330. package/bin/subagent-max-tokens-handoff-policy.cjs +0 -69
  331. package/bin/subagent-parent-responsiveness.cjs +0 -19
  332. package/bin/subagent-skill-policy.cjs +0 -206
  333. package/bin/subagent-timeout-policy.cjs +0 -182
  334. package/bin/subagent-tool-policy.cjs +0 -60
  335. package/bin/subagent-usage-rollup-policy.cjs +0 -29
  336. package/bin/system-prompt-context-policy.cjs +0 -124
  337. package/bin/system-prompt-token-cache-policy.cjs +0 -60
  338. package/bin/telegram-addressed-focus.cjs +0 -55
  339. package/bin/telegram-addressed-priority.cjs +0 -12
  340. package/bin/telegram-approval-relay.cjs +0 -290
  341. package/bin/telegram-bot-priority.cjs +0 -17
  342. package/bin/telegram-console-status-policy.cjs +0 -174
  343. package/bin/telegram-context-projection-policy.cjs +0 -141
  344. package/bin/telegram-delivery-lifecycle.cjs +0 -125
  345. package/bin/telegram-direct-focus-policy.cjs +0 -273
  346. package/bin/telegram-mcp-compatibility.cjs +0 -49
  347. package/bin/telegram-media-delivery-policy.cjs +0 -42
  348. package/bin/telegram-private-conversation-policy.cjs +0 -185
  349. package/bin/telegram-queue-handoff-policy.cjs +0 -73
  350. package/bin/telegram-remote-status-policy.cjs +0 -120
  351. package/bin/telegram-session-queue-runtime.mjs +0 -306
  352. package/bin/telegram-text-chunk-policy.cjs +0 -63
  353. package/bin/telegram-truncated-reply-policy.cjs +0 -37
  354. package/bin/telegram-urgent-policy.cjs +0 -45
  355. package/bin/telemetry-spool-policy.cjs +0 -57
  356. package/bin/thinking-activity-status-policy.cjs +0 -132
  357. package/bin/thinking-only-guard.cjs +0 -80
  358. package/bin/todo-list-turn-policy.cjs +0 -131
  359. package/bin/tool-call-loop-policy.cjs +0 -51
  360. package/bin/tool-file-persistence.cjs +0 -141
  361. package/bin/tool-result-offload-policy.cjs +0 -359
  362. package/bin/tool-result-offload-telemetry.cjs +0 -12
  363. package/bin/tool-schema-token-cache-policy.cjs +0 -41
  364. package/bin/tool-stream-preview-policy.cjs +0 -9
  365. package/bin/tui-functional-contract.cjs +0 -55
  366. package/bin/turn-tool-performance-policy.cjs +0 -486
  367. package/bin/usage-cache-efficiency-policy.cjs +0 -26
  368. package/bin/user-home-path-policy.cjs +0 -13
  369. package/bin/user-message-offload-policy.cjs +0 -103
  370. package/bin/user-prompt-hook-origin-policy.cjs +0 -34
  371. package/bin/user-tool-record-policy.cjs +0 -7
  372. package/bin/validated-learning-insight-policy.cjs +0 -58
  373. package/bin/validated-learning-outcome-trace.cjs +0 -107
  374. package/bin/validated-learning-performance-policy.cjs +0 -53
  375. package/bin/validated-learning-signal.cjs +0 -463
  376. package/bin/windows-bash-dialect-policy.cjs +0 -25
  377. package/bin/windows-node-crash-dump.cjs +0 -110
  378. package/bin/write-continuation-policy.cjs +0 -69
  379. package/codebase-index/README.md +0 -82
  380. package/codebase-index/codebase_index.py +0 -470
  381. package/standard-skills/agent-browser/SKILL.md +0 -19
  382. package/standard-skills/agent-browser/references/runtime.md +0 -8
  383. package/standard-skills/blun-session-inspector/SKILL.md +0 -41
  384. package/standard-skills/blun-session-inspector/scripts/inspect-session.cjs +0 -437
  385. package/standard-skills/design-taste-frontend/SKILL.md +0 -1206
  386. package/standard-skills/full-output-enforcement/SKILL.md +0 -49
  387. package/standard-skills/high-end-visual-design/SKILL.md +0 -98
  388. package/standard-skills/image-to-code/SKILL.md +0 -1228
  389. package/standard-skills/industrial-brutalist-ui/SKILL.md +0 -92
  390. package/standard-skills/minimalist-ui/SKILL.md +0 -85
  391. package/standard-skills/motion-design-taste/SKILL.md +0 -74
  392. package/standard-skills/playwright-testing/SKILL.md +0 -19
  393. package/standard-skills/playwright-testing/references/runtime.md +0 -7
  394. package/standard-skills/premortem/SKILL.md +0 -148
  395. package/standard-skills/redesign-existing-projects/SKILL.md +0 -178
  396. package/standard-skills/research-evidence/SKILL.md +0 -39
  397. package/standard-skills/research-evidence/references/evidence-format.md +0 -104
  398. package/standard-skills/research-evidence/scripts/evidence-collection.cjs +0 -260
  399. package/standard-skills/research-evidence/scripts/score-report.cjs +0 -130
  400. package/standard-skills/screenshot-lesen/SKILL.md +0 -52
  401. package/standard-skills/stitch-design-taste/DESIGN.md +0 -121
  402. package/standard-skills/stitch-design-taste/SKILL.md +0 -184
  403. package/standard-skills/telegram-channel/SKILL.md +0 -18
  404. package/standard-skills/telegram-channel/references/runtime.md +0 -7
  405. package/standard-skills/venture-flywheel/SKILL.md +0 -32
  406. package/standard-skills/venture-flywheel/identity/project-identity.cjs +0 -146
  407. package/standard-skills/venture-flywheel/policy/capability-engine.cjs +0 -114
  408. package/standard-skills/venture-flywheel/policy/repository-trust.cjs +0 -229
  409. package/standard-skills/venture-flywheel/references/BEISPIELE-phase0.md +0 -146
  410. package/standard-skills/venture-flywheel/references/CAPABILITY-MAP.md +0 -34
  411. package/standard-skills/venture-flywheel/references/SPEC-phase0-identity-trust.md +0 -77
  412. package/standard-skills/venture-flywheel/references/SPEC-phase0-state-events.md +0 -93
  413. package/standard-skills/venture-flywheel/schemas/capability-decision.schema.json +0 -13
  414. package/standard-skills/venture-flywheel/schemas/execution-event.schema.json +0 -44
  415. package/standard-skills/venture-flywheel/schemas/project-identity.schema.json +0 -32
  416. package/standard-skills/venture-flywheel/schemas/repository-trust.schema.json +0 -57
  417. package/standard-skills/venture-flywheel/schemas/run-transition.schema.json +0 -59
  418. package/standard-skills/venture-flywheel/state/execution-event.cjs +0 -191
  419. package/standard-skills/venture-flywheel/state/task-state-machine.cjs +0 -190
  420. package/standard-skills/web-lesen/SKILL.md +0 -73
  421. package/standard-skills/web-lesen/scripts/crawl_public.py +0 -379
  422. package/standard-skills/windows-mcp/SKILL.md +0 -19
  423. package/standard-skills/windows-mcp/references/runtime.md +0 -9
  424. package/telegram-plugin/DELIVERY.md +0 -36
  425. package/telegram-plugin/bin/telegram-approval-relay.cjs +0 -290
  426. package/telegram-plugin/bin/telegram-console-status-policy.cjs +0 -175
  427. package/telegram-plugin/bin/telegram-delivery-lifecycle.cjs +0 -125
  428. package/telegram-plugin/bin/telegram-direct-reply-policy.cjs +0 -48
  429. package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +0 -122
  430. package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +0 -186
  431. package/telegram-plugin/bin/telegram-remote-status-policy.cjs +0 -121
  432. package/telegram-plugin/bin/telegram-reply-parts.cjs +0 -149
  433. package/telegram-plugin/bin/telegram-text-chunk-policy.cjs +0 -63
  434. package/telegram-plugin/bin/telegram-typing-keepalive.cjs +0 -89
  435. package/telegram-plugin/compat/mcp-server-fa511cd1.mjs +0 -73825
  436. /package/{bin → scripts}/fix-node-pty-perms.js +0 -0
@@ -0,0 +1,1153 @@
1
+ #!/usr/bin/env python3
2
+ """Authenticated WSGI ingress for one durable source-CMS runtime.
3
+
4
+ The boundary accepts exact change and removal envelopes from the website host
5
+ and exposes content-free status, health, and capability data. Authentication
6
+ receives the request metadata and body digest, never the website payload itself.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import math
14
+ import re
15
+ import unicodedata
16
+ from dataclasses import asdict
17
+ from typing import Any, Callable, Mapping
18
+
19
+
20
+ API_SCHEMA = "blun.cms-source-runtime-api.v1"
21
+ ERROR_SCHEMA = "blun.cms-source-runtime-http-error.v1"
22
+ AUTH_REQUEST_SCHEMA = "blun.cms-source-runtime-http-auth-request.v1"
23
+ PRINCIPAL_SCHEMA = "blun.cms-source-runtime-principal.v1"
24
+ STATUS_PRINCIPAL_SCHEMA = "blun.cms-source-status-principal.v1"
25
+ CHANGE_REQUEST_SCHEMA = "blun.cms-source-change-enqueue-request.v1"
26
+ CHANGE_RESPONSE_SCHEMA = "blun.cms-source-change-enqueue-response.v2"
27
+ REMOVAL_REQUEST_SCHEMA = "blun.cms-source-removal-enqueue-request.v1"
28
+ REMOVAL_RESPONSE_SCHEMA = "blun.cms-source-removal-enqueue-response.v2"
29
+ STATUS_REQUEST_SCHEMA = "blun.cms-source-status-request.v1"
30
+ STATUS_RESPONSE_SCHEMA = "blun.cms-source-status-response.v4"
31
+ HEALTH_RESPONSE_SCHEMA = "blun.cms-source-health-response.v4"
32
+ READINESS_RESPONSE_SCHEMA = "blun.cms-source-readiness-response.v2"
33
+ CAPABILITIES_SCHEMA = "blun.cms-source-runtime-capabilities.v1"
34
+ CAPABILITIES_RESPONSE_SCHEMA = "blun.cms-source-capabilities-response.v1"
35
+
36
+ CHANGE_PATH = "/v1/localization/source/changes"
37
+ REMOVAL_PATH = "/v1/localization/source/removals"
38
+ STATUS_PATH = "/v1/localization/source/status"
39
+ HEALTH_PATH = "/v1/localization/source/health"
40
+ READINESS_PATH = "/v1/localization/source/readiness"
41
+ CAPABILITIES_PATH = "/v1/localization/source/capabilities"
42
+
43
+ MAX_BODY_BYTES = 4_000_000
44
+ MAX_HEADERS = 64
45
+ MAX_HEADER_VALUE = 4096
46
+ TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
47
+ SHA256 = re.compile(r"^[0-9a-f]{64}$")
48
+ ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
49
+ HEADER_NAME = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$")
50
+ STATUSES = {"pending", "leased", "retry_wait", "succeeded", "failed"}
51
+ REMOTE_QUEUE_STATES = STATUSES | {"cancelled"}
52
+ LIFECYCLE_STATES = {
53
+ "pending", "leased", "watching", "retry_wait", "terminal", "failed",
54
+ }
55
+ REMOTE_STATUSES = {
56
+ "awaiting_approval", "cancelled", "deleted", "deleting",
57
+ "deletion_failed", "localization_failed", "processing",
58
+ "publication_blocked", "publication_failed", "published", "publishing",
59
+ "queue_recovery", "ready", "superseded",
60
+ }
61
+ HEALTH_STATUSES = {"ok", "degraded", "blocked"}
62
+ SCOPES = {
63
+ CHANGE_PATH: "source-change:write",
64
+ REMOVAL_PATH: "source-removal:write",
65
+ STATUS_PATH: "source-status:read",
66
+ HEALTH_PATH: "source-health:read",
67
+ READINESS_PATH: "source-readiness:read",
68
+ CAPABILITIES_PATH: "source-capabilities:read",
69
+ }
70
+ METHODS = {
71
+ CHANGE_PATH: "POST",
72
+ REMOVAL_PATH: "POST",
73
+ STATUS_PATH: "POST",
74
+ HEALTH_PATH: "GET",
75
+ READINESS_PATH: "GET",
76
+ CAPABILITIES_PATH: "GET",
77
+ }
78
+
79
+
80
+ class CMSSourceHTTPBlocked(RuntimeError):
81
+ """Stable content-free HTTP failure."""
82
+
83
+ def __init__(self, code: str, status: int):
84
+ if ERROR_CODE.fullmatch(code) is None or status not in {
85
+ 400, 401, 403, 404, 405, 411, 413, 415, 409, 503,
86
+ }:
87
+ raise ValueError("invalid source HTTP failure")
88
+ super().__init__(code)
89
+ self.code = code
90
+ self.status = status
91
+
92
+
93
+ def _token(value: Any) -> str:
94
+ if (
95
+ not isinstance(value, str)
96
+ or TOKEN.fullmatch(value) is None
97
+ or not unicodedata.is_normalized("NFC", value)
98
+ ):
99
+ raise ValueError
100
+ return value
101
+
102
+
103
+ def _count(value: Any, *, maximum: int = 1_000_000) -> int:
104
+ if (
105
+ isinstance(value, bool)
106
+ or not isinstance(value, int)
107
+ or not 0 <= value <= maximum
108
+ ):
109
+ raise ValueError
110
+ return value
111
+
112
+
113
+ def _canonical_json(value: Any) -> bytes:
114
+ try:
115
+ result = json.dumps(
116
+ value,
117
+ ensure_ascii=False,
118
+ allow_nan=False,
119
+ sort_keys=True,
120
+ separators=(",", ":"),
121
+ ).encode("utf-8")
122
+ except (TypeError, ValueError, RecursionError):
123
+ raise CMSSourceHTTPBlocked("source_http.response_invalid", 503) from None
124
+ if not result or len(result) > MAX_BODY_BYTES:
125
+ raise CMSSourceHTTPBlocked("source_http.response_invalid", 503)
126
+ return result
127
+
128
+
129
+ def _pairs(items):
130
+ result = {}
131
+ for key, value in items:
132
+ if key in result:
133
+ raise ValueError
134
+ result[key] = value
135
+ return result
136
+
137
+
138
+ def _constant(_value):
139
+ raise ValueError
140
+
141
+
142
+ def _principal(value: Any, scope: str) -> dict[str, str]:
143
+ fields = {
144
+ "schema", "principal_id", "credential_id", "credential_version", "scope",
145
+ }
146
+ status_scope = scope == SCOPES[STATUS_PATH]
147
+ if status_scope:
148
+ fields.add("site_id")
149
+ if not isinstance(value, Mapping) or set(value) != fields:
150
+ raise CMSSourceHTTPBlocked("source_http.authentication_failed", 401)
151
+ try:
152
+ expected_schema = STATUS_PRINCIPAL_SCHEMA if status_scope else PRINCIPAL_SCHEMA
153
+ if value["schema"] != expected_schema:
154
+ raise ValueError
155
+ for name in ("principal_id", "credential_id", "credential_version"):
156
+ _token(value[name])
157
+ _token(value["scope"])
158
+ if status_scope:
159
+ _token(value["site_id"])
160
+ except (KeyError, ValueError, TypeError):
161
+ raise CMSSourceHTTPBlocked("source_http.authentication_failed", 401) from None
162
+ if value["scope"] != scope:
163
+ raise CMSSourceHTTPBlocked("source_http.scope_rejected", 403)
164
+ return dict(value)
165
+
166
+
167
+ def _status_payload(value: Any, *, removal: bool) -> dict[str, Any]:
168
+ try:
169
+ payload = asdict(value)
170
+ required = {
171
+ "event_id", "payload_sha256", "status", "attempts", "max_attempts",
172
+ "next_attempt_at", "lease_expires_at", "lease_expired",
173
+ "last_error_code", "remote_plan_id", "remote_job_count",
174
+ "remote_status", "response_sha256", "last_error_detail_hash",
175
+ }
176
+ if removal:
177
+ required = {
178
+ "operation", "request_id", "event_id", "payload_sha256",
179
+ "status", "attempts", "max_attempts", "next_attempt_at",
180
+ "lease_expires_at", "lease_expired", "last_error_code",
181
+ "remote_delivery_id", "remote_status", "remote_new",
182
+ "response_sha256",
183
+ }
184
+ if set(payload) != required:
185
+ raise ValueError
186
+ _token(payload["event_id"])
187
+ if removal:
188
+ _token(payload["operation"])
189
+ _token(payload["request_id"])
190
+ if SHA256.fullmatch(payload["payload_sha256"]) is None:
191
+ raise ValueError
192
+ if payload["status"] not in STATUSES:
193
+ raise ValueError
194
+ attempts = _count(payload["attempts"], maximum=20)
195
+ maximum = _count(payload["max_attempts"], maximum=20)
196
+ if not 1 <= maximum or attempts > maximum:
197
+ raise ValueError
198
+ return {
199
+ "operation": payload["operation"] if removal else "change",
200
+ "request_id": payload["request_id"] if removal else payload["event_id"],
201
+ "event_id": payload["event_id"],
202
+ "payload_sha256": payload["payload_sha256"],
203
+ "status": payload["status"],
204
+ "attempts": attempts,
205
+ "max_attempts": maximum,
206
+ }
207
+ except Exception:
208
+ raise CMSSourceHTTPBlocked(
209
+ "source_http.runtime_response_invalid", 503,
210
+ ) from None
211
+
212
+
213
+ def _optional_token(value: Any) -> str | None:
214
+ if value is None:
215
+ return None
216
+ return _token(value)
217
+
218
+
219
+ def _optional_error(value: Any) -> str | None:
220
+ if value is None:
221
+ return None
222
+ if not isinstance(value, str) or ERROR_CODE.fullmatch(value) is None:
223
+ raise ValueError
224
+ return value
225
+
226
+
227
+ def _timestamp(value: Any) -> float:
228
+ if (
229
+ isinstance(value, bool) or not isinstance(value, (int, float))
230
+ or not math.isfinite(float(value)) or float(value) < 0
231
+ ):
232
+ raise ValueError
233
+ return float(value)
234
+
235
+
236
+ def _locales(value: Any) -> list[str]:
237
+ if not isinstance(value, (list, tuple)):
238
+ raise ValueError
239
+ result = [_token(item) for item in value]
240
+ if len(result) > 24 or len(set(result)) != len(result):
241
+ raise ValueError
242
+ return result
243
+
244
+
245
+ def _source_status_payload(
246
+ value: Any,
247
+ *,
248
+ expected_event_id: str,
249
+ expected_site_id: str,
250
+ ) -> dict[str, Any]:
251
+ try:
252
+ payload = value.as_payload()
253
+ fields = {
254
+ "schema", "event_id", "site_id", "website_version",
255
+ "source_sequence", "change_sha256", "dispatch_status",
256
+ "dispatch_attempts", "dispatch_max_attempts",
257
+ "dispatch_error_code", "plan_id", "job_count",
258
+ "lifecycle_state", "lifecycle_poll_attempts",
259
+ "lifecycle_error_code", "remote_status", "lifecycle_sha256",
260
+ "required_locales", "approved_locales", "blocked_locales",
261
+ "queue_counts", "notification_state", "notification_id",
262
+ "notification_sha256", "notification_attempts",
263
+ "notification_max_attempts", "notification_error_code",
264
+ "terminal_processing_state", "terminal_processing_poll_attempts",
265
+ "terminal_processing_failures", "terminal_processing_error_code",
266
+ "receiver_processing_state", "receiver_processing_attempts",
267
+ "receiver_processing_max_attempts", "receiver_processing_error_code",
268
+ "receiver_processed_at",
269
+ }
270
+ if not isinstance(payload, Mapping) or set(payload) != fields:
271
+ raise ValueError
272
+ if payload["schema"] != "blun.cms-source-service-status.v3":
273
+ raise ValueError
274
+ event_id = _token(payload["event_id"])
275
+ site_id = _token(payload["site_id"])
276
+ if event_id != expected_event_id or site_id != expected_site_id:
277
+ raise ValueError
278
+ website_version = _token(payload["website_version"])
279
+ source_sequence = _count(payload["source_sequence"])
280
+ if source_sequence < 1 or SHA256.fullmatch(payload["change_sha256"]) is None:
281
+ raise ValueError
282
+ dispatch_status = payload["dispatch_status"]
283
+ if dispatch_status not in STATUSES:
284
+ raise ValueError
285
+ attempts = _count(payload["dispatch_attempts"], maximum=20)
286
+ maximum = _count(payload["dispatch_max_attempts"], maximum=20)
287
+ if maximum < 1 or attempts > maximum:
288
+ raise ValueError
289
+ dispatch_error = _optional_error(payload["dispatch_error_code"])
290
+ plan_id = _optional_token(payload["plan_id"])
291
+ job_count = payload["job_count"]
292
+ if job_count is not None:
293
+ job_count = _count(job_count)
294
+ if job_count < 1:
295
+ raise ValueError
296
+ lifecycle_state = payload["lifecycle_state"]
297
+ if lifecycle_state is not None and lifecycle_state not in LIFECYCLE_STATES:
298
+ raise ValueError
299
+ poll_attempts = _count(payload["lifecycle_poll_attempts"], maximum=20)
300
+ lifecycle_error = _optional_error(payload["lifecycle_error_code"])
301
+ remote_status = payload["remote_status"]
302
+ if remote_status is not None and remote_status not in REMOTE_STATUSES:
303
+ raise ValueError
304
+ lifecycle_sha256 = payload["lifecycle_sha256"]
305
+ if lifecycle_sha256 is not None and SHA256.fullmatch(lifecycle_sha256) is None:
306
+ raise ValueError
307
+ required = _locales(payload["required_locales"])
308
+ approved = _locales(payload["approved_locales"])
309
+ blocked_value = payload["blocked_locales"]
310
+ if not isinstance(blocked_value, (list, tuple)):
311
+ raise ValueError
312
+ blocked = []
313
+ for item in blocked_value:
314
+ if not isinstance(item, (list, tuple)) or len(item) != 2:
315
+ raise ValueError
316
+ blocked.append([_token(item[0]), _optional_error(item[1])])
317
+ if any(reason is None for _locale, reason in blocked):
318
+ raise ValueError
319
+ blocked_locales = [locale for locale, _reason in blocked]
320
+ if (
321
+ len(blocked) > 24
322
+ or len(set(blocked_locales)) != len(blocked_locales)
323
+ or required != sorted(required)
324
+ or approved != sorted(approved)
325
+ or blocked != sorted(blocked)
326
+ or not set(approved).issubset(required)
327
+ or not set(blocked_locales).issubset(required)
328
+ or set(approved).intersection(blocked_locales)
329
+ ):
330
+ raise ValueError
331
+ queue_value = payload["queue_counts"]
332
+ if not isinstance(queue_value, Mapping) or set(queue_value) not in (
333
+ set(), STATUSES, REMOTE_QUEUE_STATES,
334
+ ):
335
+ raise ValueError
336
+ queue_counts = {
337
+ name: _count(queue_value[name]) for name in sorted(queue_value)
338
+ }
339
+ notification_state = payload["notification_state"]
340
+ if notification_state not in {
341
+ "disabled", "awaiting_terminal", "awaiting_registration",
342
+ "pending", "leased", "retry_wait", "succeeded", "failed",
343
+ }:
344
+ raise ValueError
345
+ notification_id = _optional_token(payload["notification_id"])
346
+ notification_sha256 = payload["notification_sha256"]
347
+ if (
348
+ notification_sha256 is not None
349
+ and SHA256.fullmatch(notification_sha256) is None
350
+ ):
351
+ raise ValueError
352
+ notification_attempts = _count(
353
+ payload["notification_attempts"], maximum=20,
354
+ )
355
+ notification_max_attempts = payload["notification_max_attempts"]
356
+ if notification_max_attempts is not None:
357
+ notification_max_attempts = _count(
358
+ notification_max_attempts, maximum=20,
359
+ )
360
+ notification_error = _optional_error(
361
+ payload["notification_error_code"]
362
+ )
363
+ durable_notification = notification_state in STATUSES
364
+ if durable_notification != (notification_id is not None):
365
+ raise ValueError
366
+ if durable_notification != (notification_sha256 is not None):
367
+ raise ValueError
368
+ if durable_notification != (notification_max_attempts is not None):
369
+ raise ValueError
370
+ if durable_notification and (
371
+ notification_max_attempts < 1
372
+ or notification_attempts > notification_max_attempts
373
+ ):
374
+ raise ValueError
375
+ if not durable_notification and notification_attempts != 0:
376
+ raise ValueError
377
+ if (notification_state in {"retry_wait", "failed"}) != (
378
+ notification_error is not None
379
+ ):
380
+ raise ValueError
381
+ terminal_processing_state = payload["terminal_processing_state"]
382
+ processing_states = {
383
+ "disabled", "awaiting_notification", "awaiting_registration",
384
+ "pending", "leased", "watching", "retry_wait", "succeeded",
385
+ "failed",
386
+ }
387
+ if terminal_processing_state not in processing_states:
388
+ raise ValueError
389
+ terminal_processing_poll_attempts = _count(
390
+ payload["terminal_processing_poll_attempts"]
391
+ )
392
+ terminal_processing_failures = _count(
393
+ payload["terminal_processing_failures"], maximum=20,
394
+ )
395
+ terminal_processing_error = _optional_error(
396
+ payload["terminal_processing_error_code"]
397
+ )
398
+ receiver_processing_state = payload["receiver_processing_state"]
399
+ if (
400
+ receiver_processing_state is not None
401
+ and receiver_processing_state not in {
402
+ "pending", "leased", "retry_wait", "succeeded", "failed",
403
+ }
404
+ ):
405
+ raise ValueError
406
+ receiver_processing_attempts = payload["receiver_processing_attempts"]
407
+ receiver_processing_max_attempts = payload[
408
+ "receiver_processing_max_attempts"
409
+ ]
410
+ if receiver_processing_attempts is not None:
411
+ receiver_processing_attempts = _count(
412
+ receiver_processing_attempts, maximum=20,
413
+ )
414
+ if receiver_processing_max_attempts is not None:
415
+ receiver_processing_max_attempts = _count(
416
+ receiver_processing_max_attempts, maximum=20,
417
+ )
418
+ receiver_processing_error = _optional_error(
419
+ payload["receiver_processing_error_code"]
420
+ )
421
+ receiver_processed_at = payload["receiver_processed_at"]
422
+ if receiver_processed_at is not None:
423
+ receiver_processed_at = _timestamp(receiver_processed_at)
424
+ durable_processing = terminal_processing_state in {
425
+ "pending", "leased", "watching", "retry_wait", "succeeded",
426
+ "failed",
427
+ }
428
+ if not durable_processing and any((
429
+ terminal_processing_poll_attempts,
430
+ terminal_processing_failures,
431
+ terminal_processing_error,
432
+ receiver_processing_state,
433
+ receiver_processing_attempts,
434
+ receiver_processing_max_attempts,
435
+ receiver_processing_error,
436
+ receiver_processed_at,
437
+ )):
438
+ raise ValueError
439
+ if durable_processing and (
440
+ (terminal_processing_state in {"retry_wait", "failed"})
441
+ != (terminal_processing_error is not None)
442
+ ):
443
+ raise ValueError
444
+ if (receiver_processing_state is None) != (
445
+ receiver_processing_attempts is None
446
+ and receiver_processing_max_attempts is None
447
+ ):
448
+ raise ValueError
449
+ if receiver_processing_state is None and any((
450
+ receiver_processing_error,
451
+ receiver_processed_at,
452
+ )):
453
+ raise ValueError
454
+ if (
455
+ receiver_processing_attempts is not None
456
+ and (
457
+ receiver_processing_max_attempts < 1
458
+ or receiver_processing_attempts
459
+ > receiver_processing_max_attempts
460
+ )
461
+ ):
462
+ raise ValueError
463
+ if (receiver_processing_state == "succeeded") != (
464
+ receiver_processed_at is not None
465
+ ):
466
+ raise ValueError
467
+ if (receiver_processing_state in {"retry_wait", "failed"}) != (
468
+ receiver_processing_error is not None
469
+ ):
470
+ raise ValueError
471
+ if (plan_id is None) != (job_count is None):
472
+ raise ValueError
473
+ if (dispatch_status == "succeeded") != (plan_id is not None):
474
+ raise ValueError
475
+ if (dispatch_status in {"retry_wait", "failed"}) != (
476
+ dispatch_error is not None
477
+ ):
478
+ raise ValueError
479
+ if lifecycle_state is None:
480
+ if any((poll_attempts, lifecycle_error, remote_status, lifecycle_sha256)):
481
+ raise ValueError
482
+ if required or approved or blocked or queue_counts:
483
+ raise ValueError
484
+ elif plan_id is None:
485
+ raise ValueError
486
+ if remote_status is None and (
487
+ lifecycle_sha256 is not None
488
+ or required or approved or blocked or queue_counts
489
+ ):
490
+ raise ValueError
491
+ if remote_status is not None and (
492
+ lifecycle_sha256 is None
493
+ or not required
494
+ or job_count != len(required)
495
+ or set(queue_counts) not in (STATUSES, REMOTE_QUEUE_STATES)
496
+ or (
497
+ remote_status == "queue_recovery"
498
+ and sum(queue_counts.values()) != 0
499
+ )
500
+ or (
501
+ remote_status != "queue_recovery"
502
+ and sum(queue_counts.values()) != len(required)
503
+ )
504
+ ):
505
+ raise ValueError
506
+ return {
507
+ "schema": payload["schema"],
508
+ "event_id": event_id,
509
+ "site_id": site_id,
510
+ "website_version": website_version,
511
+ "source_sequence": source_sequence,
512
+ "change_sha256": payload["change_sha256"],
513
+ "dispatch_status": dispatch_status,
514
+ "dispatch_attempts": attempts,
515
+ "dispatch_max_attempts": maximum,
516
+ "dispatch_error_code": dispatch_error,
517
+ "plan_id": plan_id,
518
+ "job_count": job_count,
519
+ "lifecycle_state": lifecycle_state,
520
+ "lifecycle_poll_attempts": poll_attempts,
521
+ "lifecycle_error_code": lifecycle_error,
522
+ "remote_status": remote_status,
523
+ "lifecycle_sha256": lifecycle_sha256,
524
+ "required_locales": required,
525
+ "approved_locales": approved,
526
+ "blocked_locales": blocked,
527
+ "queue_counts": queue_counts,
528
+ "notification_state": notification_state,
529
+ "notification_id": notification_id,
530
+ "notification_sha256": notification_sha256,
531
+ "notification_attempts": notification_attempts,
532
+ "notification_max_attempts": notification_max_attempts,
533
+ "notification_error_code": notification_error,
534
+ "terminal_processing_state": terminal_processing_state,
535
+ "terminal_processing_poll_attempts": (
536
+ terminal_processing_poll_attempts
537
+ ),
538
+ "terminal_processing_failures": terminal_processing_failures,
539
+ "terminal_processing_error_code": terminal_processing_error,
540
+ "receiver_processing_state": receiver_processing_state,
541
+ "receiver_processing_attempts": receiver_processing_attempts,
542
+ "receiver_processing_max_attempts": (
543
+ receiver_processing_max_attempts
544
+ ),
545
+ "receiver_processing_error_code": receiver_processing_error,
546
+ "receiver_processed_at": receiver_processed_at,
547
+ }
548
+ except Exception:
549
+ raise CMSSourceHTTPBlocked(
550
+ "source_http.runtime_response_invalid", 503,
551
+ ) from None
552
+
553
+
554
+ def _component(
555
+ value: Any,
556
+ states: set[str],
557
+ *,
558
+ operations: bool = False,
559
+ remote_failures: bool = False,
560
+ ) -> dict[str, Any]:
561
+ fields = {"status", "counts", "due", "expired_leases", "failed"}
562
+ if operations:
563
+ fields.add("operations")
564
+ if remote_failures:
565
+ fields.add("remote_failures")
566
+ if not isinstance(value, Mapping) or set(value) != fields:
567
+ raise ValueError
568
+ if value["status"] not in {"ok", "blocked"}:
569
+ raise ValueError
570
+ counts = value["counts"]
571
+ if not isinstance(counts, Mapping) or set(counts) != states:
572
+ raise ValueError
573
+ normalized = {
574
+ "status": value["status"],
575
+ "counts": {name: _count(counts[name]) for name in sorted(states)},
576
+ "due": _count(value["due"]),
577
+ "expired_leases": _count(value["expired_leases"]),
578
+ "failed": _count(value["failed"]),
579
+ }
580
+ if operations:
581
+ operation_counts = value["operations"]
582
+ if not isinstance(operation_counts, Mapping) or set(operation_counts) != {
583
+ "cancellation", "tombstone",
584
+ }:
585
+ raise ValueError
586
+ normalized["operations"] = {
587
+ name: _count(operation_counts[name])
588
+ for name in ("cancellation", "tombstone")
589
+ }
590
+ if remote_failures:
591
+ normalized["remote_failures"] = _count(value["remote_failures"])
592
+ return normalized
593
+
594
+
595
+ def _health_payload(value: Any) -> dict[str, Any]:
596
+ try:
597
+ payload = value.as_payload()
598
+ if not isinstance(payload, Mapping) or set(payload) != {
599
+ "schema", "status", "pending_lifecycle_registrations",
600
+ "pending_terminal_notifications", "changes", "removals",
601
+ "pending_terminal_processing", "lifecycle", "notifications",
602
+ "terminal_processing", "error_code",
603
+ }:
604
+ raise ValueError
605
+ if payload["schema"] != "blun.cms-source-service-health.v3":
606
+ raise ValueError
607
+ if payload["status"] not in HEALTH_STATUSES:
608
+ raise ValueError
609
+ result = {
610
+ "schema": payload["schema"],
611
+ "status": payload["status"],
612
+ "pending_lifecycle_registrations": _count(
613
+ payload["pending_lifecycle_registrations"]
614
+ ),
615
+ "pending_terminal_notifications": _count(
616
+ payload["pending_terminal_notifications"]
617
+ ),
618
+ "pending_terminal_processing": _count(
619
+ payload["pending_terminal_processing"]
620
+ ),
621
+ "changes": _component(payload["changes"], STATUSES),
622
+ "removals": _component(
623
+ payload["removals"], STATUSES, operations=True,
624
+ ),
625
+ "lifecycle": _component(payload["lifecycle"], {
626
+ "pending", "leased", "watching", "retry_wait",
627
+ "terminal", "failed",
628
+ }, remote_failures=True),
629
+ "notifications": (
630
+ {}
631
+ if payload["notifications"] == {}
632
+ else _component(payload["notifications"], STATUSES)
633
+ ),
634
+ "terminal_processing": (
635
+ {}
636
+ if payload["terminal_processing"] == {}
637
+ else _component(payload["terminal_processing"], {
638
+ "pending", "leased", "watching", "retry_wait",
639
+ "succeeded", "failed",
640
+ })
641
+ ),
642
+ "error_code": payload["error_code"],
643
+ }
644
+ if result["error_code"] is not None and ERROR_CODE.fullmatch(
645
+ result["error_code"]
646
+ ) is None:
647
+ raise ValueError
648
+ _canonical_json(result)
649
+ return result
650
+ except CMSSourceHTTPBlocked:
651
+ raise
652
+ except Exception:
653
+ raise CMSSourceHTTPBlocked("source_http.health_invalid", 503) from None
654
+
655
+
656
+ def _readiness_payload(value: Any) -> dict[str, Any]:
657
+ try:
658
+ if not isinstance(value, Mapping) or set(value) != {
659
+ "schema", "status", "worker_state", "service_status", "error_code",
660
+ }:
661
+ raise ValueError
662
+ if value["schema"] != "blun.cms-source-worker-readiness.v1":
663
+ raise ValueError
664
+ if value["status"] not in {"ready", "not_ready"}:
665
+ raise ValueError
666
+ if value["worker_state"] not in {
667
+ "unmanaged", "starting", "running", "stopping", "stopped",
668
+ "failed", "closed",
669
+ }:
670
+ raise ValueError
671
+ if value["service_status"] not in {None, "ok", "degraded", "blocked"}:
672
+ raise ValueError
673
+ error_code = value["error_code"]
674
+ if error_code is not None and ERROR_CODE.fullmatch(error_code) is None:
675
+ raise ValueError
676
+ if value["status"] == "ready":
677
+ if (
678
+ value["worker_state"] != "running"
679
+ or value["service_status"] not in {"ok", "degraded"}
680
+ or error_code is not None
681
+ ):
682
+ raise ValueError
683
+ elif error_code is None:
684
+ raise ValueError
685
+ result = dict(value)
686
+ _canonical_json(result)
687
+ return result
688
+ except CMSSourceHTTPBlocked:
689
+ raise
690
+ except Exception:
691
+ raise CMSSourceHTTPBlocked(
692
+ "source_http.readiness_invalid", 503,
693
+ ) from None
694
+
695
+
696
+ def _capabilities_payload() -> dict[str, Any]:
697
+ """Build the public contract from the exact active HTTP constants."""
698
+ expected_scopes = {
699
+ CHANGE_PATH: "source-change:write",
700
+ REMOVAL_PATH: "source-removal:write",
701
+ STATUS_PATH: "source-status:read",
702
+ HEALTH_PATH: "source-health:read",
703
+ READINESS_PATH: "source-readiness:read",
704
+ CAPABILITIES_PATH: "source-capabilities:read",
705
+ }
706
+ expected_methods = {
707
+ CHANGE_PATH: "POST",
708
+ REMOVAL_PATH: "POST",
709
+ STATUS_PATH: "POST",
710
+ HEALTH_PATH: "GET",
711
+ READINESS_PATH: "GET",
712
+ CAPABILITIES_PATH: "GET",
713
+ }
714
+ if (
715
+ SCOPES != expected_scopes
716
+ or METHODS != expected_methods
717
+ or len(set(SCOPES.values())) != len(SCOPES)
718
+ ):
719
+ raise CMSSourceHTTPBlocked(
720
+ "source_http.capabilities_invalid", 503,
721
+ )
722
+
723
+ operations = {
724
+ "capabilities": {
725
+ "method": METHODS[CAPABILITIES_PATH],
726
+ "path": CAPABILITIES_PATH,
727
+ "scope": SCOPES[CAPABILITIES_PATH],
728
+ "principal_schema": PRINCIPAL_SCHEMA,
729
+ "request_schema": None,
730
+ "request_fields": [],
731
+ "response_schema": CAPABILITIES_RESPONSE_SCHEMA,
732
+ "response_fields": ["schema", "capabilities"],
733
+ "success_status": 200,
734
+ },
735
+ "change": {
736
+ "method": METHODS[CHANGE_PATH],
737
+ "path": CHANGE_PATH,
738
+ "scope": SCOPES[CHANGE_PATH],
739
+ "principal_schema": PRINCIPAL_SCHEMA,
740
+ "request_schema": CHANGE_REQUEST_SCHEMA,
741
+ "request_fields": ["schema", "change", "max_attempts"],
742
+ "response_schema": CHANGE_RESPONSE_SCHEMA,
743
+ "response_fields": [
744
+ "schema", "operation", "request_id", "event_id",
745
+ "payload_sha256", "status", "attempts", "max_attempts",
746
+ "capabilities_sha256",
747
+ ],
748
+ "success_status": 202,
749
+ },
750
+ "health": {
751
+ "method": METHODS[HEALTH_PATH],
752
+ "path": HEALTH_PATH,
753
+ "scope": SCOPES[HEALTH_PATH],
754
+ "principal_schema": PRINCIPAL_SCHEMA,
755
+ "request_schema": None,
756
+ "request_fields": [],
757
+ "response_schema": HEALTH_RESPONSE_SCHEMA,
758
+ "response_fields": ["schema", "health", "capabilities_sha256"],
759
+ "success_status": 200,
760
+ },
761
+ "removal": {
762
+ "method": METHODS[REMOVAL_PATH],
763
+ "path": REMOVAL_PATH,
764
+ "scope": SCOPES[REMOVAL_PATH],
765
+ "principal_schema": PRINCIPAL_SCHEMA,
766
+ "request_schema": REMOVAL_REQUEST_SCHEMA,
767
+ "request_fields": ["schema", "removal", "max_attempts"],
768
+ "response_schema": REMOVAL_RESPONSE_SCHEMA,
769
+ "response_fields": [
770
+ "schema", "operation", "request_id", "event_id",
771
+ "payload_sha256", "status", "attempts", "max_attempts",
772
+ "capabilities_sha256",
773
+ ],
774
+ "success_status": 202,
775
+ },
776
+ "readiness": {
777
+ "method": METHODS[READINESS_PATH],
778
+ "path": READINESS_PATH,
779
+ "scope": SCOPES[READINESS_PATH],
780
+ "principal_schema": PRINCIPAL_SCHEMA,
781
+ "request_schema": None,
782
+ "request_fields": [],
783
+ "response_schema": READINESS_RESPONSE_SCHEMA,
784
+ "response_fields": [
785
+ "schema", "readiness", "capabilities_sha256",
786
+ ],
787
+ "success_status": 200,
788
+ },
789
+ "status": {
790
+ "method": METHODS[STATUS_PATH],
791
+ "path": STATUS_PATH,
792
+ "scope": SCOPES[STATUS_PATH],
793
+ "principal_schema": STATUS_PRINCIPAL_SCHEMA,
794
+ "request_schema": STATUS_REQUEST_SCHEMA,
795
+ "request_fields": ["schema", "event_id", "site_id"],
796
+ "response_schema": STATUS_RESPONSE_SCHEMA,
797
+ "response_fields": ["schema", "status", "capabilities_sha256"],
798
+ "success_status": 200,
799
+ },
800
+ }
801
+ capabilities = {
802
+ "schema": CAPABILITIES_SCHEMA,
803
+ "api_schema": API_SCHEMA,
804
+ "authentication_request_schema": AUTH_REQUEST_SCHEMA,
805
+ "error_schema": ERROR_SCHEMA,
806
+ "limits": {
807
+ "max_body_bytes": MAX_BODY_BYTES,
808
+ "max_headers": MAX_HEADERS,
809
+ "max_header_value_bytes": MAX_HEADER_VALUE,
810
+ "max_attempts_min": 1,
811
+ "max_attempts_max": 20,
812
+ },
813
+ "operations": operations,
814
+ }
815
+ try:
816
+ encoded = _canonical_json(capabilities)
817
+ if set(operations) != {
818
+ "capabilities", "change", "health", "readiness", "removal", "status",
819
+ }:
820
+ raise ValueError
821
+ for name, operation in operations.items():
822
+ if (
823
+ operation["path"] not in SCOPES
824
+ or operation["scope"] != SCOPES[operation["path"]]
825
+ or operation["method"] != METHODS[operation["path"]]
826
+ or operation["principal_schema"] not in {
827
+ PRINCIPAL_SCHEMA, STATUS_PRINCIPAL_SCHEMA,
828
+ }
829
+ or not isinstance(operation["success_status"], int)
830
+ or name not in {
831
+ "capabilities", "change", "health", "readiness", "removal",
832
+ "status",
833
+ }
834
+ ):
835
+ raise ValueError
836
+ except CMSSourceHTTPBlocked:
837
+ raise
838
+ except Exception:
839
+ raise CMSSourceHTTPBlocked(
840
+ "source_http.capabilities_invalid", 503,
841
+ ) from None
842
+ return {
843
+ **capabilities,
844
+ "sha256": hashlib.sha256(encoded).hexdigest(),
845
+ }
846
+
847
+
848
+ class CMSSourceHTTPApplication:
849
+ """Strict authenticated WSGI adapter over a source-CMS runtime."""
850
+
851
+ def __init__(self, runtime: Any, authenticator: Callable[[dict[str, Any]], Any]):
852
+ if not all(callable(getattr(runtime, name, None)) for name in (
853
+ "enqueue_change", "enqueue_removal", "status", "health",
854
+ "worker_readiness", "require_worker_ready",
855
+ )):
856
+ raise TypeError("runtime must provide source CMS operations")
857
+ if not callable(authenticator):
858
+ raise TypeError("authenticator must be callable")
859
+ self.runtime = runtime
860
+ self.authenticator = authenticator
861
+
862
+ @staticmethod
863
+ def _headers(environ: Mapping[str, Any]) -> tuple[tuple[str, str], ...]:
864
+ headers = []
865
+ for key, value in environ.items():
866
+ if key.startswith("HTTP_"):
867
+ name = key[5:].replace("_", "-").lower()
868
+ elif key in {"CONTENT_TYPE", "CONTENT_LENGTH"}:
869
+ name = key.replace("_", "-").lower()
870
+ else:
871
+ continue
872
+ if (
873
+ HEADER_NAME.fullmatch(name) is None
874
+ or not isinstance(value, str)
875
+ or len(value) > MAX_HEADER_VALUE
876
+ or "\r" in value
877
+ or "\n" in value
878
+ ):
879
+ raise CMSSourceHTTPBlocked("source_http.headers_invalid", 400)
880
+ headers.append((name, value))
881
+ headers.sort()
882
+ if (
883
+ len(headers) > MAX_HEADERS
884
+ or len({name for name, _ in headers}) != len(headers)
885
+ ):
886
+ raise CMSSourceHTTPBlocked("source_http.headers_invalid", 400)
887
+ return tuple(headers)
888
+
889
+ @staticmethod
890
+ def _content_type(value: Any) -> bool:
891
+ if not isinstance(value, str):
892
+ return False
893
+ parts = [item.strip().lower() for item in value.split(";")]
894
+ return parts in (["application/json"], ["application/json", "charset=utf-8"])
895
+
896
+ @staticmethod
897
+ def _body(environ: Mapping[str, Any], *, required: bool) -> bytes:
898
+ if environ.get("HTTP_TRANSFER_ENCODING") not in {None, ""}:
899
+ raise CMSSourceHTTPBlocked("source_http.transfer_encoding_rejected", 400)
900
+ length = environ.get("CONTENT_LENGTH")
901
+ if not required and length in {None, "", "0"}:
902
+ return b""
903
+ if (
904
+ not isinstance(length, str)
905
+ or not length.isascii()
906
+ or not length.isdecimal()
907
+ ):
908
+ raise CMSSourceHTTPBlocked("source_http.content_length_required", 411)
909
+ size = int(length)
910
+ if size <= 0:
911
+ raise CMSSourceHTTPBlocked("source_http.body_invalid", 400)
912
+ if size > MAX_BODY_BYTES:
913
+ raise CMSSourceHTTPBlocked("source_http.body_too_large", 413)
914
+ stream = environ.get("wsgi.input")
915
+ try:
916
+ body = stream.read(size)
917
+ except Exception:
918
+ body = None
919
+ if not isinstance(body, bytes) or len(body) != size:
920
+ raise CMSSourceHTTPBlocked("source_http.body_invalid", 400)
921
+ return body
922
+
923
+ def _authenticate(
924
+ self,
925
+ environ: Mapping[str, Any],
926
+ path: str,
927
+ body: bytes,
928
+ ) -> dict[str, str]:
929
+ request = {
930
+ "schema": AUTH_REQUEST_SCHEMA,
931
+ "method": environ["REQUEST_METHOD"],
932
+ "path": path,
933
+ "headers": [list(item) for item in self._headers(environ)],
934
+ "body_sha256": hashlib.sha256(body).hexdigest(),
935
+ }
936
+ try:
937
+ principal = self.authenticator(request)
938
+ except Exception:
939
+ raise CMSSourceHTTPBlocked(
940
+ "source_http.authentication_unavailable", 503
941
+ ) from None
942
+ return _principal(principal, SCOPES[path])
943
+
944
+ @staticmethod
945
+ def _send(start_response, status: int, payload: Mapping[str, Any]):
946
+ body = _canonical_json(dict(payload))
947
+ phrases = {
948
+ 200: "OK", 202: "Accepted", 400: "Bad Request",
949
+ 401: "Unauthorized", 403: "Forbidden", 404: "Not Found",
950
+ 405: "Method Not Allowed", 409: "Conflict",
951
+ 411: "Length Required", 413: "Content Too Large",
952
+ 415: "Unsupported Media Type", 503: "Service Unavailable",
953
+ }
954
+ start_response(f"{status} {phrases[status]}", [
955
+ ("Content-Type", "application/json; charset=utf-8"),
956
+ ("Content-Length", str(len(body))),
957
+ ("Cache-Control", "no-store"),
958
+ ("X-Content-Type-Options", "nosniff"),
959
+ ("Referrer-Policy", "no-referrer"),
960
+ ])
961
+ return [body]
962
+
963
+ @classmethod
964
+ def _error(cls, start_response, failure: CMSSourceHTTPBlocked):
965
+ return cls._send(start_response, failure.status, {
966
+ "schema": ERROR_SCHEMA,
967
+ "status": "BLOCK",
968
+ "error_code": failure.code,
969
+ })
970
+
971
+ @staticmethod
972
+ def _request(body: bytes) -> Any:
973
+ try:
974
+ text = body.decode("utf-8")
975
+ if text.startswith("\ufeff"):
976
+ raise ValueError
977
+ return json.loads(
978
+ text,
979
+ object_pairs_hook=_pairs,
980
+ parse_constant=_constant,
981
+ )
982
+ except (UnicodeDecodeError, json.JSONDecodeError, ValueError, RecursionError):
983
+ raise CMSSourceHTTPBlocked("source_http.json_invalid", 400) from None
984
+
985
+ def __call__(self, environ: Mapping[str, Any], start_response: Callable[..., Any]):
986
+ try:
987
+ if not isinstance(environ, Mapping):
988
+ raise CMSSourceHTTPBlocked("source_http.environment_invalid", 400)
989
+ path = environ.get("PATH_INFO")
990
+ if path not in SCOPES:
991
+ raise CMSSourceHTTPBlocked("source_http.route_not_found", 404)
992
+ method = environ.get("REQUEST_METHOD")
993
+ expected_method = METHODS[path]
994
+ if method != expected_method:
995
+ raise CMSSourceHTTPBlocked("source_http.method_not_allowed", 405)
996
+ if environ.get("wsgi.url_scheme") != "https":
997
+ raise CMSSourceHTTPBlocked("source_http.https_required", 400)
998
+ if environ.get("QUERY_STRING") not in {None, ""}:
999
+ raise CMSSourceHTTPBlocked("source_http.query_rejected", 400)
1000
+ if path in {HEALTH_PATH, READINESS_PATH, CAPABILITIES_PATH}:
1001
+ body = self._body(environ, required=False)
1002
+ if body or environ.get("CONTENT_TYPE") not in {None, ""}:
1003
+ raise CMSSourceHTTPBlocked("source_http.body_not_allowed", 400)
1004
+ else:
1005
+ if not self._content_type(environ.get("CONTENT_TYPE")):
1006
+ raise CMSSourceHTTPBlocked("source_http.content_type_invalid", 415)
1007
+ body = self._body(environ, required=True)
1008
+ principal = self._authenticate(environ, path, body)
1009
+
1010
+ if path == CAPABILITIES_PATH:
1011
+ return self._send(start_response, 200, {
1012
+ "schema": CAPABILITIES_RESPONSE_SCHEMA,
1013
+ "capabilities": _capabilities_payload(),
1014
+ })
1015
+
1016
+ if path == HEALTH_PATH:
1017
+ try:
1018
+ health = self.runtime.health()
1019
+ except Exception:
1020
+ raise CMSSourceHTTPBlocked(
1021
+ "source_http.runtime_blocked", 503,
1022
+ ) from None
1023
+ report = _health_payload(health)
1024
+ response_status = 503 if report["status"] == "blocked" else 200
1025
+ return self._send(start_response, response_status, {
1026
+ "schema": HEALTH_RESPONSE_SCHEMA,
1027
+ "health": report,
1028
+ "capabilities_sha256": _capabilities_payload()["sha256"],
1029
+ })
1030
+
1031
+ if path == READINESS_PATH:
1032
+ try:
1033
+ readiness = self.runtime.worker_readiness()
1034
+ except Exception:
1035
+ raise CMSSourceHTTPBlocked(
1036
+ "source_http.runtime_blocked", 503,
1037
+ ) from None
1038
+ report = _readiness_payload(readiness)
1039
+ response_status = 200 if report["status"] == "ready" else 503
1040
+ return self._send(start_response, response_status, {
1041
+ "schema": READINESS_RESPONSE_SCHEMA,
1042
+ "readiness": report,
1043
+ "capabilities_sha256": _capabilities_payload()["sha256"],
1044
+ })
1045
+
1046
+ request = self._request(body)
1047
+ if path == STATUS_PATH:
1048
+ if (
1049
+ not isinstance(request, Mapping)
1050
+ or set(request) != {"schema", "event_id", "site_id"}
1051
+ or request.get("schema") != STATUS_REQUEST_SCHEMA
1052
+ ):
1053
+ raise CMSSourceHTTPBlocked("source_http.request_invalid", 400)
1054
+ try:
1055
+ event_id = _token(request["event_id"])
1056
+ site_id = _token(request["site_id"])
1057
+ except (KeyError, TypeError, ValueError):
1058
+ raise CMSSourceHTTPBlocked(
1059
+ "source_http.request_invalid", 400,
1060
+ ) from None
1061
+ if site_id != principal["site_id"]:
1062
+ raise CMSSourceHTTPBlocked(
1063
+ "source_http.status_not_found", 404,
1064
+ )
1065
+ try:
1066
+ status = self.runtime.status(event_id, site_id)
1067
+ except Exception as error:
1068
+ code = getattr(error, "code", None)
1069
+ if code == "source_runtime.request_invalid":
1070
+ raise CMSSourceHTTPBlocked(
1071
+ "source_http.request_invalid", 400,
1072
+ ) from None
1073
+ if code == "source_runtime.status_not_found":
1074
+ raise CMSSourceHTTPBlocked(
1075
+ "source_http.status_not_found", 404,
1076
+ ) from None
1077
+ raise CMSSourceHTTPBlocked(
1078
+ "source_http.runtime_blocked", 503,
1079
+ ) from None
1080
+ return self._send(start_response, 200, {
1081
+ "schema": STATUS_RESPONSE_SCHEMA,
1082
+ "status": _source_status_payload(
1083
+ status,
1084
+ expected_event_id=event_id,
1085
+ expected_site_id=site_id,
1086
+ ),
1087
+ "capabilities_sha256": _capabilities_payload()["sha256"],
1088
+ })
1089
+
1090
+ envelope_key = "change" if path == CHANGE_PATH else "removal"
1091
+ expected_schema = (
1092
+ CHANGE_REQUEST_SCHEMA if path == CHANGE_PATH else REMOVAL_REQUEST_SCHEMA
1093
+ )
1094
+ if (
1095
+ not isinstance(request, Mapping)
1096
+ or set(request) != {"schema", envelope_key, "max_attempts"}
1097
+ or request.get("schema") != expected_schema
1098
+ ):
1099
+ raise CMSSourceHTTPBlocked("source_http.request_invalid", 400)
1100
+ maximum = request["max_attempts"]
1101
+ if (
1102
+ isinstance(maximum, bool)
1103
+ or not isinstance(maximum, int)
1104
+ or not 1 <= maximum <= 20
1105
+ ):
1106
+ raise CMSSourceHTTPBlocked("source_http.request_invalid", 400)
1107
+ try:
1108
+ self.runtime.require_worker_ready()
1109
+ if path == CHANGE_PATH:
1110
+ status = self.runtime.enqueue_change(
1111
+ request["change"], max_attempts=maximum,
1112
+ )
1113
+ else:
1114
+ status = self.runtime.enqueue_removal(
1115
+ request["removal"], max_attempts=maximum,
1116
+ )
1117
+ except Exception as error:
1118
+ code = getattr(error, "code", None)
1119
+ if code == "source_runtime.request_invalid":
1120
+ raise CMSSourceHTTPBlocked(
1121
+ "source_http.request_invalid", 400,
1122
+ ) from None
1123
+ if code == "source_runtime.idempotency_collision":
1124
+ raise CMSSourceHTTPBlocked(
1125
+ "source_http.idempotency_collision", 409,
1126
+ ) from None
1127
+ if code in {
1128
+ "source_runtime.service_blocked",
1129
+ "source_runtime.database_unsafe",
1130
+ "source_runtime.database_unavailable",
1131
+ "source_runtime.closed",
1132
+ "source_runtime.foreign_process",
1133
+ }:
1134
+ raise CMSSourceHTTPBlocked(
1135
+ "source_http.runtime_blocked", 503,
1136
+ ) from None
1137
+ raise CMSSourceHTTPBlocked("source_http.runtime_blocked", 503) from None
1138
+ payload = _status_payload(status, removal=path == REMOVAL_PATH)
1139
+ return self._send(start_response, 202, {
1140
+ "schema": (
1141
+ CHANGE_RESPONSE_SCHEMA
1142
+ if path == CHANGE_PATH else REMOVAL_RESPONSE_SCHEMA
1143
+ ),
1144
+ **payload,
1145
+ "capabilities_sha256": _capabilities_payload()["sha256"],
1146
+ })
1147
+ except CMSSourceHTTPBlocked as failure:
1148
+ return self._error(start_response, failure)
1149
+ except Exception:
1150
+ return self._error(
1151
+ start_response,
1152
+ CMSSourceHTTPBlocked("source_http.internal", 503),
1153
+ )