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.
- package/CHANGELOG.md +11 -185
- package/LIESMICH.txt +51 -13
- package/README.md +44 -47
- package/agent-spine-plugin/.codex-plugin/plugin.json +16 -4
- package/agent-spine-plugin/CHANGELOG.md +37 -5
- package/agent-spine-plugin/README.md +3 -3
- package/agent-spine-plugin/blun.plugin.json +45 -10
- package/agent-spine-plugin/docs/artifact-evaluation.md +93 -0
- package/agent-spine-plugin/docs/host-integration.md +42 -27
- package/agent-spine-plugin/docs/preflight-recall.md +4 -2
- package/agent-spine-plugin/docs/session-timeline.md +97 -236
- package/agent-spine-plugin/docs/world-model.md +25 -0
- package/agent-spine-plugin/hooks/codex.json +1 -1
- package/agent-spine-plugin/hooks/hooks.json +1 -1
- package/agent-spine-plugin/package.json +1 -3
- package/agent-spine-plugin/scripts/check-hosts.js +3 -3
- package/agent-spine-plugin/scripts/release-check.js +10 -5
- package/agent-spine-plugin/scripts/run-checks.js +4 -1
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +32 -6
- package/agent-spine-plugin/src/cli-learning.js +15 -0
- package/agent-spine-plugin/src/cli.js +2 -0
- package/agent-spine-plugin/src/hook.js +32 -32
- package/agent-spine-plugin/src/lib/action-lesson-recall.js +73 -8
- package/agent-spine-plugin/src/lib/briefing.js +146 -36
- package/agent-spine-plugin/src/lib/channel-continuity.js +19 -0
- package/agent-spine-plugin/src/lib/delivery-agent-usage.js +14 -7
- package/agent-spine-plugin/src/lib/gateway-group-response.js +128 -0
- package/agent-spine-plugin/src/lib/gateway-runs.js +24 -15
- package/agent-spine-plugin/src/lib/hook-briefing-use.js +13 -3
- package/agent-spine-plugin/src/lib/hook-context.js +16 -3
- package/agent-spine-plugin/src/lib/hook-output.js +129 -5
- package/agent-spine-plugin/src/lib/hook-timeline.js +5 -3
- package/agent-spine-plugin/src/lib/indexed-memory.js +2 -2
- package/agent-spine-plugin/src/lib/learning-artifact-evaluator.js +114 -0
- package/agent-spine-plugin/src/lib/learning-context.js +11 -4
- package/agent-spine-plugin/src/lib/learning-measurements.js +2 -2
- package/agent-spine-plugin/src/lib/mcp-runtime.js +89 -3
- package/agent-spine-plugin/src/lib/mcp-source-context.js +12 -2
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +91 -8
- package/agent-spine-plugin/src/lib/mcp-world-tools.js +2 -2
- package/agent-spine-plugin/src/lib/owned-file-lock.js +20 -1
- package/agent-spine-plugin/src/lib/persona-runtime.js +2 -2
- package/agent-spine-plugin/src/lib/preflight-delivery-id.js +27 -0
- package/agent-spine-plugin/src/lib/preflight.js +4 -4
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +15 -0
- package/agent-spine-plugin/src/lib/session-timeline-contract.js +12 -4
- package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +36 -7
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +13 -10
- package/agent-spine-plugin/src/lib/session-timeline-invocation.js +1 -1
- package/agent-spine-plugin/src/lib/session-timeline-king.js +14 -0
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +18 -12
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +5 -0
- package/agent-spine-plugin/src/lib/session-timeline-query.js +2 -0
- package/agent-spine-plugin/src/lib/session-timeline-results.js +35 -10
- package/agent-spine-plugin/src/lib/session-timeline-source-open.js +30 -0
- package/agent-spine-plugin/src/lib/session-timeline.js +122 -75
- package/agent-spine-plugin/src/lib/source-roots.js +3 -2
- package/agent-spine-plugin/src/lib/task-knowledge-context.js +22 -1
- package/agent-spine-plugin/src/lib/timeline-continuation-update.js +100 -0
- package/agent-spine-plugin/src/lib/timeline-tool-guard.js +30 -7
- package/agent-spine-plugin/src/lib/timeline-user-feedback.js +217 -0
- package/agent-spine-plugin/src/lib/timeline-world-capture.js +233 -0
- package/agent-spine-plugin/src/lib/world-knowledge.js +59 -2
- package/agent-spine-plugin/src/lib/world-model.js +64 -9
- package/agent-spine-plugin/src/worker.js +13 -1
- package/bin/blun.js +43 -28
- package/bin/core-bootstrap.js +5 -4
- package/bin/king.js +43 -28
- package/bin/launcher-mode.js +1 -10
- package/bin/launcher-runtime.js +128 -295
- package/bin/managed-node.js +0 -0
- package/bin/managed-plugin-selection.cjs +0 -1
- package/bin/native-module-repair.js +0 -0
- package/bin/node-runtime.js +0 -0
- package/bin/node-version.js +0 -0
- package/bin/plugin-bootstrap.js +56 -120
- package/bin/private-paths.js +11 -34
- package/bin/standard-tools-bootstrap.js +34 -114
- package/bin/turn-thinking-policy.cjs +3 -11
- package/bin/update-copy.js +200 -0
- package/bin/update-lease.js +0 -0
- package/bin/update-notice.js +136 -289
- package/bin/verify-agent-behavior.cjs +122 -0
- package/bin/verify-agent-components.cjs +104 -0
- package/bin/verify-bundled-agent-sources.cjs +57 -0
- package/blun.mjs +143076 -135288
- package/bundled-agent-sources.json +701 -0
- package/package.json +12 -15
- package/standard-skills/translate-native/README.md +1293 -0
- package/standard-skills/translate-native/SKILL.md +172 -22
- package/standard-skills/translate-native/VERSION +1 -1
- package/standard-skills/translate-native/agents/openai.yaml +18 -0
- package/standard-skills/translate-native/assets/icon.svg +8 -0
- package/standard-skills/translate-native/docs/BLUN_CODE_INTEGRATION.md +76 -0
- package/standard-skills/translate-native/docs/PREMORTEM.md +489 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +2035 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1302 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +136 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HEALTH_HTTP.md +130 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +175 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +86 -0
- package/standard-skills/translate-native/integrations/AGENT_RULES.md +32 -0
- package/standard-skills/translate-native/integrations/adapters/blun-code-language-guard.js +514 -0
- package/standard-skills/translate-native/integrations/adapters/node-language-guard.js +230 -0
- package/standard-skills/translate-native/integrations/audit_log.py +327 -0
- package/standard-skills/translate-native/integrations/claude_language_hook.js +1536 -0
- package/standard-skills/translate-native/integrations/commercial_localization_profile.py +42 -0
- package/standard-skills/translate-native/integrations/delivery-policy.example.json +28 -0
- package/standard-skills/translate-native/integrations/enforced_delivery.py +543 -0
- package/standard-skills/translate-native/integrations/guard_service.py +435 -0
- package/standard-skills/translate-native/integrations/language_gateway.py +67 -0
- package/standard-skills/translate-native/integrations/mcp_auth_headers.py +198 -0
- package/standard-skills/translate-native/integrations/mcp_http_gateway.py +429 -0
- package/standard-skills/translate-native/integrations/non_language_html_entities.js +1485 -0
- package/standard-skills/translate-native/integrations/pre_output_guard.py +65 -0
- package/standard-skills/translate-native/integrations/task_router.py +101 -0
- package/standard-skills/translate-native/integrations/website_localization.py +401 -0
- package/standard-skills/translate-native/integrations/website_localization_api.py +581 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark.py +1885 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_campaign.py +1772 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_candidate.py +506 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_http.py +400 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_review_store.py +781 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_reviewer_http.py +500 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_runtime.py +1107 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_suite.py +463 -0
- package/standard-skills/translate-native/integrations/website_localization_cms.py +2835 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_client.py +875 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_dispatch.py +805 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_http.py +588 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_lifecycle_monitor.py +991 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +1441 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +414 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +1073 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_removal_dispatch.py +865 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +583 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +964 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +665 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +1153 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +675 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +1125 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification.py +674 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +444 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +1469 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +1142 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +634 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +804 -0
- package/standard-skills/translate-native/integrations/website_localization_deepl_baseline.py +922 -0
- package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +482 -0
- package/standard-skills/translate-native/integrations/website_localization_health.py +1541 -0
- package/standard-skills/translate-native/integrations/website_localization_health_http.py +372 -0
- package/standard-skills/translate-native/integrations/website_localization_http_provider.py +297 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_http.py +479 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_intake.py +363 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_queue.py +1449 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_store.py +420 -0
- package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +235 -0
- package/standard-skills/translate-native/integrations/website_localization_queue.py +671 -0
- package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +516 -0
- package/standard-skills/translate-native/integrations/website_localization_release.py +928 -0
- package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +1008 -0
- package/standard-skills/translate-native/integrations/website_localization_runner.py +276 -0
- package/standard-skills/translate-native/integrations/website_localization_runtime.py +862 -0
- package/standard-skills/translate-native/integrations/website_localization_service.py +350 -0
- package/standard-skills/translate-native/integrations/website_localization_supervisor.py +511 -0
- package/standard-skills/translate-native/integrations/website_localization_worker.py +663 -0
- package/standard-skills/translate-native/provenance.json +3 -4
- package/standard-skills/translate-native/references/commercial-localization.md +177 -0
- package/standard-skills/translate-native/scripts/blun_language_guard.py +7 -1
- package/standard-skills/translate-native/scripts/check_commercial_review.py +80 -0
- package/standard-skills/translate-native/scripts/commercial_localization_profile.py +333 -0
- package/standard-tools/language-guard/LICENSE +21 -0
- package/standard-tools/language-guard/VERSION +1 -0
- package/standard-tools/language-guard/blun_language_guard.py +7 -1
- package/standard-tools/language-guard/check_commercial_review.py +80 -0
- package/standard-tools/language-guard/commercial_localization_profile.py +333 -0
- package/standard-tools/language-guard/language_gateway.py +62 -0
- package/standard-tools/language-guard/pre_output_guard.py +64 -0
- package/standard-tools/language-guard/provenance.json +4 -11
- package/standard-tools/manifest.json +34 -11
- package/telegram-plugin/commands/access.md +2 -10
- package/telegram-plugin/dist/bridge.mjs +64041 -687
- package/telegram-plugin/dist/mcp-server.mjs +72810 -9027
- package/telegram-plugin/dist/noise.mjs +28 -63511
- package/agent-spine-plugin/CONTRIBUTING.md +0 -52
- package/agent-spine-plugin/SECURITY.md +0 -47
- package/agent-spine-plugin/docs/assignment-continuation.md +0 -48
- package/agent-spine-plugin/docs/releasing.md +0 -85
- package/agent-spine-plugin/docs/structured-completion.md +0 -67
- package/bin/abort-listener-policy.cjs +0 -43
- package/bin/active-steer-priority-policy.cjs +0 -24
- package/bin/agent-api-http-adapter.mjs +0 -446
- package/bin/agent-api-private-http-server.mjs +0 -288
- package/bin/agent-api-runtime.mjs +0 -252
- package/bin/agent-api-service-environment.mjs +0 -236
- package/bin/agent-api-service-host.mjs +0 -209
- package/bin/agent-api-service-process.mjs +0 -171
- package/bin/agent-api-session-registry.mjs +0 -428
- package/bin/agent-api-tool-broker.cjs +0 -248
- package/bin/agent-api-turn-controller.mjs +0 -461
- package/bin/agent-api-usage-journal.cjs +0 -259
- package/bin/agent-resume-snapshot.cjs +0 -241
- package/bin/agentspine-king-goal-inbox.mjs +0 -111
- package/bin/agentspine-king-goal-intake.mjs +0 -106
- package/bin/approval-rejection-stop.cjs +0 -15
- package/bin/assistant-message-offload-policy.cjs +0 -284
- package/bin/baseline-skill-performance-policy.cjs +0 -39
- package/bin/bash-search-scope-policy.cjs +0 -49
- package/bin/codebase-search-runtime.cjs +0 -23
- package/bin/cognitive-action-checkpoint.cjs +0 -1104
- package/bin/cognitive-attention-delivery.cjs +0 -76
- package/bin/cognitive-attention-policy.cjs +0 -143
- package/bin/cognitive-attention-runtime.cjs +0 -91
- package/bin/cognitive-context-projection.cjs +0 -73
- package/bin/cognitive-cross-portal-acceptance.cjs +0 -443
- package/bin/cognitive-effective-view.cjs +0 -77
- package/bin/cognitive-focus-projection.cjs +0 -206
- package/bin/cognitive-focus-scope.cjs +0 -37
- package/bin/cognitive-goal-autostart-policy.cjs +0 -72
- package/bin/cognitive-goal-time-trigger-controller.cjs +0 -146
- package/bin/cognitive-memory-adapter.cjs +0 -282
- package/bin/cognitive-memory-command.cjs +0 -293
- package/bin/cognitive-memory-provider.cjs +0 -92
- package/bin/cognitive-salience-policy.cjs +0 -159
- package/bin/cognitive-state-store.cjs +0 -508
- package/bin/cognitive-turn-lifecycle.cjs +0 -624
- package/bin/cognitive-work-focus.cjs +0 -180
- package/bin/compaction-history-archive.cjs +0 -166
- package/bin/compaction-history-startup.cjs +0 -50
- package/bin/compaction-model-policy.cjs +0 -31
- package/bin/compaction-stage-policy.cjs +0 -21
- package/bin/compaction-transaction-policy.cjs +0 -122
- package/bin/config-write-dedup-policy.cjs +0 -27
- package/bin/context-budget-ledger.cjs +0 -31
- package/bin/context-doctor-policy.cjs +0 -70
- package/bin/context-insight-policy.cjs +0 -36
- package/bin/context-performance-policy.cjs +0 -19
- package/bin/context-pressure-policy.cjs +0 -20
- package/bin/cron-run-output.cjs +0 -45
- package/bin/cron-run-store.cjs +0 -145
- package/bin/curiosity-scout-policy.cjs +0 -49
- package/bin/default-model-output-budget-policy.cjs +0 -28
- package/bin/durable-task-resume-policy.cjs +0 -130
- package/bin/durable-task-resume-runtime.cjs +0 -117
- package/bin/durable-task-resume-store.cjs +0 -88
- package/bin/editable-tool-approval-policy.cjs +0 -540
- package/bin/editable-tool-approval-runtime.cjs +0 -99
- package/bin/effective-system-prompt-cache-policy.cjs +0 -33
- package/bin/error-memory-performance-policy.cjs +0 -113
- package/bin/file-observation-policy.cjs +0 -133
- package/bin/foreground-output-capture-policy.cjs +0 -41
- package/bin/generated-source-health.cjs +0 -142
- package/bin/glob-pattern-policy.cjs +0 -13
- package/bin/goal-completion-evidence-policy.cjs +0 -120
- package/bin/grep-output-limit-policy.cjs +0 -39
- package/bin/historical-media-projection-policy.cjs +0 -48
- package/bin/history-offload-pressure-policy.cjs +0 -33
- package/bin/html-to-research-markdown.cjs +0 -147
- package/bin/identity-context-policy.cjs +0 -764
- package/bin/identity-journal-policy.cjs +0 -107
- package/bin/input-draft-persistence.cjs +0 -77
- package/bin/king-tui-function-contract.json +0 -33
- package/bin/launcher-restart-policy.cjs +0 -150
- package/bin/live-response-repetition-guard.cjs +0 -196
- package/bin/llm-config-log-dedup-policy.cjs +0 -76
- package/bin/loop-event-record-policy.cjs +0 -174
- package/bin/managed-context-startup-policy.cjs +0 -27
- package/bin/media-activity-layout-policy.cjs +0 -34
- package/bin/media-auto-retrieval-policy.cjs +0 -90
- package/bin/media-result-policy.cjs +0 -59
- package/bin/micro-compaction-policy.cjs +0 -145
- package/bin/mistake-relevance-policy.cjs +0 -319
- package/bin/model-retry-progress-policy.cjs +0 -46
- package/bin/native-large-file-io.cjs +0 -42
- package/bin/native-runtime-cache.cjs +0 -76
- package/bin/natural-presence-policy.cjs +0 -28
- package/bin/noninteractive-shell-env-policy.cjs +0 -19
- package/bin/observer-hooks.cjs +0 -14
- package/bin/outbound-claim-provenance.cjs +0 -150
- package/bin/oversized-context-offload-policy.cjs +0 -86
- package/bin/pending-media-policy.cjs +0 -182
- package/bin/pending-token-estimate-policy.cjs +0 -41
- package/bin/personal-memory-consent-policy.cjs +0 -72
- package/bin/personal-memory-performance-policy.cjs +0 -12
- package/bin/personality-choice-policy.cjs +0 -101
- package/bin/personality-memory-adapter.cjs +0 -379
- package/bin/personality-mode.cjs +0 -46
- package/bin/personality-setup-policy.cjs +0 -197
- package/bin/proactive-compaction-policy.cjs +0 -25
- package/bin/profile-identity-resolution.cjs +0 -136
- package/bin/profile-runtime.cjs +0 -318
- package/bin/profile-tool-exclusion-policy.cjs +0 -37
- package/bin/programmatic-context-isolation.cjs +0 -25
- package/bin/programmatic-tool-runtime.mjs +0 -627
- package/bin/provider-idle-timeout-policy.cjs +0 -14
- package/bin/provider-model-refresh-deadline.cjs +0 -53
- package/bin/provider-model-refresh-policy.cjs +0 -107
- package/bin/rate-limit-recovery-policy.cjs +0 -47
- package/bin/read-batch-policy.cjs +0 -32
- package/bin/read-continuation-policy.cjs +0 -59
- package/bin/recurring-cron-history-policy.cjs +0 -124
- package/bin/relationship-continuity-policy.cjs +0 -143
- package/bin/relationship-curiosity-policy.cjs +0 -107
- package/bin/relationship-learning-policy.cjs +0 -168
- package/bin/release-artifact-freeze-policy.cjs +0 -30
- package/bin/reload-plugin-bootstrap.cjs +0 -18
- package/bin/reload-queue-policy.cjs +0 -38
- package/bin/repeated-assistant-response-policy.cjs +0 -232
- package/bin/repeated-injection-projection.cjs +0 -107
- package/bin/repeated-user-message-projection.cjs +0 -8
- package/bin/research-page-result.cjs +0 -74
- package/bin/retry-checkpoint-policy.cjs +0 -13
- package/bin/runtime-exit-ledger.cjs +0 -144
- package/bin/scoped-cron-run-policy.cjs +0 -358
- package/bin/session-checkpoint-policy.cjs +0 -25
- package/bin/session-compaction-policy.cjs +0 -84
- package/bin/session-replay-policy.cjs +0 -20
- package/bin/session-replay-window-policy.cjs +0 -40
- package/bin/session-resume-checkpoint.cjs +0 -254
- package/bin/session-scrollback-archive.cjs +0 -229
- package/bin/skill-activation-performance-policy.cjs +0 -69
- package/bin/skill-listing-performance-policy.cjs +0 -92
- package/bin/soul-organization-policy.cjs +0 -78
- package/bin/soul-preservation-policy.cjs +0 -20
- package/bin/startup-preferences.cjs +0 -131
- package/bin/streaming-flush-performance-policy.cjs +0 -28
- package/bin/structured-agent-swarm-output.cjs +0 -325
- package/bin/structured-subagent-output.cjs +0 -252
- package/bin/subagent-context-fork-policy.cjs +0 -155
- package/bin/subagent-max-tokens-handoff-policy.cjs +0 -69
- package/bin/subagent-parent-responsiveness.cjs +0 -19
- package/bin/subagent-skill-policy.cjs +0 -206
- package/bin/subagent-timeout-policy.cjs +0 -182
- package/bin/subagent-tool-policy.cjs +0 -60
- package/bin/subagent-usage-rollup-policy.cjs +0 -29
- package/bin/system-prompt-context-policy.cjs +0 -124
- package/bin/system-prompt-token-cache-policy.cjs +0 -60
- package/bin/telegram-addressed-focus.cjs +0 -55
- package/bin/telegram-addressed-priority.cjs +0 -12
- package/bin/telegram-approval-relay.cjs +0 -290
- package/bin/telegram-bot-priority.cjs +0 -17
- package/bin/telegram-console-status-policy.cjs +0 -174
- package/bin/telegram-context-projection-policy.cjs +0 -141
- package/bin/telegram-delivery-lifecycle.cjs +0 -125
- package/bin/telegram-direct-focus-policy.cjs +0 -273
- package/bin/telegram-mcp-compatibility.cjs +0 -49
- package/bin/telegram-media-delivery-policy.cjs +0 -42
- package/bin/telegram-private-conversation-policy.cjs +0 -185
- package/bin/telegram-queue-handoff-policy.cjs +0 -73
- package/bin/telegram-remote-status-policy.cjs +0 -120
- package/bin/telegram-session-queue-runtime.mjs +0 -306
- package/bin/telegram-text-chunk-policy.cjs +0 -63
- package/bin/telegram-truncated-reply-policy.cjs +0 -37
- package/bin/telegram-urgent-policy.cjs +0 -45
- package/bin/telemetry-spool-policy.cjs +0 -57
- package/bin/thinking-activity-status-policy.cjs +0 -132
- package/bin/thinking-only-guard.cjs +0 -80
- package/bin/todo-list-turn-policy.cjs +0 -131
- package/bin/tool-call-loop-policy.cjs +0 -51
- package/bin/tool-file-persistence.cjs +0 -141
- package/bin/tool-result-offload-policy.cjs +0 -359
- package/bin/tool-result-offload-telemetry.cjs +0 -12
- package/bin/tool-schema-token-cache-policy.cjs +0 -41
- package/bin/tool-stream-preview-policy.cjs +0 -9
- package/bin/tui-functional-contract.cjs +0 -55
- package/bin/turn-tool-performance-policy.cjs +0 -486
- package/bin/usage-cache-efficiency-policy.cjs +0 -26
- package/bin/user-home-path-policy.cjs +0 -13
- package/bin/user-message-offload-policy.cjs +0 -103
- package/bin/user-prompt-hook-origin-policy.cjs +0 -34
- package/bin/user-tool-record-policy.cjs +0 -7
- package/bin/validated-learning-insight-policy.cjs +0 -58
- package/bin/validated-learning-outcome-trace.cjs +0 -107
- package/bin/validated-learning-performance-policy.cjs +0 -53
- package/bin/validated-learning-signal.cjs +0 -463
- package/bin/windows-bash-dialect-policy.cjs +0 -25
- package/bin/windows-node-crash-dump.cjs +0 -110
- package/bin/write-continuation-policy.cjs +0 -69
- package/codebase-index/README.md +0 -82
- package/codebase-index/codebase_index.py +0 -470
- package/standard-skills/agent-browser/SKILL.md +0 -19
- package/standard-skills/agent-browser/references/runtime.md +0 -8
- package/standard-skills/blun-session-inspector/SKILL.md +0 -41
- package/standard-skills/blun-session-inspector/scripts/inspect-session.cjs +0 -437
- package/standard-skills/design-taste-frontend/SKILL.md +0 -1206
- package/standard-skills/full-output-enforcement/SKILL.md +0 -49
- package/standard-skills/high-end-visual-design/SKILL.md +0 -98
- package/standard-skills/image-to-code/SKILL.md +0 -1228
- package/standard-skills/industrial-brutalist-ui/SKILL.md +0 -92
- package/standard-skills/minimalist-ui/SKILL.md +0 -85
- package/standard-skills/motion-design-taste/SKILL.md +0 -74
- package/standard-skills/playwright-testing/SKILL.md +0 -19
- package/standard-skills/playwright-testing/references/runtime.md +0 -7
- package/standard-skills/premortem/SKILL.md +0 -148
- package/standard-skills/redesign-existing-projects/SKILL.md +0 -178
- package/standard-skills/research-evidence/SKILL.md +0 -39
- package/standard-skills/research-evidence/references/evidence-format.md +0 -104
- package/standard-skills/research-evidence/scripts/evidence-collection.cjs +0 -260
- package/standard-skills/research-evidence/scripts/score-report.cjs +0 -130
- package/standard-skills/screenshot-lesen/SKILL.md +0 -52
- package/standard-skills/stitch-design-taste/DESIGN.md +0 -121
- package/standard-skills/stitch-design-taste/SKILL.md +0 -184
- package/standard-skills/telegram-channel/SKILL.md +0 -18
- package/standard-skills/telegram-channel/references/runtime.md +0 -7
- package/standard-skills/venture-flywheel/SKILL.md +0 -32
- package/standard-skills/venture-flywheel/identity/project-identity.cjs +0 -146
- package/standard-skills/venture-flywheel/policy/capability-engine.cjs +0 -114
- package/standard-skills/venture-flywheel/policy/repository-trust.cjs +0 -229
- package/standard-skills/venture-flywheel/references/BEISPIELE-phase0.md +0 -146
- package/standard-skills/venture-flywheel/references/CAPABILITY-MAP.md +0 -34
- package/standard-skills/venture-flywheel/references/SPEC-phase0-identity-trust.md +0 -77
- package/standard-skills/venture-flywheel/references/SPEC-phase0-state-events.md +0 -93
- package/standard-skills/venture-flywheel/schemas/capability-decision.schema.json +0 -13
- package/standard-skills/venture-flywheel/schemas/execution-event.schema.json +0 -44
- package/standard-skills/venture-flywheel/schemas/project-identity.schema.json +0 -32
- package/standard-skills/venture-flywheel/schemas/repository-trust.schema.json +0 -57
- package/standard-skills/venture-flywheel/schemas/run-transition.schema.json +0 -59
- package/standard-skills/venture-flywheel/state/execution-event.cjs +0 -191
- package/standard-skills/venture-flywheel/state/task-state-machine.cjs +0 -190
- package/standard-skills/web-lesen/SKILL.md +0 -73
- package/standard-skills/web-lesen/scripts/crawl_public.py +0 -379
- package/standard-skills/windows-mcp/SKILL.md +0 -19
- package/standard-skills/windows-mcp/references/runtime.md +0 -9
- package/telegram-plugin/DELIVERY.md +0 -36
- package/telegram-plugin/bin/telegram-approval-relay.cjs +0 -290
- package/telegram-plugin/bin/telegram-console-status-policy.cjs +0 -175
- package/telegram-plugin/bin/telegram-delivery-lifecycle.cjs +0 -125
- package/telegram-plugin/bin/telegram-direct-reply-policy.cjs +0 -48
- package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +0 -122
- package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +0 -186
- package/telegram-plugin/bin/telegram-remote-status-policy.cjs +0 -121
- package/telegram-plugin/bin/telegram-reply-parts.cjs +0 -149
- package/telegram-plugin/bin/telegram-text-chunk-policy.cjs +0 -63
- package/telegram-plugin/bin/telegram-typing-keepalive.cjs +0 -89
- package/telegram-plugin/compat/mcp-server-fa511cd1.mjs +0 -73825
- /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
|
+
)
|