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_source_delivery.py
ADDED
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Durable website-side delivery outbox for the source-CMS HTTPS client.
|
|
3
|
+
|
|
4
|
+
The website host persists one immutable change, cancellation, or tombstone
|
|
5
|
+
before network access. One token-bound lease performs one client call. A crash
|
|
6
|
+
after remote acceptance safely replays the same identity after lease expiry;
|
|
7
|
+
the source runtime remains the idempotency authority.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import re
|
|
17
|
+
import secrets
|
|
18
|
+
import sqlite3
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from contextlib import contextmanager
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Callable, Iterator, Mapping
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SCHEMA_VERSION = 1
|
|
28
|
+
MAX_ATTEMPTS = 20
|
|
29
|
+
MAX_LEASE_SECONDS = 86_400.0
|
|
30
|
+
MAX_DELAY_SECONDS = 86_400.0
|
|
31
|
+
STATUSES = ("pending", "leased", "retry_wait", "succeeded", "failed")
|
|
32
|
+
ACTIVE_STATUSES = {"pending", "leased", "retry_wait"}
|
|
33
|
+
OPERATIONS = ("change", "cancellation", "tombstone")
|
|
34
|
+
TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
|
|
35
|
+
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
36
|
+
ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
|
|
37
|
+
_META_COLUMNS = ("singleton", "schema_version")
|
|
38
|
+
_COLUMNS = (
|
|
39
|
+
"operation", "request_id", "event_id", "site_id", "payload_json",
|
|
40
|
+
"payload_sha256", "capabilities_sha256", "status", "attempts",
|
|
41
|
+
"delivery_max_attempts", "source_max_attempts", "next_attempt_at",
|
|
42
|
+
"lease_owner", "lease_token", "lease_expires_at", "last_error_code",
|
|
43
|
+
"response_sha256", "created_at", "updated_at",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _load_module(name: str, path: Path):
|
|
48
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
49
|
+
if spec is None or spec.loader is None:
|
|
50
|
+
raise RuntimeError(f"cannot load source delivery dependency: {path.name}")
|
|
51
|
+
module = importlib.util.module_from_spec(spec)
|
|
52
|
+
sys.modules[spec.name] = module
|
|
53
|
+
spec.loader.exec_module(module)
|
|
54
|
+
return module
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
58
|
+
_CLIENT = _load_module(
|
|
59
|
+
"blun_website_localization_cms_source_delivery_client",
|
|
60
|
+
_ROOT / "integrations" / "website_localization_cms_source_client.py",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CMSSourceDeliveryBlocked(RuntimeError):
|
|
65
|
+
"""Stable, content-free failure of the durable delivery outbox."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, code: str):
|
|
68
|
+
if not isinstance(code, str) or ERROR_CODE.fullmatch(code) is None:
|
|
69
|
+
raise ValueError("source delivery failure is invalid")
|
|
70
|
+
super().__init__(code)
|
|
71
|
+
self.code = code
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class SourceDeliveryClaim:
|
|
76
|
+
operation: str
|
|
77
|
+
request_id: str
|
|
78
|
+
event_id: str
|
|
79
|
+
site_id: str
|
|
80
|
+
payload_sha256: str
|
|
81
|
+
capabilities_sha256: str
|
|
82
|
+
attempt: int
|
|
83
|
+
delivery_max_attempts: int
|
|
84
|
+
source_max_attempts: int
|
|
85
|
+
lease_owner: str
|
|
86
|
+
lease_token: str
|
|
87
|
+
lease_expires_at: float
|
|
88
|
+
payload_json: str = field(repr=False, compare=False)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class SourceDeliveryStatus:
|
|
93
|
+
operation: str
|
|
94
|
+
request_id: str
|
|
95
|
+
event_id: str
|
|
96
|
+
site_id: str
|
|
97
|
+
payload_sha256: str
|
|
98
|
+
capabilities_sha256: str
|
|
99
|
+
status: str
|
|
100
|
+
attempts: int
|
|
101
|
+
delivery_max_attempts: int
|
|
102
|
+
source_max_attempts: int
|
|
103
|
+
next_attempt_at: float
|
|
104
|
+
lease_expires_at: float | None
|
|
105
|
+
lease_expired: bool
|
|
106
|
+
last_error_code: str | None
|
|
107
|
+
response_sha256: str | None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class SourceDeliveryOutcome:
|
|
112
|
+
operation: str
|
|
113
|
+
request_id: str
|
|
114
|
+
event_id: str
|
|
115
|
+
status: str
|
|
116
|
+
attempt: int
|
|
117
|
+
delivery_max_attempts: int
|
|
118
|
+
next_attempt_at: float
|
|
119
|
+
error_code: str | None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class SourceDeliveryHealth:
|
|
124
|
+
schema: str
|
|
125
|
+
status: str
|
|
126
|
+
counts: dict[str, int]
|
|
127
|
+
operations: dict[str, int]
|
|
128
|
+
due: int
|
|
129
|
+
expired_leases: int
|
|
130
|
+
failed: int
|
|
131
|
+
contract_mismatches: int
|
|
132
|
+
error_code: str | None
|
|
133
|
+
|
|
134
|
+
def as_payload(self) -> dict[str, Any]:
|
|
135
|
+
return {
|
|
136
|
+
"schema": self.schema,
|
|
137
|
+
"status": self.status,
|
|
138
|
+
"counts": dict(self.counts),
|
|
139
|
+
"operations": dict(self.operations),
|
|
140
|
+
"due": self.due,
|
|
141
|
+
"expired_leases": self.expired_leases,
|
|
142
|
+
"failed": self.failed,
|
|
143
|
+
"contract_mismatches": self.contract_mismatches,
|
|
144
|
+
"error_code": self.error_code,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _token(value: Any, code: str, *, limit: int = 256) -> str:
|
|
149
|
+
if (
|
|
150
|
+
not isinstance(value, str)
|
|
151
|
+
or len(value) > limit
|
|
152
|
+
or TOKEN.fullmatch(value) is None
|
|
153
|
+
):
|
|
154
|
+
raise CMSSourceDeliveryBlocked(code)
|
|
155
|
+
return value
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _sha256(value: Any, code: str) -> str:
|
|
159
|
+
if not isinstance(value, str) or SHA256.fullmatch(value) is None:
|
|
160
|
+
raise CMSSourceDeliveryBlocked(code)
|
|
161
|
+
return value
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _timestamp(value: Any, code: str) -> float:
|
|
165
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
166
|
+
raise CMSSourceDeliveryBlocked(code)
|
|
167
|
+
result = float(value)
|
|
168
|
+
if result < 0 or not math.isfinite(result):
|
|
169
|
+
raise CMSSourceDeliveryBlocked(code)
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _duration(value: Any, code: str, *, maximum: float) -> float:
|
|
174
|
+
result = _timestamp(value, code)
|
|
175
|
+
if result <= 0 or result > maximum:
|
|
176
|
+
raise CMSSourceDeliveryBlocked(code)
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _attempts(value: Any, code: str) -> int:
|
|
181
|
+
if (
|
|
182
|
+
isinstance(value, bool)
|
|
183
|
+
or not isinstance(value, int)
|
|
184
|
+
or not 1 <= value <= MAX_ATTEMPTS
|
|
185
|
+
):
|
|
186
|
+
raise CMSSourceDeliveryBlocked(code)
|
|
187
|
+
return value
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _canonical(value: Any, code: str) -> str:
|
|
191
|
+
try:
|
|
192
|
+
result = json.dumps(
|
|
193
|
+
value, ensure_ascii=False, allow_nan=False, sort_keys=True,
|
|
194
|
+
separators=(",", ":"),
|
|
195
|
+
)
|
|
196
|
+
except (TypeError, ValueError, RecursionError):
|
|
197
|
+
raise CMSSourceDeliveryBlocked(code) from None
|
|
198
|
+
if not result or len(result.encode("utf-8")) > _CLIENT._HTTP.MAX_BODY_BYTES:
|
|
199
|
+
raise CMSSourceDeliveryBlocked(code)
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _hash(value: str) -> str:
|
|
204
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _payload(value: Any) -> tuple[str, str, str, str, str, str]:
|
|
208
|
+
if not isinstance(value, Mapping):
|
|
209
|
+
raise CMSSourceDeliveryBlocked("source_delivery.request_invalid")
|
|
210
|
+
try:
|
|
211
|
+
schema = value.get("schema")
|
|
212
|
+
if schema == _CLIENT._CMS.CHANGE_SCHEMA:
|
|
213
|
+
copied, raw = _CLIENT._copy_payload(value, "change")
|
|
214
|
+
operation = "change"
|
|
215
|
+
request_id = copied["event_id"]
|
|
216
|
+
else:
|
|
217
|
+
copied, raw = _CLIENT._copy_payload(value, "removal")
|
|
218
|
+
if copied["schema"] == _CLIENT._CMS.CANCELLATION_SCHEMA:
|
|
219
|
+
operation, identity = "cancellation", "cancellation_id"
|
|
220
|
+
else:
|
|
221
|
+
operation, identity = "tombstone", "tombstone_id"
|
|
222
|
+
request_id = copied[identity]
|
|
223
|
+
event_id = copied["event_id"]
|
|
224
|
+
site_id = copied["site_id"]
|
|
225
|
+
except Exception:
|
|
226
|
+
raise CMSSourceDeliveryBlocked(
|
|
227
|
+
"source_delivery.request_invalid",
|
|
228
|
+
) from None
|
|
229
|
+
payload_json = raw.decode("utf-8")
|
|
230
|
+
return (
|
|
231
|
+
operation, request_id, event_id, site_id, payload_json,
|
|
232
|
+
hashlib.sha256(raw).hexdigest(),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _error_code(error: Exception) -> tuple[str, bool]:
|
|
237
|
+
code = getattr(error, "code", None)
|
|
238
|
+
retryable = getattr(error, "retryable", None)
|
|
239
|
+
declared = getattr(error, "cms_source_client_failure", False) is True
|
|
240
|
+
if (
|
|
241
|
+
not declared
|
|
242
|
+
or not isinstance(code, str)
|
|
243
|
+
or ERROR_CODE.fullmatch(code) is None
|
|
244
|
+
or not isinstance(retryable, bool)
|
|
245
|
+
):
|
|
246
|
+
return "source_delivery.client_failure", False
|
|
247
|
+
return code, retryable
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@contextmanager
|
|
251
|
+
def _transaction(connection: sqlite3.Connection) -> Iterator[None]:
|
|
252
|
+
if connection.in_transaction:
|
|
253
|
+
raise CMSSourceDeliveryBlocked("source_delivery.transaction_nested")
|
|
254
|
+
try:
|
|
255
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
256
|
+
yield
|
|
257
|
+
connection.commit()
|
|
258
|
+
except Exception:
|
|
259
|
+
if connection.in_transaction:
|
|
260
|
+
connection.rollback()
|
|
261
|
+
raise
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class DurableCMSSourceDeliveryOutbox:
|
|
265
|
+
"""Persist and deliver source events at least once through one pinned client."""
|
|
266
|
+
|
|
267
|
+
def __init__(
|
|
268
|
+
self,
|
|
269
|
+
connection: sqlite3.Connection,
|
|
270
|
+
client: Any,
|
|
271
|
+
*,
|
|
272
|
+
clock: Callable[[], float | int] = time.time,
|
|
273
|
+
base_delay_seconds: float | int = 5,
|
|
274
|
+
max_delay_seconds: float | int = 300,
|
|
275
|
+
):
|
|
276
|
+
if not isinstance(connection, sqlite3.Connection):
|
|
277
|
+
raise TypeError("connection must be sqlite3.Connection")
|
|
278
|
+
if not callable(clock):
|
|
279
|
+
raise TypeError("clock must be callable")
|
|
280
|
+
digest = getattr(client, "expected_capabilities_sha256", None)
|
|
281
|
+
if (
|
|
282
|
+
not callable(getattr(client, "submit_change", None))
|
|
283
|
+
or not callable(getattr(client, "submit_removal", None))
|
|
284
|
+
or not isinstance(digest, str)
|
|
285
|
+
or SHA256.fullmatch(digest) is None
|
|
286
|
+
):
|
|
287
|
+
raise TypeError("client must provide the pinned source operations")
|
|
288
|
+
timeout = getattr(client, "timeout", None)
|
|
289
|
+
if (
|
|
290
|
+
isinstance(timeout, bool)
|
|
291
|
+
or not isinstance(timeout, (int, float))
|
|
292
|
+
or not math.isfinite(float(timeout))
|
|
293
|
+
or not 0 < float(timeout) <= 300
|
|
294
|
+
):
|
|
295
|
+
raise TypeError("client timeout is invalid")
|
|
296
|
+
self.connection = connection
|
|
297
|
+
self.connection.row_factory = sqlite3.Row
|
|
298
|
+
self.client = client
|
|
299
|
+
self.capabilities_sha256 = digest
|
|
300
|
+
self.timeout = float(timeout)
|
|
301
|
+
self.clock = clock
|
|
302
|
+
self.base_delay_seconds = _duration(
|
|
303
|
+
base_delay_seconds, "source_delivery.delay_invalid",
|
|
304
|
+
maximum=MAX_DELAY_SECONDS,
|
|
305
|
+
)
|
|
306
|
+
self.max_delay_seconds = _duration(
|
|
307
|
+
max_delay_seconds, "source_delivery.delay_invalid",
|
|
308
|
+
maximum=MAX_DELAY_SECONDS,
|
|
309
|
+
)
|
|
310
|
+
if self.base_delay_seconds > self.max_delay_seconds:
|
|
311
|
+
raise CMSSourceDeliveryBlocked("source_delivery.delay_invalid")
|
|
312
|
+
self._initialize()
|
|
313
|
+
|
|
314
|
+
def _now(self, value: float | int | None = None) -> float:
|
|
315
|
+
try:
|
|
316
|
+
supplied = self.clock() if value is None else value
|
|
317
|
+
except Exception:
|
|
318
|
+
raise CMSSourceDeliveryBlocked(
|
|
319
|
+
"source_delivery.time_invalid",
|
|
320
|
+
) from None
|
|
321
|
+
return _timestamp(supplied, "source_delivery.time_invalid")
|
|
322
|
+
|
|
323
|
+
def _initialize(self) -> None:
|
|
324
|
+
with _transaction(self.connection):
|
|
325
|
+
self.connection.execute("""
|
|
326
|
+
CREATE TABLE IF NOT EXISTS cms_source_delivery_meta (
|
|
327
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
328
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 1)
|
|
329
|
+
)
|
|
330
|
+
""")
|
|
331
|
+
self.connection.execute("""
|
|
332
|
+
INSERT OR IGNORE INTO cms_source_delivery_meta VALUES (1, 1)
|
|
333
|
+
""")
|
|
334
|
+
self.connection.execute("""
|
|
335
|
+
CREATE TABLE IF NOT EXISTS cms_source_delivery_outbox (
|
|
336
|
+
operation TEXT NOT NULL CHECK (
|
|
337
|
+
operation IN ('change', 'cancellation', 'tombstone')
|
|
338
|
+
),
|
|
339
|
+
request_id TEXT NOT NULL,
|
|
340
|
+
event_id TEXT NOT NULL,
|
|
341
|
+
site_id TEXT NOT NULL,
|
|
342
|
+
payload_json TEXT NOT NULL,
|
|
343
|
+
payload_sha256 TEXT NOT NULL,
|
|
344
|
+
capabilities_sha256 TEXT NOT NULL,
|
|
345
|
+
status TEXT NOT NULL CHECK (
|
|
346
|
+
status IN (
|
|
347
|
+
'pending', 'leased', 'retry_wait',
|
|
348
|
+
'succeeded', 'failed'
|
|
349
|
+
)
|
|
350
|
+
),
|
|
351
|
+
attempts INTEGER NOT NULL CHECK (attempts >= 0),
|
|
352
|
+
delivery_max_attempts INTEGER NOT NULL CHECK (
|
|
353
|
+
delivery_max_attempts BETWEEN 1 AND 20
|
|
354
|
+
),
|
|
355
|
+
source_max_attempts INTEGER NOT NULL CHECK (
|
|
356
|
+
source_max_attempts BETWEEN 1 AND 20
|
|
357
|
+
),
|
|
358
|
+
next_attempt_at REAL NOT NULL,
|
|
359
|
+
lease_owner TEXT,
|
|
360
|
+
lease_token TEXT,
|
|
361
|
+
lease_expires_at REAL,
|
|
362
|
+
last_error_code TEXT,
|
|
363
|
+
response_sha256 TEXT,
|
|
364
|
+
created_at REAL NOT NULL,
|
|
365
|
+
updated_at REAL NOT NULL,
|
|
366
|
+
PRIMARY KEY (operation, request_id),
|
|
367
|
+
CHECK (
|
|
368
|
+
(status = 'leased' AND lease_owner IS NOT NULL
|
|
369
|
+
AND lease_token IS NOT NULL
|
|
370
|
+
AND lease_expires_at IS NOT NULL)
|
|
371
|
+
OR
|
|
372
|
+
(status <> 'leased' AND lease_owner IS NULL
|
|
373
|
+
AND lease_token IS NULL
|
|
374
|
+
AND lease_expires_at IS NULL)
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
""")
|
|
378
|
+
self.connection.execute("""
|
|
379
|
+
CREATE INDEX IF NOT EXISTS cms_source_delivery_due
|
|
380
|
+
ON cms_source_delivery_outbox (
|
|
381
|
+
status, next_attempt_at, created_at, operation, request_id
|
|
382
|
+
)
|
|
383
|
+
""")
|
|
384
|
+
self._validate_schema()
|
|
385
|
+
|
|
386
|
+
def _validate_schema(self) -> None:
|
|
387
|
+
meta_columns = tuple(
|
|
388
|
+
row["name"] for row in self.connection.execute(
|
|
389
|
+
"PRAGMA table_info(cms_source_delivery_meta)"
|
|
390
|
+
).fetchall()
|
|
391
|
+
)
|
|
392
|
+
meta = self.connection.execute(
|
|
393
|
+
"SELECT singleton, schema_version FROM cms_source_delivery_meta"
|
|
394
|
+
).fetchall()
|
|
395
|
+
columns = tuple(
|
|
396
|
+
row["name"] for row in self.connection.execute(
|
|
397
|
+
"PRAGMA table_info(cms_source_delivery_outbox)"
|
|
398
|
+
).fetchall()
|
|
399
|
+
)
|
|
400
|
+
if (
|
|
401
|
+
meta_columns != _META_COLUMNS
|
|
402
|
+
or len(meta) != 1
|
|
403
|
+
or tuple(meta[0]) != (1, SCHEMA_VERSION)
|
|
404
|
+
or columns != _COLUMNS
|
|
405
|
+
):
|
|
406
|
+
raise CMSSourceDeliveryBlocked("source_delivery.schema_altered")
|
|
407
|
+
|
|
408
|
+
def enqueue_change(
|
|
409
|
+
self,
|
|
410
|
+
change: Mapping[str, Any],
|
|
411
|
+
*,
|
|
412
|
+
source_max_attempts: int = 5,
|
|
413
|
+
delivery_max_attempts: int = 5,
|
|
414
|
+
now: float | int | None = None,
|
|
415
|
+
) -> SourceDeliveryStatus:
|
|
416
|
+
return self._enqueue(
|
|
417
|
+
change,
|
|
418
|
+
source_max_attempts=source_max_attempts,
|
|
419
|
+
delivery_max_attempts=delivery_max_attempts,
|
|
420
|
+
now=now,
|
|
421
|
+
expected_operation="change",
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def enqueue_removal(
|
|
425
|
+
self,
|
|
426
|
+
removal: Mapping[str, Any],
|
|
427
|
+
*,
|
|
428
|
+
source_max_attempts: int = 5,
|
|
429
|
+
delivery_max_attempts: int = 5,
|
|
430
|
+
now: float | int | None = None,
|
|
431
|
+
) -> SourceDeliveryStatus:
|
|
432
|
+
return self._enqueue(
|
|
433
|
+
removal,
|
|
434
|
+
source_max_attempts=source_max_attempts,
|
|
435
|
+
delivery_max_attempts=delivery_max_attempts,
|
|
436
|
+
now=now,
|
|
437
|
+
expected_operation=None,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
def _enqueue(
|
|
441
|
+
self,
|
|
442
|
+
value: Mapping[str, Any],
|
|
443
|
+
*,
|
|
444
|
+
source_max_attempts: int,
|
|
445
|
+
delivery_max_attempts: int,
|
|
446
|
+
now: float | int | None,
|
|
447
|
+
expected_operation: str | None,
|
|
448
|
+
) -> SourceDeliveryStatus:
|
|
449
|
+
self._validate_schema()
|
|
450
|
+
source_max_attempts = _attempts(
|
|
451
|
+
source_max_attempts, "source_delivery.source_attempts_invalid",
|
|
452
|
+
)
|
|
453
|
+
delivery_max_attempts = _attempts(
|
|
454
|
+
delivery_max_attempts, "source_delivery.attempts_invalid",
|
|
455
|
+
)
|
|
456
|
+
current = self._now(now)
|
|
457
|
+
payload = _payload(value)
|
|
458
|
+
operation, request_id, event_id, site_id, payload_json, payload_hash = payload
|
|
459
|
+
if expected_operation is not None and operation != expected_operation:
|
|
460
|
+
raise CMSSourceDeliveryBlocked("source_delivery.request_invalid")
|
|
461
|
+
with _transaction(self.connection):
|
|
462
|
+
row = self.connection.execute("""
|
|
463
|
+
SELECT payload_sha256, capabilities_sha256,
|
|
464
|
+
delivery_max_attempts, source_max_attempts
|
|
465
|
+
FROM cms_source_delivery_outbox
|
|
466
|
+
WHERE operation = ? AND request_id = ?
|
|
467
|
+
""", (operation, request_id)).fetchone()
|
|
468
|
+
binding = (
|
|
469
|
+
payload_hash,
|
|
470
|
+
self.capabilities_sha256,
|
|
471
|
+
delivery_max_attempts,
|
|
472
|
+
source_max_attempts,
|
|
473
|
+
)
|
|
474
|
+
if row is not None and tuple(row) != binding:
|
|
475
|
+
raise CMSSourceDeliveryBlocked(
|
|
476
|
+
"source_delivery.idempotency_collision",
|
|
477
|
+
)
|
|
478
|
+
if row is None:
|
|
479
|
+
self.connection.execute("""
|
|
480
|
+
INSERT INTO cms_source_delivery_outbox (
|
|
481
|
+
operation, request_id, event_id, site_id, payload_json,
|
|
482
|
+
payload_sha256, capabilities_sha256, status, attempts,
|
|
483
|
+
delivery_max_attempts, source_max_attempts,
|
|
484
|
+
next_attempt_at, created_at, updated_at
|
|
485
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?, ?, ?, ?)
|
|
486
|
+
""", (
|
|
487
|
+
operation, request_id, event_id, site_id, payload_json,
|
|
488
|
+
payload_hash, self.capabilities_sha256,
|
|
489
|
+
delivery_max_attempts, source_max_attempts,
|
|
490
|
+
current, current, current,
|
|
491
|
+
))
|
|
492
|
+
return self.status(operation, request_id, now=current)
|
|
493
|
+
|
|
494
|
+
def _validated_row(self, row: sqlite3.Row) -> None:
|
|
495
|
+
try:
|
|
496
|
+
operation = row["operation"]
|
|
497
|
+
request_id = _token(
|
|
498
|
+
row["request_id"], "source_delivery.integrity",
|
|
499
|
+
)
|
|
500
|
+
event_id = _token(row["event_id"], "source_delivery.integrity")
|
|
501
|
+
site_id = _token(row["site_id"], "source_delivery.integrity")
|
|
502
|
+
payload_hash = _sha256(
|
|
503
|
+
row["payload_sha256"], "source_delivery.integrity",
|
|
504
|
+
)
|
|
505
|
+
_sha256(row["capabilities_sha256"], "source_delivery.integrity")
|
|
506
|
+
status = row["status"]
|
|
507
|
+
attempts = row["attempts"]
|
|
508
|
+
delivery_max = _attempts(
|
|
509
|
+
row["delivery_max_attempts"], "source_delivery.integrity",
|
|
510
|
+
)
|
|
511
|
+
_attempts(row["source_max_attempts"], "source_delivery.integrity")
|
|
512
|
+
next_attempt = _timestamp(
|
|
513
|
+
row["next_attempt_at"], "source_delivery.integrity",
|
|
514
|
+
)
|
|
515
|
+
created = _timestamp(row["created_at"], "source_delivery.integrity")
|
|
516
|
+
updated = _timestamp(row["updated_at"], "source_delivery.integrity")
|
|
517
|
+
parsed = json.loads(row["payload_json"])
|
|
518
|
+
normalized = _payload(parsed)
|
|
519
|
+
except CMSSourceDeliveryBlocked:
|
|
520
|
+
raise
|
|
521
|
+
except Exception:
|
|
522
|
+
raise CMSSourceDeliveryBlocked(
|
|
523
|
+
"source_delivery.integrity",
|
|
524
|
+
) from None
|
|
525
|
+
if (
|
|
526
|
+
operation not in OPERATIONS
|
|
527
|
+
or status not in STATUSES
|
|
528
|
+
or isinstance(attempts, bool)
|
|
529
|
+
or not isinstance(attempts, int)
|
|
530
|
+
or not 0 <= attempts <= delivery_max
|
|
531
|
+
or not created <= updated
|
|
532
|
+
or next_attempt < created
|
|
533
|
+
or normalized != (
|
|
534
|
+
operation, request_id, event_id, site_id,
|
|
535
|
+
row["payload_json"], payload_hash,
|
|
536
|
+
)
|
|
537
|
+
):
|
|
538
|
+
raise CMSSourceDeliveryBlocked("source_delivery.integrity")
|
|
539
|
+
leased = status == "leased"
|
|
540
|
+
lease_values = (
|
|
541
|
+
row["lease_owner"], row["lease_token"], row["lease_expires_at"],
|
|
542
|
+
)
|
|
543
|
+
if leased:
|
|
544
|
+
if (
|
|
545
|
+
not all(value is not None for value in lease_values)
|
|
546
|
+
or not _token(
|
|
547
|
+
row["lease_owner"], "source_delivery.integrity", limit=128,
|
|
548
|
+
)
|
|
549
|
+
or not _token(
|
|
550
|
+
row["lease_token"], "source_delivery.integrity", limit=256,
|
|
551
|
+
)
|
|
552
|
+
or _timestamp(
|
|
553
|
+
row["lease_expires_at"], "source_delivery.integrity",
|
|
554
|
+
) <= updated
|
|
555
|
+
):
|
|
556
|
+
raise CMSSourceDeliveryBlocked("source_delivery.integrity")
|
|
557
|
+
elif any(value is not None for value in lease_values):
|
|
558
|
+
raise CMSSourceDeliveryBlocked("source_delivery.integrity")
|
|
559
|
+
error = row["last_error_code"]
|
|
560
|
+
response_hash = row["response_sha256"]
|
|
561
|
+
if error is not None and (
|
|
562
|
+
not isinstance(error, str) or ERROR_CODE.fullmatch(error) is None
|
|
563
|
+
):
|
|
564
|
+
raise CMSSourceDeliveryBlocked("source_delivery.integrity")
|
|
565
|
+
if response_hash is not None:
|
|
566
|
+
_sha256(response_hash, "source_delivery.integrity")
|
|
567
|
+
if (
|
|
568
|
+
(status in {"retry_wait", "failed"}) != (error is not None)
|
|
569
|
+
or (status == "succeeded") != (response_hash is not None)
|
|
570
|
+
):
|
|
571
|
+
raise CMSSourceDeliveryBlocked("source_delivery.integrity")
|
|
572
|
+
|
|
573
|
+
def _validate_rows(self) -> list[sqlite3.Row]:
|
|
574
|
+
rows = self.connection.execute(
|
|
575
|
+
"SELECT * FROM cms_source_delivery_outbox"
|
|
576
|
+
).fetchall()
|
|
577
|
+
for row in rows:
|
|
578
|
+
self._validated_row(row)
|
|
579
|
+
return rows
|
|
580
|
+
|
|
581
|
+
def status(
|
|
582
|
+
self,
|
|
583
|
+
operation: str,
|
|
584
|
+
request_id: str,
|
|
585
|
+
*,
|
|
586
|
+
now: float | int | None = None,
|
|
587
|
+
) -> SourceDeliveryStatus:
|
|
588
|
+
self._validate_schema()
|
|
589
|
+
if operation not in OPERATIONS:
|
|
590
|
+
raise CMSSourceDeliveryBlocked("source_delivery.status_invalid")
|
|
591
|
+
request_id = _token(request_id, "source_delivery.status_invalid")
|
|
592
|
+
current = self._now(now)
|
|
593
|
+
row = self.connection.execute("""
|
|
594
|
+
SELECT * FROM cms_source_delivery_outbox
|
|
595
|
+
WHERE operation = ? AND request_id = ?
|
|
596
|
+
""", (operation, request_id)).fetchone()
|
|
597
|
+
if row is None:
|
|
598
|
+
raise CMSSourceDeliveryBlocked("source_delivery.status_not_found")
|
|
599
|
+
self._validated_row(row)
|
|
600
|
+
return SourceDeliveryStatus(
|
|
601
|
+
operation=row["operation"],
|
|
602
|
+
request_id=row["request_id"],
|
|
603
|
+
event_id=row["event_id"],
|
|
604
|
+
site_id=row["site_id"],
|
|
605
|
+
payload_sha256=row["payload_sha256"],
|
|
606
|
+
capabilities_sha256=row["capabilities_sha256"],
|
|
607
|
+
status=row["status"],
|
|
608
|
+
attempts=int(row["attempts"]),
|
|
609
|
+
delivery_max_attempts=int(row["delivery_max_attempts"]),
|
|
610
|
+
source_max_attempts=int(row["source_max_attempts"]),
|
|
611
|
+
next_attempt_at=float(row["next_attempt_at"]),
|
|
612
|
+
lease_expires_at=(
|
|
613
|
+
None
|
|
614
|
+
if row["lease_expires_at"] is None
|
|
615
|
+
else float(row["lease_expires_at"])
|
|
616
|
+
),
|
|
617
|
+
lease_expired=(
|
|
618
|
+
row["status"] == "leased"
|
|
619
|
+
and float(row["lease_expires_at"]) <= current
|
|
620
|
+
),
|
|
621
|
+
last_error_code=row["last_error_code"],
|
|
622
|
+
response_sha256=row["response_sha256"],
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
def claim(
|
|
626
|
+
self,
|
|
627
|
+
worker_id: str,
|
|
628
|
+
*,
|
|
629
|
+
lease_seconds: float | int = 600,
|
|
630
|
+
now: float | int | None = None,
|
|
631
|
+
) -> SourceDeliveryClaim | None:
|
|
632
|
+
self._validate_schema()
|
|
633
|
+
worker_id = _token(
|
|
634
|
+
worker_id, "source_delivery.worker_invalid", limit=128,
|
|
635
|
+
)
|
|
636
|
+
lease_seconds = _duration(
|
|
637
|
+
lease_seconds, "source_delivery.lease_invalid",
|
|
638
|
+
maximum=MAX_LEASE_SECONDS,
|
|
639
|
+
)
|
|
640
|
+
if lease_seconds <= self.timeout:
|
|
641
|
+
raise CMSSourceDeliveryBlocked("source_delivery.lease_too_short")
|
|
642
|
+
current = self._now(now)
|
|
643
|
+
claim = None
|
|
644
|
+
with _transaction(self.connection):
|
|
645
|
+
self._validate_rows()
|
|
646
|
+
self.connection.execute("""
|
|
647
|
+
UPDATE cms_source_delivery_outbox
|
|
648
|
+
SET status = CASE
|
|
649
|
+
WHEN attempts >= delivery_max_attempts
|
|
650
|
+
THEN 'failed' ELSE 'retry_wait' END,
|
|
651
|
+
next_attempt_at = ?, lease_owner = NULL,
|
|
652
|
+
lease_token = NULL, lease_expires_at = NULL,
|
|
653
|
+
last_error_code = 'source_delivery.lease_expired',
|
|
654
|
+
updated_at = ?
|
|
655
|
+
WHERE status = 'leased' AND lease_expires_at <= ?
|
|
656
|
+
""", (current, current, current))
|
|
657
|
+
mismatch = self.connection.execute("""
|
|
658
|
+
SELECT 1 FROM cms_source_delivery_outbox
|
|
659
|
+
WHERE status IN ('pending', 'leased', 'retry_wait')
|
|
660
|
+
AND capabilities_sha256 <> ? LIMIT 1
|
|
661
|
+
""", (self.capabilities_sha256,)).fetchone()
|
|
662
|
+
if mismatch is not None:
|
|
663
|
+
raise CMSSourceDeliveryBlocked(
|
|
664
|
+
"source_delivery.contract_changed",
|
|
665
|
+
)
|
|
666
|
+
row = self.connection.execute("""
|
|
667
|
+
SELECT * FROM cms_source_delivery_outbox
|
|
668
|
+
WHERE status IN ('pending', 'retry_wait')
|
|
669
|
+
AND next_attempt_at <= ?
|
|
670
|
+
AND attempts < delivery_max_attempts
|
|
671
|
+
ORDER BY CASE operation WHEN 'change' THEN 1 ELSE 0 END,
|
|
672
|
+
created_at, operation, request_id
|
|
673
|
+
LIMIT 1
|
|
674
|
+
""", (current,)).fetchone()
|
|
675
|
+
if row is not None:
|
|
676
|
+
self._validated_row(row)
|
|
677
|
+
lease_token = secrets.token_urlsafe(32)
|
|
678
|
+
lease_expires_at = current + lease_seconds
|
|
679
|
+
updated = self.connection.execute("""
|
|
680
|
+
UPDATE cms_source_delivery_outbox
|
|
681
|
+
SET status = 'leased', attempts = attempts + 1,
|
|
682
|
+
lease_owner = ?, lease_token = ?, lease_expires_at = ?,
|
|
683
|
+
last_error_code = NULL, updated_at = ?
|
|
684
|
+
WHERE operation = ? AND request_id = ?
|
|
685
|
+
AND status IN ('pending', 'retry_wait')
|
|
686
|
+
""", (
|
|
687
|
+
worker_id, lease_token, lease_expires_at, current,
|
|
688
|
+
row["operation"], row["request_id"],
|
|
689
|
+
))
|
|
690
|
+
if updated.rowcount != 1:
|
|
691
|
+
raise CMSSourceDeliveryBlocked(
|
|
692
|
+
"source_delivery.claim_lost",
|
|
693
|
+
)
|
|
694
|
+
claim = SourceDeliveryClaim(
|
|
695
|
+
operation=row["operation"],
|
|
696
|
+
request_id=row["request_id"],
|
|
697
|
+
event_id=row["event_id"],
|
|
698
|
+
site_id=row["site_id"],
|
|
699
|
+
payload_sha256=row["payload_sha256"],
|
|
700
|
+
capabilities_sha256=row["capabilities_sha256"],
|
|
701
|
+
attempt=int(row["attempts"]) + 1,
|
|
702
|
+
delivery_max_attempts=int(
|
|
703
|
+
row["delivery_max_attempts"]
|
|
704
|
+
),
|
|
705
|
+
source_max_attempts=int(row["source_max_attempts"]),
|
|
706
|
+
lease_owner=worker_id,
|
|
707
|
+
lease_token=lease_token,
|
|
708
|
+
lease_expires_at=lease_expires_at,
|
|
709
|
+
payload_json=row["payload_json"],
|
|
710
|
+
)
|
|
711
|
+
return claim
|
|
712
|
+
|
|
713
|
+
@staticmethod
|
|
714
|
+
def _claim(value: Any) -> SourceDeliveryClaim:
|
|
715
|
+
if not isinstance(value, SourceDeliveryClaim):
|
|
716
|
+
raise CMSSourceDeliveryBlocked("source_delivery.claim_invalid")
|
|
717
|
+
for name in (
|
|
718
|
+
"request_id", "event_id", "site_id", "lease_owner", "lease_token",
|
|
719
|
+
):
|
|
720
|
+
_token(getattr(value, name), "source_delivery.claim_invalid")
|
|
721
|
+
if (
|
|
722
|
+
value.operation not in OPERATIONS
|
|
723
|
+
or SHA256.fullmatch(value.payload_sha256) is None
|
|
724
|
+
or SHA256.fullmatch(value.capabilities_sha256) is None
|
|
725
|
+
or not 1 <= value.attempt <= value.delivery_max_attempts <= 20
|
|
726
|
+
or not 1 <= value.source_max_attempts <= 20
|
|
727
|
+
):
|
|
728
|
+
raise CMSSourceDeliveryBlocked("source_delivery.claim_invalid")
|
|
729
|
+
_timestamp(
|
|
730
|
+
value.lease_expires_at, "source_delivery.claim_invalid",
|
|
731
|
+
)
|
|
732
|
+
return value
|
|
733
|
+
|
|
734
|
+
def _current_row(
|
|
735
|
+
self, claim: SourceDeliveryClaim, current: float,
|
|
736
|
+
) -> sqlite3.Row:
|
|
737
|
+
row = self.connection.execute("""
|
|
738
|
+
SELECT * FROM cms_source_delivery_outbox
|
|
739
|
+
WHERE operation = ? AND request_id = ?
|
|
740
|
+
""", (claim.operation, claim.request_id)).fetchone()
|
|
741
|
+
if row is None:
|
|
742
|
+
raise CMSSourceDeliveryBlocked("source_delivery.claim_lost")
|
|
743
|
+
self._validated_row(row)
|
|
744
|
+
if (
|
|
745
|
+
row["status"] != "leased"
|
|
746
|
+
or row["lease_owner"] != claim.lease_owner
|
|
747
|
+
or row["lease_token"] != claim.lease_token
|
|
748
|
+
or float(row["lease_expires_at"]) != claim.lease_expires_at
|
|
749
|
+
or float(row["lease_expires_at"]) <= current
|
|
750
|
+
or int(row["attempts"]) != claim.attempt
|
|
751
|
+
or row["payload_sha256"] != claim.payload_sha256
|
|
752
|
+
or row["capabilities_sha256"] != claim.capabilities_sha256
|
|
753
|
+
):
|
|
754
|
+
raise CMSSourceDeliveryBlocked("source_delivery.claim_lost")
|
|
755
|
+
return row
|
|
756
|
+
|
|
757
|
+
def _response(self, claim: SourceDeliveryClaim, value: Any) -> str:
|
|
758
|
+
if not isinstance(value, Mapping):
|
|
759
|
+
raise CMSSourceDeliveryBlocked("source_delivery.response_invalid")
|
|
760
|
+
response_json = _canonical(
|
|
761
|
+
dict(value), "source_delivery.response_invalid",
|
|
762
|
+
)
|
|
763
|
+
try:
|
|
764
|
+
response = json.loads(response_json)
|
|
765
|
+
except (json.JSONDecodeError, RecursionError):
|
|
766
|
+
raise CMSSourceDeliveryBlocked(
|
|
767
|
+
"source_delivery.response_invalid",
|
|
768
|
+
) from None
|
|
769
|
+
expected_schema = (
|
|
770
|
+
_CLIENT._HTTP.CHANGE_RESPONSE_SCHEMA
|
|
771
|
+
if claim.operation == "change"
|
|
772
|
+
else _CLIENT._HTTP.REMOVAL_RESPONSE_SCHEMA
|
|
773
|
+
)
|
|
774
|
+
if (
|
|
775
|
+
set(response) != {
|
|
776
|
+
"schema", "operation", "request_id", "event_id",
|
|
777
|
+
"payload_sha256", "status", "attempts", "max_attempts",
|
|
778
|
+
"capabilities_sha256",
|
|
779
|
+
}
|
|
780
|
+
or response.get("schema") != expected_schema
|
|
781
|
+
or response.get("operation") != claim.operation
|
|
782
|
+
or response.get("request_id") != claim.request_id
|
|
783
|
+
or response.get("event_id") != claim.event_id
|
|
784
|
+
or response.get("payload_sha256") != claim.payload_sha256
|
|
785
|
+
or response.get("status") not in _CLIENT._HTTP.STATUSES
|
|
786
|
+
or response.get("capabilities_sha256")
|
|
787
|
+
!= claim.capabilities_sha256
|
|
788
|
+
or isinstance(response.get("attempts"), bool)
|
|
789
|
+
or not isinstance(response.get("attempts"), int)
|
|
790
|
+
or not 0 <= response["attempts"] <= claim.source_max_attempts
|
|
791
|
+
or response.get("max_attempts") != claim.source_max_attempts
|
|
792
|
+
):
|
|
793
|
+
raise CMSSourceDeliveryBlocked("source_delivery.response_invalid")
|
|
794
|
+
return response_json
|
|
795
|
+
|
|
796
|
+
def complete(
|
|
797
|
+
self,
|
|
798
|
+
claim: SourceDeliveryClaim,
|
|
799
|
+
response: Mapping[str, Any],
|
|
800
|
+
*,
|
|
801
|
+
now: float | int | None = None,
|
|
802
|
+
) -> SourceDeliveryStatus:
|
|
803
|
+
self._validate_schema()
|
|
804
|
+
claim = self._claim(claim)
|
|
805
|
+
current = self._now(now)
|
|
806
|
+
response_hash = _hash(self._response(claim, response))
|
|
807
|
+
with _transaction(self.connection):
|
|
808
|
+
self._current_row(claim, current)
|
|
809
|
+
updated = self.connection.execute("""
|
|
810
|
+
UPDATE cms_source_delivery_outbox
|
|
811
|
+
SET status = 'succeeded', lease_owner = NULL,
|
|
812
|
+
lease_token = NULL, lease_expires_at = NULL,
|
|
813
|
+
last_error_code = NULL, response_sha256 = ?, updated_at = ?
|
|
814
|
+
WHERE operation = ? AND request_id = ?
|
|
815
|
+
AND status = 'leased' AND lease_token = ?
|
|
816
|
+
""", (
|
|
817
|
+
response_hash, current, claim.operation, claim.request_id,
|
|
818
|
+
claim.lease_token,
|
|
819
|
+
))
|
|
820
|
+
if updated.rowcount != 1:
|
|
821
|
+
raise CMSSourceDeliveryBlocked("source_delivery.claim_lost")
|
|
822
|
+
return self.status(claim.operation, claim.request_id, now=current)
|
|
823
|
+
|
|
824
|
+
def _record_failure(
|
|
825
|
+
self,
|
|
826
|
+
claim: SourceDeliveryClaim,
|
|
827
|
+
code: str,
|
|
828
|
+
*,
|
|
829
|
+
retryable: bool,
|
|
830
|
+
now: float | int | None = None,
|
|
831
|
+
) -> SourceDeliveryStatus:
|
|
832
|
+
self._validate_schema()
|
|
833
|
+
claim = self._claim(claim)
|
|
834
|
+
if not isinstance(code, str) or ERROR_CODE.fullmatch(code) is None:
|
|
835
|
+
code = "source_delivery.client_failure"
|
|
836
|
+
retryable = False
|
|
837
|
+
current = self._now(now)
|
|
838
|
+
with _transaction(self.connection):
|
|
839
|
+
self._current_row(claim, current)
|
|
840
|
+
retry = retryable and claim.attempt < claim.delivery_max_attempts
|
|
841
|
+
delay = min(
|
|
842
|
+
self.max_delay_seconds,
|
|
843
|
+
self.base_delay_seconds * (2 ** (claim.attempt - 1)),
|
|
844
|
+
)
|
|
845
|
+
status = "retry_wait" if retry else "failed"
|
|
846
|
+
next_attempt = current + delay if retry else current
|
|
847
|
+
updated = self.connection.execute("""
|
|
848
|
+
UPDATE cms_source_delivery_outbox
|
|
849
|
+
SET status = ?, next_attempt_at = ?, lease_owner = NULL,
|
|
850
|
+
lease_token = NULL, lease_expires_at = NULL,
|
|
851
|
+
last_error_code = ?, response_sha256 = NULL, updated_at = ?
|
|
852
|
+
WHERE operation = ? AND request_id = ?
|
|
853
|
+
AND status = 'leased' AND lease_token = ?
|
|
854
|
+
""", (
|
|
855
|
+
status, next_attempt, code, current, claim.operation,
|
|
856
|
+
claim.request_id, claim.lease_token,
|
|
857
|
+
))
|
|
858
|
+
if updated.rowcount != 1:
|
|
859
|
+
raise CMSSourceDeliveryBlocked("source_delivery.claim_lost")
|
|
860
|
+
return self.status(claim.operation, claim.request_id, now=current)
|
|
861
|
+
|
|
862
|
+
def run_once(
|
|
863
|
+
self,
|
|
864
|
+
worker_id: str,
|
|
865
|
+
*,
|
|
866
|
+
lease_seconds: float | int = 600,
|
|
867
|
+
now: float | int | None = None,
|
|
868
|
+
) -> SourceDeliveryOutcome | None:
|
|
869
|
+
claim = self.claim(
|
|
870
|
+
worker_id, lease_seconds=lease_seconds, now=now,
|
|
871
|
+
)
|
|
872
|
+
if claim is None:
|
|
873
|
+
return None
|
|
874
|
+
try:
|
|
875
|
+
payload = json.loads(claim.payload_json)
|
|
876
|
+
if claim.operation == "change":
|
|
877
|
+
response = self.client.submit_change(
|
|
878
|
+
payload, max_attempts=claim.source_max_attempts,
|
|
879
|
+
)
|
|
880
|
+
else:
|
|
881
|
+
response = self.client.submit_removal(
|
|
882
|
+
payload, max_attempts=claim.source_max_attempts,
|
|
883
|
+
)
|
|
884
|
+
status = self.complete(claim, response)
|
|
885
|
+
except CMSSourceDeliveryBlocked as error:
|
|
886
|
+
if error.code != "source_delivery.response_invalid":
|
|
887
|
+
raise
|
|
888
|
+
status = self._record_failure(
|
|
889
|
+
claim, error.code, retryable=False,
|
|
890
|
+
)
|
|
891
|
+
except Exception as error:
|
|
892
|
+
code, retryable = _error_code(error)
|
|
893
|
+
status = self._record_failure(
|
|
894
|
+
claim, code, retryable=retryable,
|
|
895
|
+
)
|
|
896
|
+
return SourceDeliveryOutcome(
|
|
897
|
+
operation=status.operation,
|
|
898
|
+
request_id=status.request_id,
|
|
899
|
+
event_id=status.event_id,
|
|
900
|
+
status=status.status,
|
|
901
|
+
attempt=status.attempts,
|
|
902
|
+
delivery_max_attempts=status.delivery_max_attempts,
|
|
903
|
+
next_attempt_at=status.next_attempt_at,
|
|
904
|
+
error_code=status.last_error_code,
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
def health(
|
|
908
|
+
self, *, now: float | int | None = None,
|
|
909
|
+
) -> SourceDeliveryHealth:
|
|
910
|
+
self._validate_schema()
|
|
911
|
+
current = self._now(now)
|
|
912
|
+
try:
|
|
913
|
+
rows = self._validate_rows()
|
|
914
|
+
except CMSSourceDeliveryBlocked:
|
|
915
|
+
return SourceDeliveryHealth(
|
|
916
|
+
schema="blun.cms-source-delivery-health.v1",
|
|
917
|
+
status="blocked",
|
|
918
|
+
counts={}, operations={}, due=0, expired_leases=0,
|
|
919
|
+
failed=0, contract_mismatches=0,
|
|
920
|
+
error_code="source_delivery.integrity",
|
|
921
|
+
)
|
|
922
|
+
counts = {name: 0 for name in STATUSES}
|
|
923
|
+
operations = {name: 0 for name in OPERATIONS}
|
|
924
|
+
due = expired = failed = mismatches = 0
|
|
925
|
+
for row in rows:
|
|
926
|
+
counts[row["status"]] += 1
|
|
927
|
+
operations[row["operation"]] += 1
|
|
928
|
+
if (
|
|
929
|
+
row["status"] in {"pending", "retry_wait"}
|
|
930
|
+
and float(row["next_attempt_at"]) <= current
|
|
931
|
+
):
|
|
932
|
+
due += 1
|
|
933
|
+
if (
|
|
934
|
+
row["status"] == "leased"
|
|
935
|
+
and float(row["lease_expires_at"]) <= current
|
|
936
|
+
):
|
|
937
|
+
expired += 1
|
|
938
|
+
if row["status"] == "failed":
|
|
939
|
+
failed += 1
|
|
940
|
+
if (
|
|
941
|
+
row["status"] in ACTIVE_STATUSES
|
|
942
|
+
and row["capabilities_sha256"] != self.capabilities_sha256
|
|
943
|
+
):
|
|
944
|
+
mismatches += 1
|
|
945
|
+
blocked = failed > 0 or expired > 0 or mismatches > 0
|
|
946
|
+
if failed:
|
|
947
|
+
error = "source_delivery.failed"
|
|
948
|
+
elif expired:
|
|
949
|
+
error = "source_delivery.lease_expired"
|
|
950
|
+
elif mismatches:
|
|
951
|
+
error = "source_delivery.contract_changed"
|
|
952
|
+
else:
|
|
953
|
+
error = None
|
|
954
|
+
return SourceDeliveryHealth(
|
|
955
|
+
schema="blun.cms-source-delivery-health.v1",
|
|
956
|
+
status="blocked" if blocked else "ok",
|
|
957
|
+
counts=counts,
|
|
958
|
+
operations=operations,
|
|
959
|
+
due=due,
|
|
960
|
+
expired_leases=expired,
|
|
961
|
+
failed=failed,
|
|
962
|
+
contract_mismatches=mismatches,
|
|
963
|
+
error_code=error,
|
|
964
|
+
)
|