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
package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py
ADDED
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Durable SQLite host callbacks for the fail-closed CMS receiver.
|
|
3
|
+
|
|
4
|
+
The trusted host registers its current source and tombstone expectations. The
|
|
5
|
+
receiver invokes this store only after transport and signature verification.
|
|
6
|
+
Every callback rechecks the current binding inside one SQLite write transaction
|
|
7
|
+
so a concurrent source change cannot publish or delete the wrong generation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sqlite3
|
|
16
|
+
import time
|
|
17
|
+
import unicodedata
|
|
18
|
+
from contextlib import contextmanager
|
|
19
|
+
from typing import Any, Callable, Iterator, Mapping
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SCHEMA_VERSION = 1
|
|
23
|
+
MAX_JSON_BYTES = 4_000_000
|
|
24
|
+
TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
|
|
25
|
+
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
26
|
+
CONTENT_TYPES = {
|
|
27
|
+
"headline", "cta", "marketing", "ui", "documentation", "seo", "legal",
|
|
28
|
+
"commercial",
|
|
29
|
+
}
|
|
30
|
+
PUBLICATION_EXPECTATION_FIELDS = (
|
|
31
|
+
"event_id", "site_id", "website_version", "plan_id", "source_id",
|
|
32
|
+
"source_revision", "source_sequence", "source_sha256", "required_locales",
|
|
33
|
+
"content_type", "commercial_profile",
|
|
34
|
+
)
|
|
35
|
+
TOMBSTONE_EXPECTATION_FIELDS = (
|
|
36
|
+
"tombstone_id", "event_id", "site_id", "website_version", "plan_id",
|
|
37
|
+
"source_id", "source_sequence", "publication_delivery_id",
|
|
38
|
+
"publication_payload_sha256", "locales",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CMSReceiverStoreBlocked(RuntimeError):
|
|
43
|
+
"""A private durable-host failure; the HTTP receiver redacts its detail."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _canonical_json(value: Any) -> str:
|
|
47
|
+
try:
|
|
48
|
+
encoded = json.dumps(
|
|
49
|
+
value,
|
|
50
|
+
ensure_ascii=False,
|
|
51
|
+
allow_nan=False,
|
|
52
|
+
sort_keys=True,
|
|
53
|
+
separators=(",", ":"),
|
|
54
|
+
)
|
|
55
|
+
except (TypeError, ValueError, RecursionError) as error:
|
|
56
|
+
raise CMSReceiverStoreBlocked("store JSON is invalid") from error
|
|
57
|
+
raw = encoded.encode("utf-8")
|
|
58
|
+
if not raw or len(raw) > MAX_JSON_BYTES:
|
|
59
|
+
raise CMSReceiverStoreBlocked("store JSON is outside the supported size")
|
|
60
|
+
return encoded
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _decode_json(value: Any, *, field: str) -> Any:
|
|
64
|
+
if not isinstance(value, str) or len(value.encode("utf-8")) > MAX_JSON_BYTES:
|
|
65
|
+
raise CMSReceiverStoreBlocked(f"{field} is invalid")
|
|
66
|
+
try:
|
|
67
|
+
decoded = json.loads(value)
|
|
68
|
+
except (json.JSONDecodeError, RecursionError) as error:
|
|
69
|
+
raise CMSReceiverStoreBlocked(f"{field} is invalid") from error
|
|
70
|
+
if _canonical_json(decoded) != value:
|
|
71
|
+
raise CMSReceiverStoreBlocked(f"{field} is not canonical")
|
|
72
|
+
return decoded
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _mapping(value: Any, fields: tuple[str, ...], *, name: str) -> dict[str, Any]:
|
|
76
|
+
if isinstance(value, Mapping):
|
|
77
|
+
result = dict(value)
|
|
78
|
+
if set(result) != set(fields):
|
|
79
|
+
raise CMSReceiverStoreBlocked(f"{name} fields are invalid")
|
|
80
|
+
return result
|
|
81
|
+
missing = object()
|
|
82
|
+
result = {field: getattr(value, field, missing) for field in fields}
|
|
83
|
+
if any(item is missing for item in result.values()):
|
|
84
|
+
raise CMSReceiverStoreBlocked(f"{name} is invalid")
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _token(value: Any, *, field: str) -> str:
|
|
89
|
+
if (
|
|
90
|
+
not isinstance(value, str)
|
|
91
|
+
or TOKEN.fullmatch(value) is None
|
|
92
|
+
or not unicodedata.is_normalized("NFC", value)
|
|
93
|
+
):
|
|
94
|
+
raise CMSReceiverStoreBlocked(f"{field} is invalid")
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _sha256(value: Any, *, field: str) -> str:
|
|
99
|
+
if not isinstance(value, str) or SHA256.fullmatch(value) is None:
|
|
100
|
+
raise CMSReceiverStoreBlocked(f"{field} is invalid")
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _sequence(value: Any) -> int:
|
|
105
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
106
|
+
raise CMSReceiverStoreBlocked("source_sequence is invalid")
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _locales(value: Any, *, field: str) -> tuple[str, ...]:
|
|
111
|
+
if isinstance(value, list):
|
|
112
|
+
value = tuple(value)
|
|
113
|
+
if (
|
|
114
|
+
not isinstance(value, tuple)
|
|
115
|
+
or not value
|
|
116
|
+
or value != tuple(sorted(set(value)))
|
|
117
|
+
or any(_token(locale, field=field) != locale for locale in value)
|
|
118
|
+
):
|
|
119
|
+
raise CMSReceiverStoreBlocked(f"{field} is invalid")
|
|
120
|
+
return value
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _timestamp(value: Any) -> float:
|
|
124
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
125
|
+
raise CMSReceiverStoreBlocked("clock returned an invalid timestamp")
|
|
126
|
+
result = float(value)
|
|
127
|
+
if result < 0 or result != result or result in {float("inf"), float("-inf")}:
|
|
128
|
+
raise CMSReceiverStoreBlocked("clock returned an invalid timestamp")
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _binding_sha256(value: Mapping[str, Any]) -> str:
|
|
133
|
+
return hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _publication_expectation(value: Any) -> dict[str, Any]:
|
|
137
|
+
result = _mapping(
|
|
138
|
+
value, PUBLICATION_EXPECTATION_FIELDS, name="publication expectation",
|
|
139
|
+
)
|
|
140
|
+
for field in (
|
|
141
|
+
"event_id", "site_id", "website_version", "plan_id", "source_id",
|
|
142
|
+
"source_revision",
|
|
143
|
+
):
|
|
144
|
+
result[field] = _token(result[field], field=field)
|
|
145
|
+
result["source_sequence"] = _sequence(result["source_sequence"])
|
|
146
|
+
result["source_sha256"] = _sha256(
|
|
147
|
+
result["source_sha256"], field="source_sha256",
|
|
148
|
+
)
|
|
149
|
+
result["required_locales"] = _locales(
|
|
150
|
+
result["required_locales"], field="required_locales",
|
|
151
|
+
)
|
|
152
|
+
result["content_type"] = _token(result["content_type"], field="content_type")
|
|
153
|
+
if result["content_type"] not in CONTENT_TYPES:
|
|
154
|
+
raise CMSReceiverStoreBlocked("content_type is invalid")
|
|
155
|
+
commercial = result["commercial_profile"]
|
|
156
|
+
if commercial is not None:
|
|
157
|
+
commercial = _token(commercial, field="commercial_profile")
|
|
158
|
+
if (result["content_type"] == "commercial") != (commercial is not None):
|
|
159
|
+
raise CMSReceiverStoreBlocked("commercial profile scope is invalid")
|
|
160
|
+
result["commercial_profile"] = commercial
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _tombstone_expectation(value: Any) -> dict[str, Any]:
|
|
165
|
+
result = _mapping(
|
|
166
|
+
value, TOMBSTONE_EXPECTATION_FIELDS, name="tombstone expectation",
|
|
167
|
+
)
|
|
168
|
+
for field in (
|
|
169
|
+
"tombstone_id", "event_id", "site_id", "website_version", "plan_id",
|
|
170
|
+
"source_id", "publication_delivery_id",
|
|
171
|
+
):
|
|
172
|
+
result[field] = _token(result[field], field=field)
|
|
173
|
+
result["source_sequence"] = _sequence(result["source_sequence"])
|
|
174
|
+
result["publication_payload_sha256"] = _sha256(
|
|
175
|
+
result["publication_payload_sha256"],
|
|
176
|
+
field="publication_payload_sha256",
|
|
177
|
+
)
|
|
178
|
+
result["locales"] = _locales(result["locales"], field="locales")
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _verified(value: Any, *, kind: str) -> tuple[str, str, dict[str, Any], str]:
|
|
183
|
+
delivery_id = _token(getattr(value, "delivery_id", None), field="delivery_id")
|
|
184
|
+
payload_sha256 = _sha256(
|
|
185
|
+
getattr(value, "payload_sha256", None), field="payload_sha256",
|
|
186
|
+
)
|
|
187
|
+
payload = getattr(value, "payload", None)
|
|
188
|
+
if not isinstance(payload, dict):
|
|
189
|
+
raise CMSReceiverStoreBlocked(f"verified {kind} payload is invalid")
|
|
190
|
+
payload_json = _canonical_json(payload)
|
|
191
|
+
if hashlib.sha256(payload_json.encode("utf-8")).hexdigest() != payload_sha256:
|
|
192
|
+
raise CMSReceiverStoreBlocked(f"verified {kind} hash is invalid")
|
|
193
|
+
if payload.get("delivery_id") != delivery_id:
|
|
194
|
+
raise CMSReceiverStoreBlocked(f"verified {kind} delivery is invalid")
|
|
195
|
+
return delivery_id, payload_sha256, payload, payload_json
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@contextmanager
|
|
199
|
+
def _transaction(connection: sqlite3.Connection) -> Iterator[None]:
|
|
200
|
+
if connection.in_transaction:
|
|
201
|
+
raise CMSReceiverStoreBlocked("store cannot join an external transaction")
|
|
202
|
+
try:
|
|
203
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
204
|
+
yield
|
|
205
|
+
except Exception:
|
|
206
|
+
connection.rollback()
|
|
207
|
+
raise
|
|
208
|
+
else:
|
|
209
|
+
connection.commit()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class DurableCMSReceiverStore:
|
|
213
|
+
"""Dedicated crash-safe store implementing every receiver host callback."""
|
|
214
|
+
|
|
215
|
+
def __init__(
|
|
216
|
+
self,
|
|
217
|
+
connection: sqlite3.Connection,
|
|
218
|
+
*,
|
|
219
|
+
clock: Callable[[], float | int] = time.time,
|
|
220
|
+
):
|
|
221
|
+
if not isinstance(connection, sqlite3.Connection) or connection.in_transaction:
|
|
222
|
+
raise CMSReceiverStoreBlocked("connection must be an idle SQLite connection")
|
|
223
|
+
if not callable(clock):
|
|
224
|
+
raise CMSReceiverStoreBlocked("clock must be callable")
|
|
225
|
+
self.connection = connection
|
|
226
|
+
self.clock = clock
|
|
227
|
+
self.connection.row_factory = sqlite3.Row
|
|
228
|
+
self.connection.execute("PRAGMA foreign_keys = ON")
|
|
229
|
+
self.connection.execute("PRAGMA busy_timeout = 5000")
|
|
230
|
+
self.connection.execute("PRAGMA secure_delete = ON")
|
|
231
|
+
if int(self.connection.execute("PRAGMA secure_delete").fetchone()[0]) != 1:
|
|
232
|
+
raise CMSReceiverStoreBlocked("secure deletion is unavailable")
|
|
233
|
+
version = int(self.connection.execute("PRAGMA user_version").fetchone()[0])
|
|
234
|
+
if version not in {0, SCHEMA_VERSION}:
|
|
235
|
+
raise CMSReceiverStoreBlocked("unsupported CMS receiver store schema")
|
|
236
|
+
if version == 0:
|
|
237
|
+
self._create_schema()
|
|
238
|
+
self._verify_schema()
|
|
239
|
+
|
|
240
|
+
def _create_schema(self) -> None:
|
|
241
|
+
with _transaction(self.connection):
|
|
242
|
+
version = int(
|
|
243
|
+
self.connection.execute("PRAGMA user_version").fetchone()[0]
|
|
244
|
+
)
|
|
245
|
+
if version == SCHEMA_VERSION:
|
|
246
|
+
return
|
|
247
|
+
if version != 0:
|
|
248
|
+
raise CMSReceiverStoreBlocked("CMS receiver store schema changed")
|
|
249
|
+
self.connection.execute("""
|
|
250
|
+
CREATE TABLE cms_receiver_sources (
|
|
251
|
+
site_id TEXT NOT NULL,
|
|
252
|
+
source_id TEXT NOT NULL,
|
|
253
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
254
|
+
website_version TEXT NOT NULL,
|
|
255
|
+
plan_id TEXT NOT NULL,
|
|
256
|
+
source_revision TEXT NOT NULL,
|
|
257
|
+
source_sequence INTEGER NOT NULL CHECK (source_sequence > 0),
|
|
258
|
+
source_sha256 TEXT NOT NULL,
|
|
259
|
+
required_locales_json TEXT NOT NULL,
|
|
260
|
+
content_type TEXT NOT NULL,
|
|
261
|
+
commercial_profile TEXT,
|
|
262
|
+
expectation_sha256 TEXT NOT NULL,
|
|
263
|
+
active_delivery_id TEXT,
|
|
264
|
+
created_at REAL NOT NULL,
|
|
265
|
+
updated_at REAL NOT NULL,
|
|
266
|
+
PRIMARY KEY (site_id, source_id)
|
|
267
|
+
)
|
|
268
|
+
""")
|
|
269
|
+
self.connection.execute("""
|
|
270
|
+
CREATE TABLE cms_receiver_publications (
|
|
271
|
+
delivery_id TEXT PRIMARY KEY,
|
|
272
|
+
payload_sha256 TEXT NOT NULL UNIQUE,
|
|
273
|
+
site_id TEXT NOT NULL,
|
|
274
|
+
source_id TEXT NOT NULL,
|
|
275
|
+
source_sequence INTEGER NOT NULL CHECK (source_sequence > 0),
|
|
276
|
+
source_revision TEXT NOT NULL,
|
|
277
|
+
payload_json TEXT,
|
|
278
|
+
status TEXT NOT NULL CHECK (
|
|
279
|
+
status IN ('active', 'superseded', 'deleted')
|
|
280
|
+
),
|
|
281
|
+
tombstone_delivery_id TEXT,
|
|
282
|
+
tombstone_payload_sha256 TEXT,
|
|
283
|
+
created_at REAL NOT NULL,
|
|
284
|
+
updated_at REAL NOT NULL,
|
|
285
|
+
UNIQUE (site_id, source_id, source_sequence)
|
|
286
|
+
)
|
|
287
|
+
""")
|
|
288
|
+
self.connection.execute("""
|
|
289
|
+
CREATE TABLE cms_receiver_localizations (
|
|
290
|
+
delivery_id TEXT NOT NULL,
|
|
291
|
+
locale TEXT NOT NULL,
|
|
292
|
+
target_text TEXT NOT NULL,
|
|
293
|
+
target_sha256 TEXT NOT NULL,
|
|
294
|
+
approval_id TEXT NOT NULL,
|
|
295
|
+
approval_expires_at REAL NOT NULL,
|
|
296
|
+
release_evidence_json TEXT NOT NULL,
|
|
297
|
+
PRIMARY KEY (delivery_id, locale),
|
|
298
|
+
FOREIGN KEY (delivery_id)
|
|
299
|
+
REFERENCES cms_receiver_publications (delivery_id)
|
|
300
|
+
ON DELETE CASCADE
|
|
301
|
+
)
|
|
302
|
+
""")
|
|
303
|
+
self.connection.execute("""
|
|
304
|
+
CREATE TABLE cms_receiver_tombstones (
|
|
305
|
+
tombstone_id TEXT PRIMARY KEY,
|
|
306
|
+
event_id TEXT NOT NULL,
|
|
307
|
+
site_id TEXT NOT NULL,
|
|
308
|
+
website_version TEXT NOT NULL,
|
|
309
|
+
plan_id TEXT NOT NULL,
|
|
310
|
+
source_id TEXT NOT NULL,
|
|
311
|
+
source_sequence INTEGER NOT NULL CHECK (source_sequence > 0),
|
|
312
|
+
publication_delivery_id TEXT NOT NULL UNIQUE,
|
|
313
|
+
publication_payload_sha256 TEXT NOT NULL,
|
|
314
|
+
locales_json TEXT NOT NULL,
|
|
315
|
+
expectation_sha256 TEXT NOT NULL,
|
|
316
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'deleted')),
|
|
317
|
+
delivery_id TEXT UNIQUE,
|
|
318
|
+
payload_sha256 TEXT UNIQUE,
|
|
319
|
+
created_at REAL NOT NULL,
|
|
320
|
+
updated_at REAL NOT NULL
|
|
321
|
+
)
|
|
322
|
+
""")
|
|
323
|
+
self.connection.execute(
|
|
324
|
+
"CREATE INDEX cms_receiver_publications_source "
|
|
325
|
+
"ON cms_receiver_publications (site_id, source_id, status)"
|
|
326
|
+
)
|
|
327
|
+
self.connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
|
328
|
+
|
|
329
|
+
def _verify_schema(self) -> None:
|
|
330
|
+
if int(self.connection.execute("PRAGMA secure_delete").fetchone()[0]) != 1:
|
|
331
|
+
raise CMSReceiverStoreBlocked("secure deletion was disabled")
|
|
332
|
+
expected = {
|
|
333
|
+
"cms_receiver_sources": (
|
|
334
|
+
"site_id", "source_id", "event_id", "website_version", "plan_id",
|
|
335
|
+
"source_revision", "source_sequence", "source_sha256",
|
|
336
|
+
"required_locales_json", "content_type", "commercial_profile",
|
|
337
|
+
"expectation_sha256", "active_delivery_id", "created_at",
|
|
338
|
+
"updated_at",
|
|
339
|
+
),
|
|
340
|
+
"cms_receiver_publications": (
|
|
341
|
+
"delivery_id", "payload_sha256", "site_id", "source_id",
|
|
342
|
+
"source_sequence", "source_revision", "payload_json", "status",
|
|
343
|
+
"tombstone_delivery_id", "tombstone_payload_sha256", "created_at",
|
|
344
|
+
"updated_at",
|
|
345
|
+
),
|
|
346
|
+
"cms_receiver_localizations": (
|
|
347
|
+
"delivery_id", "locale", "target_text", "target_sha256",
|
|
348
|
+
"approval_id", "approval_expires_at", "release_evidence_json",
|
|
349
|
+
),
|
|
350
|
+
"cms_receiver_tombstones": (
|
|
351
|
+
"tombstone_id", "event_id", "site_id", "website_version",
|
|
352
|
+
"plan_id", "source_id", "source_sequence",
|
|
353
|
+
"publication_delivery_id", "publication_payload_sha256",
|
|
354
|
+
"locales_json", "expectation_sha256", "status", "delivery_id",
|
|
355
|
+
"payload_sha256", "created_at", "updated_at",
|
|
356
|
+
),
|
|
357
|
+
}
|
|
358
|
+
version = int(self.connection.execute("PRAGMA user_version").fetchone()[0])
|
|
359
|
+
if version != SCHEMA_VERSION:
|
|
360
|
+
raise CMSReceiverStoreBlocked("CMS receiver store schema version is invalid")
|
|
361
|
+
for table, columns in expected.items():
|
|
362
|
+
actual = tuple(
|
|
363
|
+
row[1] for row in self.connection.execute(
|
|
364
|
+
f"PRAGMA table_info({table})"
|
|
365
|
+
).fetchall()
|
|
366
|
+
)
|
|
367
|
+
if actual != columns:
|
|
368
|
+
raise CMSReceiverStoreBlocked(f"CMS receiver store table {table} drifted")
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def _source_from_row(row: sqlite3.Row) -> dict[str, Any]:
|
|
372
|
+
value = {
|
|
373
|
+
"event_id": row["event_id"],
|
|
374
|
+
"site_id": row["site_id"],
|
|
375
|
+
"website_version": row["website_version"],
|
|
376
|
+
"plan_id": row["plan_id"],
|
|
377
|
+
"source_id": row["source_id"],
|
|
378
|
+
"source_revision": row["source_revision"],
|
|
379
|
+
"source_sequence": row["source_sequence"],
|
|
380
|
+
"source_sha256": row["source_sha256"],
|
|
381
|
+
"required_locales": tuple(_decode_json(
|
|
382
|
+
row["required_locales_json"], field="required_locales_json",
|
|
383
|
+
)),
|
|
384
|
+
"content_type": row["content_type"],
|
|
385
|
+
"commercial_profile": row["commercial_profile"],
|
|
386
|
+
}
|
|
387
|
+
result = _publication_expectation(value)
|
|
388
|
+
if _binding_sha256(result) != row["expectation_sha256"]:
|
|
389
|
+
raise CMSReceiverStoreBlocked("source expectation hash is invalid")
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
@staticmethod
|
|
393
|
+
def _tombstone_from_row(row: sqlite3.Row) -> dict[str, Any]:
|
|
394
|
+
value = {
|
|
395
|
+
"tombstone_id": row["tombstone_id"],
|
|
396
|
+
"event_id": row["event_id"],
|
|
397
|
+
"site_id": row["site_id"],
|
|
398
|
+
"website_version": row["website_version"],
|
|
399
|
+
"plan_id": row["plan_id"],
|
|
400
|
+
"source_id": row["source_id"],
|
|
401
|
+
"source_sequence": row["source_sequence"],
|
|
402
|
+
"publication_delivery_id": row["publication_delivery_id"],
|
|
403
|
+
"publication_payload_sha256": row["publication_payload_sha256"],
|
|
404
|
+
"locales": tuple(_decode_json(row["locales_json"], field="locales_json")),
|
|
405
|
+
}
|
|
406
|
+
result = _tombstone_expectation(value)
|
|
407
|
+
if _binding_sha256(result) != row["expectation_sha256"]:
|
|
408
|
+
raise CMSReceiverStoreBlocked("tombstone expectation hash is invalid")
|
|
409
|
+
return result
|
|
410
|
+
|
|
411
|
+
def _validate_publication_row(
|
|
412
|
+
self, row: sqlite3.Row,
|
|
413
|
+
) -> dict[str, Any] | None:
|
|
414
|
+
delivery_id = _token(row["delivery_id"], field="delivery_id")
|
|
415
|
+
payload_sha256 = _sha256(row["payload_sha256"], field="payload_sha256")
|
|
416
|
+
_token(row["site_id"], field="site_id")
|
|
417
|
+
_token(row["source_id"], field="source_id")
|
|
418
|
+
_sequence(row["source_sequence"])
|
|
419
|
+
_token(row["source_revision"], field="source_revision")
|
|
420
|
+
localizations = self.connection.execute("""
|
|
421
|
+
SELECT locale, target_text, target_sha256, approval_id,
|
|
422
|
+
approval_expires_at, release_evidence_json
|
|
423
|
+
FROM cms_receiver_localizations
|
|
424
|
+
WHERE delivery_id = ? ORDER BY locale
|
|
425
|
+
""", (delivery_id,)).fetchall()
|
|
426
|
+
if row["status"] in {"deleted", "superseded"}:
|
|
427
|
+
if (
|
|
428
|
+
row["payload_json"] is not None
|
|
429
|
+
or localizations
|
|
430
|
+
):
|
|
431
|
+
raise CMSReceiverStoreBlocked(
|
|
432
|
+
"inactive publication retained localized content"
|
|
433
|
+
)
|
|
434
|
+
if row["status"] == "superseded":
|
|
435
|
+
if (
|
|
436
|
+
row["tombstone_delivery_id"] is not None
|
|
437
|
+
or row["tombstone_payload_sha256"] is not None
|
|
438
|
+
):
|
|
439
|
+
raise CMSReceiverStoreBlocked(
|
|
440
|
+
"superseded publication is invalid"
|
|
441
|
+
)
|
|
442
|
+
return None
|
|
443
|
+
if (
|
|
444
|
+
_token(
|
|
445
|
+
row["tombstone_delivery_id"], field="tombstone_delivery_id",
|
|
446
|
+
) != row["tombstone_delivery_id"]
|
|
447
|
+
or _sha256(
|
|
448
|
+
row["tombstone_payload_sha256"],
|
|
449
|
+
field="tombstone_payload_sha256",
|
|
450
|
+
) != row["tombstone_payload_sha256"]
|
|
451
|
+
):
|
|
452
|
+
raise CMSReceiverStoreBlocked("deleted publication is invalid")
|
|
453
|
+
return None
|
|
454
|
+
if (
|
|
455
|
+
row["status"] != "active"
|
|
456
|
+
or row["tombstone_delivery_id"] is not None
|
|
457
|
+
or row["tombstone_payload_sha256"] is not None
|
|
458
|
+
):
|
|
459
|
+
raise CMSReceiverStoreBlocked("publication state is invalid")
|
|
460
|
+
payload = _decode_json(row["payload_json"], field="payload_json")
|
|
461
|
+
if (
|
|
462
|
+
not isinstance(payload, dict)
|
|
463
|
+
or payload.get("delivery_id") != delivery_id
|
|
464
|
+
or payload.get("site_id") != row["site_id"]
|
|
465
|
+
or payload.get("source_id") != row["source_id"]
|
|
466
|
+
or payload.get("source_sequence") != row["source_sequence"]
|
|
467
|
+
or payload.get("source_revision") != row["source_revision"]
|
|
468
|
+
or hashlib.sha256(_canonical_json(payload).encode("utf-8")).hexdigest()
|
|
469
|
+
!= payload_sha256
|
|
470
|
+
):
|
|
471
|
+
raise CMSReceiverStoreBlocked("stored publication binding is invalid")
|
|
472
|
+
expected = payload.get("localizations")
|
|
473
|
+
if not isinstance(expected, list) or not all(
|
|
474
|
+
isinstance(item, dict) for item in expected
|
|
475
|
+
):
|
|
476
|
+
raise CMSReceiverStoreBlocked("stored locale bundle is incomplete")
|
|
477
|
+
locales = [item.get("locale") for item in expected]
|
|
478
|
+
if any(not isinstance(locale, str) for locale in locales):
|
|
479
|
+
raise CMSReceiverStoreBlocked("stored locale bundle is incomplete")
|
|
480
|
+
if len(localizations) != len(expected) or locales != sorted(set(locales)):
|
|
481
|
+
raise CMSReceiverStoreBlocked("stored locale bundle is incomplete")
|
|
482
|
+
for stored, item in zip(localizations, expected):
|
|
483
|
+
if not isinstance(item, dict):
|
|
484
|
+
raise CMSReceiverStoreBlocked("stored locale is invalid")
|
|
485
|
+
target_text = stored["target_text"]
|
|
486
|
+
evidence = _decode_json(
|
|
487
|
+
stored["release_evidence_json"], field="release_evidence_json",
|
|
488
|
+
)
|
|
489
|
+
if (
|
|
490
|
+
stored["locale"] != item.get("locale")
|
|
491
|
+
or target_text != item.get("target_text")
|
|
492
|
+
or stored["target_sha256"] != item.get("target_sha256")
|
|
493
|
+
or stored["approval_id"] != item.get("approval_id")
|
|
494
|
+
or stored["approval_expires_at"] != item.get("approval_expires_at")
|
|
495
|
+
or evidence != item.get("release_evidence")
|
|
496
|
+
or not isinstance(target_text, str)
|
|
497
|
+
or hashlib.sha256(target_text.encode("utf-8")).hexdigest()
|
|
498
|
+
!= stored["target_sha256"]
|
|
499
|
+
):
|
|
500
|
+
raise CMSReceiverStoreBlocked("stored locale binding is invalid")
|
|
501
|
+
return payload
|
|
502
|
+
|
|
503
|
+
@staticmethod
|
|
504
|
+
def _assert_publication_binding(
|
|
505
|
+
payload: Mapping[str, Any], expectation: Mapping[str, Any],
|
|
506
|
+
) -> None:
|
|
507
|
+
for field in (
|
|
508
|
+
"event_id", "site_id", "website_version", "plan_id", "source_id",
|
|
509
|
+
"source_revision", "source_sequence", "source_sha256",
|
|
510
|
+
):
|
|
511
|
+
if payload.get(field) != expectation[field]:
|
|
512
|
+
raise CMSReceiverStoreBlocked("publication is no longer current")
|
|
513
|
+
localizations = payload.get("localizations")
|
|
514
|
+
if not isinstance(localizations, list):
|
|
515
|
+
raise CMSReceiverStoreBlocked("publication localizations are invalid")
|
|
516
|
+
locales = tuple(item.get("locale") for item in localizations if isinstance(item, dict))
|
|
517
|
+
if locales != expectation["required_locales"] or len(locales) != len(localizations):
|
|
518
|
+
raise CMSReceiverStoreBlocked("publication locale set is no longer current")
|
|
519
|
+
for item in localizations:
|
|
520
|
+
evidence = item.get("release_evidence")
|
|
521
|
+
if (
|
|
522
|
+
not isinstance(evidence, dict)
|
|
523
|
+
or evidence.get("content_type") != expectation["content_type"]
|
|
524
|
+
or evidence.get("commercial_profile")
|
|
525
|
+
!= expectation["commercial_profile"]
|
|
526
|
+
):
|
|
527
|
+
raise CMSReceiverStoreBlocked("publication scope is no longer current")
|
|
528
|
+
|
|
529
|
+
def register_source(self, expectation: Any) -> Mapping[str, Any]:
|
|
530
|
+
"""Register or monotonically advance one trusted current source."""
|
|
531
|
+
|
|
532
|
+
value = _publication_expectation(expectation)
|
|
533
|
+
now = _timestamp(self.clock())
|
|
534
|
+
locales_json = _canonical_json(list(value["required_locales"]))
|
|
535
|
+
expectation_sha256 = _binding_sha256(value)
|
|
536
|
+
with _transaction(self.connection):
|
|
537
|
+
self._verify_schema()
|
|
538
|
+
row = self.connection.execute(
|
|
539
|
+
"SELECT * FROM cms_receiver_sources WHERE site_id = ? AND source_id = ?",
|
|
540
|
+
(value["site_id"], value["source_id"]),
|
|
541
|
+
).fetchone()
|
|
542
|
+
if row is not None:
|
|
543
|
+
current = self._source_from_row(row)
|
|
544
|
+
if value["source_sequence"] < current["source_sequence"]:
|
|
545
|
+
raise CMSReceiverStoreBlocked("source sequence moved backwards")
|
|
546
|
+
if value["source_sequence"] == current["source_sequence"]:
|
|
547
|
+
if value != current:
|
|
548
|
+
raise CMSReceiverStoreBlocked("source sequence was reused")
|
|
549
|
+
return {
|
|
550
|
+
"site_id": value["site_id"],
|
|
551
|
+
"source_id": value["source_id"],
|
|
552
|
+
"source_sequence": value["source_sequence"],
|
|
553
|
+
"status": "current",
|
|
554
|
+
}
|
|
555
|
+
if row["active_delivery_id"] is not None and self.connection.execute(
|
|
556
|
+
"SELECT 1 FROM cms_receiver_tombstones "
|
|
557
|
+
"WHERE publication_delivery_id = ? AND status = 'pending'",
|
|
558
|
+
(row["active_delivery_id"],),
|
|
559
|
+
).fetchone() is not None:
|
|
560
|
+
raise CMSReceiverStoreBlocked(
|
|
561
|
+
"source cannot advance during a pending tombstone"
|
|
562
|
+
)
|
|
563
|
+
try:
|
|
564
|
+
self.connection.execute("""
|
|
565
|
+
UPDATE cms_receiver_sources
|
|
566
|
+
SET event_id = ?, website_version = ?, plan_id = ?,
|
|
567
|
+
source_revision = ?, source_sequence = ?, source_sha256 = ?,
|
|
568
|
+
required_locales_json = ?, content_type = ?,
|
|
569
|
+
commercial_profile = ?, expectation_sha256 = ?,
|
|
570
|
+
updated_at = ?
|
|
571
|
+
WHERE site_id = ? AND source_id = ?
|
|
572
|
+
""", (
|
|
573
|
+
value["event_id"], value["website_version"], value["plan_id"],
|
|
574
|
+
value["source_revision"], value["source_sequence"],
|
|
575
|
+
value["source_sha256"], locales_json, value["content_type"],
|
|
576
|
+
value["commercial_profile"], expectation_sha256, now,
|
|
577
|
+
value["site_id"], value["source_id"],
|
|
578
|
+
))
|
|
579
|
+
except sqlite3.IntegrityError as error:
|
|
580
|
+
raise CMSReceiverStoreBlocked("source identity collided") from error
|
|
581
|
+
else:
|
|
582
|
+
try:
|
|
583
|
+
self.connection.execute("""
|
|
584
|
+
INSERT INTO cms_receiver_sources (
|
|
585
|
+
site_id, source_id, event_id, website_version, plan_id,
|
|
586
|
+
source_revision, source_sequence, source_sha256,
|
|
587
|
+
required_locales_json, content_type, commercial_profile,
|
|
588
|
+
expectation_sha256, active_delivery_id, created_at,
|
|
589
|
+
updated_at
|
|
590
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
|
|
591
|
+
""", (
|
|
592
|
+
value["site_id"], value["source_id"], value["event_id"],
|
|
593
|
+
value["website_version"], value["plan_id"],
|
|
594
|
+
value["source_revision"], value["source_sequence"],
|
|
595
|
+
value["source_sha256"], locales_json, value["content_type"],
|
|
596
|
+
value["commercial_profile"], expectation_sha256, now, now,
|
|
597
|
+
))
|
|
598
|
+
except sqlite3.IntegrityError as error:
|
|
599
|
+
raise CMSReceiverStoreBlocked("source identity collided") from error
|
|
600
|
+
return {
|
|
601
|
+
"site_id": value["site_id"],
|
|
602
|
+
"source_id": value["source_id"],
|
|
603
|
+
"source_sequence": value["source_sequence"],
|
|
604
|
+
"status": "current",
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
def resolve_publication_expectation(self, publication: Any) -> Mapping[str, Any]:
|
|
608
|
+
_, _, payload, _ = _verified(publication, kind="publication")
|
|
609
|
+
site_id = _token(payload.get("site_id"), field="site_id")
|
|
610
|
+
source_id = _token(payload.get("source_id"), field="source_id")
|
|
611
|
+
self._verify_schema()
|
|
612
|
+
row = self.connection.execute(
|
|
613
|
+
"SELECT * FROM cms_receiver_sources WHERE site_id = ? AND source_id = ?",
|
|
614
|
+
(site_id, source_id),
|
|
615
|
+
).fetchone()
|
|
616
|
+
if row is None:
|
|
617
|
+
raise CMSReceiverStoreBlocked("source expectation is missing")
|
|
618
|
+
return self._source_from_row(row)
|
|
619
|
+
|
|
620
|
+
def commit(self, publication: Any) -> Mapping[str, Any]:
|
|
621
|
+
"""Install a complete bundle, then atomically scrub its predecessor."""
|
|
622
|
+
|
|
623
|
+
delivery_id, payload_sha256, payload, payload_json = _verified(
|
|
624
|
+
publication, kind="publication",
|
|
625
|
+
)
|
|
626
|
+
now = _timestamp(self.clock())
|
|
627
|
+
with _transaction(self.connection):
|
|
628
|
+
self._verify_schema()
|
|
629
|
+
row = self.connection.execute(
|
|
630
|
+
"SELECT * FROM cms_receiver_sources WHERE site_id = ? AND source_id = ?",
|
|
631
|
+
(payload.get("site_id"), payload.get("source_id")),
|
|
632
|
+
).fetchone()
|
|
633
|
+
if row is None:
|
|
634
|
+
raise CMSReceiverStoreBlocked("source expectation is missing")
|
|
635
|
+
current = self._source_from_row(row)
|
|
636
|
+
self._assert_publication_binding(payload, current)
|
|
637
|
+
existing = self.connection.execute(
|
|
638
|
+
"SELECT * FROM cms_receiver_publications WHERE delivery_id = ?",
|
|
639
|
+
(delivery_id,),
|
|
640
|
+
).fetchone()
|
|
641
|
+
if existing is not None:
|
|
642
|
+
if (
|
|
643
|
+
existing["payload_sha256"] == payload_sha256
|
|
644
|
+
and existing["status"] == "active"
|
|
645
|
+
and row["active_delivery_id"] == delivery_id
|
|
646
|
+
and self._validate_publication_row(existing) == payload
|
|
647
|
+
):
|
|
648
|
+
return {
|
|
649
|
+
"delivery_id": delivery_id,
|
|
650
|
+
"payload_sha256": payload_sha256,
|
|
651
|
+
"status": "committed",
|
|
652
|
+
}
|
|
653
|
+
raise CMSReceiverStoreBlocked("publication delivery collided")
|
|
654
|
+
if self.connection.execute(
|
|
655
|
+
"SELECT 1 FROM cms_receiver_publications "
|
|
656
|
+
"WHERE site_id = ? AND source_id = ? AND source_sequence = ?",
|
|
657
|
+
(current["site_id"], current["source_id"], current["source_sequence"]),
|
|
658
|
+
).fetchone() is not None:
|
|
659
|
+
raise CMSReceiverStoreBlocked("publication generation collided")
|
|
660
|
+
old_delivery_id = row["active_delivery_id"]
|
|
661
|
+
if old_delivery_id is not None:
|
|
662
|
+
changed = self.connection.execute("""
|
|
663
|
+
UPDATE cms_receiver_publications
|
|
664
|
+
SET status = 'superseded', updated_at = ?
|
|
665
|
+
WHERE delivery_id = ? AND status = 'active'
|
|
666
|
+
""", (now, old_delivery_id)).rowcount
|
|
667
|
+
if changed != 1:
|
|
668
|
+
raise CMSReceiverStoreBlocked("active publication state is invalid")
|
|
669
|
+
try:
|
|
670
|
+
self.connection.execute("""
|
|
671
|
+
INSERT INTO cms_receiver_publications (
|
|
672
|
+
delivery_id, payload_sha256, site_id, source_id,
|
|
673
|
+
source_sequence, source_revision, payload_json, status,
|
|
674
|
+
tombstone_delivery_id, tombstone_payload_sha256,
|
|
675
|
+
created_at, updated_at
|
|
676
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', NULL, NULL, ?, ?)
|
|
677
|
+
""", (
|
|
678
|
+
delivery_id, payload_sha256, current["site_id"],
|
|
679
|
+
current["source_id"], current["source_sequence"],
|
|
680
|
+
current["source_revision"], payload_json, now, now,
|
|
681
|
+
))
|
|
682
|
+
for item in payload["localizations"]:
|
|
683
|
+
self.connection.execute("""
|
|
684
|
+
INSERT INTO cms_receiver_localizations (
|
|
685
|
+
delivery_id, locale, target_text, target_sha256,
|
|
686
|
+
approval_id, approval_expires_at, release_evidence_json
|
|
687
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
688
|
+
""", (
|
|
689
|
+
delivery_id, item["locale"], item["target_text"],
|
|
690
|
+
item["target_sha256"], item["approval_id"],
|
|
691
|
+
item["approval_expires_at"],
|
|
692
|
+
_canonical_json(item["release_evidence"]),
|
|
693
|
+
))
|
|
694
|
+
except (KeyError, TypeError, sqlite3.IntegrityError) as error:
|
|
695
|
+
raise CMSReceiverStoreBlocked("publication bundle is invalid") from error
|
|
696
|
+
changed = self.connection.execute("""
|
|
697
|
+
UPDATE cms_receiver_sources
|
|
698
|
+
SET active_delivery_id = ?, updated_at = ?
|
|
699
|
+
WHERE site_id = ? AND source_id = ? AND source_sequence = ?
|
|
700
|
+
AND source_revision = ? AND source_sha256 = ?
|
|
701
|
+
""", (
|
|
702
|
+
delivery_id, now, current["site_id"], current["source_id"],
|
|
703
|
+
current["source_sequence"], current["source_revision"],
|
|
704
|
+
current["source_sha256"],
|
|
705
|
+
)).rowcount
|
|
706
|
+
if changed != 1:
|
|
707
|
+
raise CMSReceiverStoreBlocked("source changed during publication")
|
|
708
|
+
if old_delivery_id is not None:
|
|
709
|
+
try:
|
|
710
|
+
self.connection.execute(
|
|
711
|
+
"DELETE FROM cms_receiver_localizations WHERE delivery_id = ?",
|
|
712
|
+
(old_delivery_id,),
|
|
713
|
+
)
|
|
714
|
+
changed = self.connection.execute("""
|
|
715
|
+
UPDATE cms_receiver_publications
|
|
716
|
+
SET payload_json = NULL, updated_at = ?
|
|
717
|
+
WHERE delivery_id = ? AND status = 'superseded'
|
|
718
|
+
""", (now, old_delivery_id)).rowcount
|
|
719
|
+
except sqlite3.Error as error:
|
|
720
|
+
raise CMSReceiverStoreBlocked(
|
|
721
|
+
"superseded publication cleanup failed"
|
|
722
|
+
) from error
|
|
723
|
+
if changed != 1:
|
|
724
|
+
raise CMSReceiverStoreBlocked(
|
|
725
|
+
"superseded publication cleanup was not atomic"
|
|
726
|
+
)
|
|
727
|
+
return {
|
|
728
|
+
"delivery_id": delivery_id,
|
|
729
|
+
"payload_sha256": payload_sha256,
|
|
730
|
+
"status": "committed",
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
def read_active_bundle(self, expectation: Any) -> Mapping[str, Any] | None:
|
|
734
|
+
"""Read only the active bundle for one exact trusted source binding."""
|
|
735
|
+
|
|
736
|
+
expected = _publication_expectation(expectation)
|
|
737
|
+
site_id = expected["site_id"]
|
|
738
|
+
source_id = expected["source_id"]
|
|
739
|
+
self._verify_schema()
|
|
740
|
+
source = self.connection.execute(
|
|
741
|
+
"SELECT * FROM cms_receiver_sources "
|
|
742
|
+
"WHERE site_id = ? AND source_id = ?",
|
|
743
|
+
(site_id, source_id),
|
|
744
|
+
).fetchone()
|
|
745
|
+
if source is None:
|
|
746
|
+
return None
|
|
747
|
+
self._source_from_row(source)
|
|
748
|
+
if source["active_delivery_id"] is None:
|
|
749
|
+
return None
|
|
750
|
+
publication = self.connection.execute(
|
|
751
|
+
"SELECT * FROM cms_receiver_publications WHERE delivery_id = ?",
|
|
752
|
+
(source["active_delivery_id"],),
|
|
753
|
+
).fetchone()
|
|
754
|
+
if (
|
|
755
|
+
publication is None
|
|
756
|
+
or publication["status"] != "active"
|
|
757
|
+
or publication["site_id"] != site_id
|
|
758
|
+
or publication["source_id"] != source_id
|
|
759
|
+
):
|
|
760
|
+
raise CMSReceiverStoreBlocked("active publication is invalid")
|
|
761
|
+
payload = self._validate_publication_row(publication)
|
|
762
|
+
if payload is None:
|
|
763
|
+
raise CMSReceiverStoreBlocked("active publication was deleted")
|
|
764
|
+
self._assert_publication_binding(payload, expected)
|
|
765
|
+
return payload
|
|
766
|
+
|
|
767
|
+
def register_tombstone(self, expectation: Any) -> Mapping[str, Any]:
|
|
768
|
+
"""Pre-authorize one exact deletion of the currently active publication."""
|
|
769
|
+
|
|
770
|
+
value = _tombstone_expectation(expectation)
|
|
771
|
+
now = _timestamp(self.clock())
|
|
772
|
+
locales_json = _canonical_json(list(value["locales"]))
|
|
773
|
+
expectation_sha256 = _binding_sha256(value)
|
|
774
|
+
with _transaction(self.connection):
|
|
775
|
+
self._verify_schema()
|
|
776
|
+
existing = self.connection.execute(
|
|
777
|
+
"SELECT * FROM cms_receiver_tombstones WHERE tombstone_id = ?",
|
|
778
|
+
(value["tombstone_id"],),
|
|
779
|
+
).fetchone()
|
|
780
|
+
if existing is not None:
|
|
781
|
+
if self._tombstone_from_row(existing) != value:
|
|
782
|
+
raise CMSReceiverStoreBlocked("tombstone identity collided")
|
|
783
|
+
return {
|
|
784
|
+
"tombstone_id": value["tombstone_id"],
|
|
785
|
+
"publication_delivery_id": value["publication_delivery_id"],
|
|
786
|
+
"status": existing["status"],
|
|
787
|
+
}
|
|
788
|
+
publication = self.connection.execute(
|
|
789
|
+
"SELECT * FROM cms_receiver_publications WHERE delivery_id = ?",
|
|
790
|
+
(value["publication_delivery_id"],),
|
|
791
|
+
).fetchone()
|
|
792
|
+
source = self.connection.execute(
|
|
793
|
+
"SELECT active_delivery_id FROM cms_receiver_sources "
|
|
794
|
+
"WHERE site_id = ? AND source_id = ?",
|
|
795
|
+
(value["site_id"], value["source_id"]),
|
|
796
|
+
).fetchone()
|
|
797
|
+
if (
|
|
798
|
+
publication is None
|
|
799
|
+
or publication["status"] != "active"
|
|
800
|
+
or publication["payload_sha256"]
|
|
801
|
+
!= value["publication_payload_sha256"]
|
|
802
|
+
or publication["site_id"] != value["site_id"]
|
|
803
|
+
or publication["source_id"] != value["source_id"]
|
|
804
|
+
or publication["source_sequence"] != value["source_sequence"]
|
|
805
|
+
or source is None
|
|
806
|
+
or source["active_delivery_id"] != value["publication_delivery_id"]
|
|
807
|
+
):
|
|
808
|
+
raise CMSReceiverStoreBlocked("tombstone publication is not active")
|
|
809
|
+
payload = self._validate_publication_row(publication)
|
|
810
|
+
if (
|
|
811
|
+
not isinstance(payload, dict)
|
|
812
|
+
or payload.get("event_id") != value["event_id"]
|
|
813
|
+
or payload.get("website_version") != value["website_version"]
|
|
814
|
+
or payload.get("plan_id") != value["plan_id"]
|
|
815
|
+
or tuple(item.get("locale") for item in payload.get("localizations", []))
|
|
816
|
+
!= value["locales"]
|
|
817
|
+
):
|
|
818
|
+
raise CMSReceiverStoreBlocked("tombstone publication binding is invalid")
|
|
819
|
+
try:
|
|
820
|
+
self.connection.execute("""
|
|
821
|
+
INSERT INTO cms_receiver_tombstones (
|
|
822
|
+
tombstone_id, event_id, site_id, website_version, plan_id,
|
|
823
|
+
source_id, source_sequence, publication_delivery_id,
|
|
824
|
+
publication_payload_sha256, locales_json,
|
|
825
|
+
expectation_sha256, status, delivery_id, payload_sha256,
|
|
826
|
+
created_at, updated_at
|
|
827
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, ?)
|
|
828
|
+
""", (
|
|
829
|
+
value["tombstone_id"], value["event_id"], value["site_id"],
|
|
830
|
+
value["website_version"], value["plan_id"], value["source_id"],
|
|
831
|
+
value["source_sequence"], value["publication_delivery_id"],
|
|
832
|
+
value["publication_payload_sha256"], locales_json,
|
|
833
|
+
expectation_sha256, now, now,
|
|
834
|
+
))
|
|
835
|
+
except sqlite3.IntegrityError as error:
|
|
836
|
+
raise CMSReceiverStoreBlocked("tombstone publication collided") from error
|
|
837
|
+
return {
|
|
838
|
+
"tombstone_id": value["tombstone_id"],
|
|
839
|
+
"publication_delivery_id": value["publication_delivery_id"],
|
|
840
|
+
"status": "pending",
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
def resolve_tombstone_expectation(self, tombstone: Any) -> Mapping[str, Any]:
|
|
844
|
+
_, _, payload, _ = _verified(tombstone, kind="tombstone")
|
|
845
|
+
tombstone_id = _token(payload.get("tombstone_id"), field="tombstone_id")
|
|
846
|
+
self._verify_schema()
|
|
847
|
+
row = self.connection.execute(
|
|
848
|
+
"SELECT * FROM cms_receiver_tombstones WHERE tombstone_id = ?",
|
|
849
|
+
(tombstone_id,),
|
|
850
|
+
).fetchone()
|
|
851
|
+
if row is None:
|
|
852
|
+
raise CMSReceiverStoreBlocked("tombstone expectation is missing")
|
|
853
|
+
return self._tombstone_from_row(row)
|
|
854
|
+
|
|
855
|
+
def delete(self, tombstone: Any) -> Mapping[str, Any]:
|
|
856
|
+
"""Atomically remove one exact active bundle and retain content-free evidence."""
|
|
857
|
+
|
|
858
|
+
delivery_id, payload_sha256, payload, _ = _verified(
|
|
859
|
+
tombstone, kind="tombstone",
|
|
860
|
+
)
|
|
861
|
+
now = _timestamp(self.clock())
|
|
862
|
+
with _transaction(self.connection):
|
|
863
|
+
self._verify_schema()
|
|
864
|
+
row = self.connection.execute(
|
|
865
|
+
"SELECT * FROM cms_receiver_tombstones WHERE tombstone_id = ?",
|
|
866
|
+
(payload.get("tombstone_id"),),
|
|
867
|
+
).fetchone()
|
|
868
|
+
if row is None:
|
|
869
|
+
raise CMSReceiverStoreBlocked("tombstone expectation is missing")
|
|
870
|
+
expected = self._tombstone_from_row(row)
|
|
871
|
+
for field, value in expected.items():
|
|
872
|
+
actual = tuple(payload.get(field, ())) if field == "locales" else payload.get(field)
|
|
873
|
+
if actual != value:
|
|
874
|
+
raise CMSReceiverStoreBlocked("tombstone is no longer current")
|
|
875
|
+
if row["status"] == "deleted":
|
|
876
|
+
if row["delivery_id"] == delivery_id and row["payload_sha256"] == payload_sha256:
|
|
877
|
+
publication = self.connection.execute(
|
|
878
|
+
"SELECT * FROM cms_receiver_publications WHERE delivery_id = ?",
|
|
879
|
+
(expected["publication_delivery_id"],),
|
|
880
|
+
).fetchone()
|
|
881
|
+
source = self.connection.execute(
|
|
882
|
+
"SELECT active_delivery_id FROM cms_receiver_sources "
|
|
883
|
+
"WHERE site_id = ? AND source_id = ?",
|
|
884
|
+
(expected["site_id"], expected["source_id"]),
|
|
885
|
+
).fetchone()
|
|
886
|
+
if (
|
|
887
|
+
publication is None
|
|
888
|
+
or self._validate_publication_row(publication) is not None
|
|
889
|
+
or publication["status"] != "deleted"
|
|
890
|
+
or publication["payload_sha256"]
|
|
891
|
+
!= expected["publication_payload_sha256"]
|
|
892
|
+
or publication["tombstone_delivery_id"] != delivery_id
|
|
893
|
+
or publication["tombstone_payload_sha256"] != payload_sha256
|
|
894
|
+
or source is None
|
|
895
|
+
or source["active_delivery_id"]
|
|
896
|
+
== expected["publication_delivery_id"]
|
|
897
|
+
):
|
|
898
|
+
raise CMSReceiverStoreBlocked(
|
|
899
|
+
"deleted publication evidence is invalid"
|
|
900
|
+
)
|
|
901
|
+
return {
|
|
902
|
+
"delivery_id": delivery_id,
|
|
903
|
+
"payload_sha256": payload_sha256,
|
|
904
|
+
"status": "deleted",
|
|
905
|
+
}
|
|
906
|
+
raise CMSReceiverStoreBlocked("tombstone delivery collided")
|
|
907
|
+
publication = self.connection.execute(
|
|
908
|
+
"SELECT * FROM cms_receiver_publications WHERE delivery_id = ?",
|
|
909
|
+
(expected["publication_delivery_id"],),
|
|
910
|
+
).fetchone()
|
|
911
|
+
source = self.connection.execute(
|
|
912
|
+
"SELECT active_delivery_id FROM cms_receiver_sources "
|
|
913
|
+
"WHERE site_id = ? AND source_id = ?",
|
|
914
|
+
(expected["site_id"], expected["source_id"]),
|
|
915
|
+
).fetchone()
|
|
916
|
+
if (
|
|
917
|
+
publication is None
|
|
918
|
+
or publication["status"] != "active"
|
|
919
|
+
or publication["payload_sha256"]
|
|
920
|
+
!= expected["publication_payload_sha256"]
|
|
921
|
+
or source is None
|
|
922
|
+
or source["active_delivery_id"]
|
|
923
|
+
!= expected["publication_delivery_id"]
|
|
924
|
+
):
|
|
925
|
+
raise CMSReceiverStoreBlocked("tombstone publication changed")
|
|
926
|
+
if self._validate_publication_row(publication) is None:
|
|
927
|
+
raise CMSReceiverStoreBlocked("tombstone publication was deleted")
|
|
928
|
+
self.connection.execute(
|
|
929
|
+
"DELETE FROM cms_receiver_localizations WHERE delivery_id = ?",
|
|
930
|
+
(expected["publication_delivery_id"],),
|
|
931
|
+
)
|
|
932
|
+
changed = self.connection.execute("""
|
|
933
|
+
UPDATE cms_receiver_publications
|
|
934
|
+
SET payload_json = NULL, status = 'deleted',
|
|
935
|
+
tombstone_delivery_id = ?, tombstone_payload_sha256 = ?,
|
|
936
|
+
updated_at = ?
|
|
937
|
+
WHERE delivery_id = ? AND status = 'active'
|
|
938
|
+
""", (
|
|
939
|
+
delivery_id, payload_sha256, now,
|
|
940
|
+
expected["publication_delivery_id"],
|
|
941
|
+
)).rowcount
|
|
942
|
+
if changed != 1:
|
|
943
|
+
raise CMSReceiverStoreBlocked("publication deletion was not atomic")
|
|
944
|
+
changed = self.connection.execute("""
|
|
945
|
+
UPDATE cms_receiver_sources
|
|
946
|
+
SET active_delivery_id = NULL, updated_at = ?
|
|
947
|
+
WHERE site_id = ? AND source_id = ? AND active_delivery_id = ?
|
|
948
|
+
""", (
|
|
949
|
+
now, expected["site_id"], expected["source_id"],
|
|
950
|
+
expected["publication_delivery_id"],
|
|
951
|
+
)).rowcount
|
|
952
|
+
if changed != 1:
|
|
953
|
+
raise CMSReceiverStoreBlocked("source deletion was not atomic")
|
|
954
|
+
changed = self.connection.execute("""
|
|
955
|
+
UPDATE cms_receiver_tombstones
|
|
956
|
+
SET status = 'deleted', delivery_id = ?, payload_sha256 = ?,
|
|
957
|
+
updated_at = ?
|
|
958
|
+
WHERE tombstone_id = ? AND status = 'pending'
|
|
959
|
+
""", (
|
|
960
|
+
delivery_id, payload_sha256, now, expected["tombstone_id"],
|
|
961
|
+
)).rowcount
|
|
962
|
+
if changed != 1:
|
|
963
|
+
raise CMSReceiverStoreBlocked("tombstone deletion was not atomic")
|
|
964
|
+
return {
|
|
965
|
+
"delivery_id": delivery_id,
|
|
966
|
+
"payload_sha256": payload_sha256,
|
|
967
|
+
"status": "deleted",
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
def check(self, probe: Any) -> Mapping[str, Any]:
|
|
971
|
+
"""Return the exact content-free health receipt after durable checks."""
|
|
972
|
+
|
|
973
|
+
probe_id = _token(getattr(probe, "probe_id", None), field="probe_id")
|
|
974
|
+
contract_sha256 = _sha256(
|
|
975
|
+
getattr(probe, "contract_sha256", None), field="contract_sha256",
|
|
976
|
+
)
|
|
977
|
+
self._verify_schema()
|
|
978
|
+
if self.connection.in_transaction:
|
|
979
|
+
raise CMSReceiverStoreBlocked("health cannot join an external transaction")
|
|
980
|
+
quick = self.connection.execute("PRAGMA quick_check").fetchall()
|
|
981
|
+
if [tuple(row) for row in quick] != [("ok",)]:
|
|
982
|
+
raise CMSReceiverStoreBlocked("SQLite integrity check failed")
|
|
983
|
+
foreign_keys = self.connection.execute("PRAGMA foreign_key_check").fetchall()
|
|
984
|
+
if foreign_keys:
|
|
985
|
+
raise CMSReceiverStoreBlocked("SQLite foreign-key check failed")
|
|
986
|
+
sources = self.connection.execute(
|
|
987
|
+
"SELECT * FROM cms_receiver_sources ORDER BY site_id, source_id"
|
|
988
|
+
).fetchall()
|
|
989
|
+
for source in sources:
|
|
990
|
+
self._source_from_row(source)
|
|
991
|
+
invalid_active = self.connection.execute("""
|
|
992
|
+
SELECT 1
|
|
993
|
+
FROM cms_receiver_sources AS source
|
|
994
|
+
LEFT JOIN cms_receiver_publications AS publication
|
|
995
|
+
ON publication.delivery_id = source.active_delivery_id
|
|
996
|
+
WHERE source.active_delivery_id IS NOT NULL AND (
|
|
997
|
+
publication.delivery_id IS NULL
|
|
998
|
+
OR publication.status != 'active'
|
|
999
|
+
OR publication.site_id != source.site_id
|
|
1000
|
+
OR publication.source_id != source.source_id
|
|
1001
|
+
)
|
|
1002
|
+
LIMIT 1
|
|
1003
|
+
""").fetchone()
|
|
1004
|
+
orphan_active = self.connection.execute("""
|
|
1005
|
+
SELECT 1
|
|
1006
|
+
FROM cms_receiver_publications AS publication
|
|
1007
|
+
LEFT JOIN cms_receiver_sources AS source
|
|
1008
|
+
ON source.site_id = publication.site_id
|
|
1009
|
+
AND source.source_id = publication.source_id
|
|
1010
|
+
AND source.active_delivery_id = publication.delivery_id
|
|
1011
|
+
WHERE publication.status = 'active' AND source.site_id IS NULL
|
|
1012
|
+
LIMIT 1
|
|
1013
|
+
""").fetchone()
|
|
1014
|
+
if invalid_active is not None or orphan_active is not None:
|
|
1015
|
+
raise CMSReceiverStoreBlocked("durable receiver state is inconsistent")
|
|
1016
|
+
publications = self.connection.execute(
|
|
1017
|
+
"SELECT * FROM cms_receiver_publications ORDER BY delivery_id"
|
|
1018
|
+
).fetchall()
|
|
1019
|
+
for publication in publications:
|
|
1020
|
+
self._validate_publication_row(publication)
|
|
1021
|
+
tombstones = self.connection.execute(
|
|
1022
|
+
"SELECT * FROM cms_receiver_tombstones ORDER BY tombstone_id"
|
|
1023
|
+
).fetchall()
|
|
1024
|
+
for tombstone in tombstones:
|
|
1025
|
+
value = self._tombstone_from_row(tombstone)
|
|
1026
|
+
if tombstone["status"] == "pending":
|
|
1027
|
+
publication = self.connection.execute("""
|
|
1028
|
+
SELECT publication.status, publication.payload_sha256,
|
|
1029
|
+
source.active_delivery_id
|
|
1030
|
+
FROM cms_receiver_publications AS publication
|
|
1031
|
+
LEFT JOIN cms_receiver_sources AS source
|
|
1032
|
+
ON source.site_id = publication.site_id
|
|
1033
|
+
AND source.source_id = publication.source_id
|
|
1034
|
+
WHERE publication.delivery_id = ?
|
|
1035
|
+
""", (value["publication_delivery_id"],)).fetchone()
|
|
1036
|
+
if (
|
|
1037
|
+
tombstone["delivery_id"] is not None
|
|
1038
|
+
or tombstone["payload_sha256"] is not None
|
|
1039
|
+
or publication is None
|
|
1040
|
+
or publication["status"] != "active"
|
|
1041
|
+
or publication["payload_sha256"]
|
|
1042
|
+
!= value["publication_payload_sha256"]
|
|
1043
|
+
or publication["active_delivery_id"]
|
|
1044
|
+
!= value["publication_delivery_id"]
|
|
1045
|
+
):
|
|
1046
|
+
raise CMSReceiverStoreBlocked("pending tombstone is invalid")
|
|
1047
|
+
elif tombstone["status"] == "deleted":
|
|
1048
|
+
delivery_id = _token(tombstone["delivery_id"], field="delivery_id")
|
|
1049
|
+
payload_sha256 = _sha256(
|
|
1050
|
+
tombstone["payload_sha256"], field="payload_sha256",
|
|
1051
|
+
)
|
|
1052
|
+
publication = self.connection.execute(
|
|
1053
|
+
"SELECT status, payload_sha256, tombstone_delivery_id, "
|
|
1054
|
+
"tombstone_payload_sha256 "
|
|
1055
|
+
"FROM cms_receiver_publications WHERE delivery_id = ?",
|
|
1056
|
+
(value["publication_delivery_id"],),
|
|
1057
|
+
).fetchone()
|
|
1058
|
+
if (
|
|
1059
|
+
publication is None
|
|
1060
|
+
or publication["status"] != "deleted"
|
|
1061
|
+
or publication["payload_sha256"]
|
|
1062
|
+
!= value["publication_payload_sha256"]
|
|
1063
|
+
or publication["tombstone_delivery_id"] != delivery_id
|
|
1064
|
+
or publication["tombstone_payload_sha256"] != payload_sha256
|
|
1065
|
+
):
|
|
1066
|
+
raise CMSReceiverStoreBlocked("deleted tombstone is invalid")
|
|
1067
|
+
else:
|
|
1068
|
+
raise CMSReceiverStoreBlocked("tombstone state is invalid")
|
|
1069
|
+
return {
|
|
1070
|
+
"probe_id": probe_id,
|
|
1071
|
+
"contract_sha256": contract_sha256,
|
|
1072
|
+
"status": "healthy",
|
|
1073
|
+
}
|