blun-king-cli 9.1.587 → 9.1.588
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -185
- package/LIESMICH.txt +51 -13
- package/README.md +44 -47
- package/agent-spine-plugin/.codex-plugin/plugin.json +16 -4
- package/agent-spine-plugin/CHANGELOG.md +37 -5
- package/agent-spine-plugin/README.md +3 -3
- package/agent-spine-plugin/blun.plugin.json +45 -10
- package/agent-spine-plugin/docs/artifact-evaluation.md +93 -0
- package/agent-spine-plugin/docs/host-integration.md +42 -27
- package/agent-spine-plugin/docs/preflight-recall.md +4 -2
- package/agent-spine-plugin/docs/session-timeline.md +97 -236
- package/agent-spine-plugin/docs/world-model.md +25 -0
- package/agent-spine-plugin/hooks/codex.json +1 -1
- package/agent-spine-plugin/hooks/hooks.json +1 -1
- package/agent-spine-plugin/package.json +1 -3
- package/agent-spine-plugin/scripts/check-hosts.js +3 -3
- package/agent-spine-plugin/scripts/release-check.js +10 -5
- package/agent-spine-plugin/scripts/run-checks.js +4 -1
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +32 -6
- package/agent-spine-plugin/src/cli-learning.js +15 -0
- package/agent-spine-plugin/src/cli.js +2 -0
- package/agent-spine-plugin/src/hook.js +32 -32
- package/agent-spine-plugin/src/lib/action-lesson-recall.js +73 -8
- package/agent-spine-plugin/src/lib/briefing.js +146 -36
- package/agent-spine-plugin/src/lib/channel-continuity.js +19 -0
- package/agent-spine-plugin/src/lib/delivery-agent-usage.js +14 -7
- package/agent-spine-plugin/src/lib/gateway-group-response.js +128 -0
- package/agent-spine-plugin/src/lib/gateway-runs.js +24 -15
- package/agent-spine-plugin/src/lib/hook-briefing-use.js +13 -3
- package/agent-spine-plugin/src/lib/hook-context.js +16 -3
- package/agent-spine-plugin/src/lib/hook-output.js +129 -5
- package/agent-spine-plugin/src/lib/hook-timeline.js +5 -3
- package/agent-spine-plugin/src/lib/indexed-memory.js +2 -2
- package/agent-spine-plugin/src/lib/learning-artifact-evaluator.js +114 -0
- package/agent-spine-plugin/src/lib/learning-context.js +11 -4
- package/agent-spine-plugin/src/lib/learning-measurements.js +2 -2
- package/agent-spine-plugin/src/lib/mcp-runtime.js +89 -3
- package/agent-spine-plugin/src/lib/mcp-source-context.js +12 -2
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +91 -8
- package/agent-spine-plugin/src/lib/mcp-world-tools.js +2 -2
- package/agent-spine-plugin/src/lib/owned-file-lock.js +20 -1
- package/agent-spine-plugin/src/lib/persona-runtime.js +2 -2
- package/agent-spine-plugin/src/lib/preflight-delivery-id.js +27 -0
- package/agent-spine-plugin/src/lib/preflight.js +4 -4
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +15 -0
- package/agent-spine-plugin/src/lib/session-timeline-contract.js +12 -4
- package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +36 -7
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +13 -10
- package/agent-spine-plugin/src/lib/session-timeline-invocation.js +1 -1
- package/agent-spine-plugin/src/lib/session-timeline-king.js +14 -0
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +18 -12
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +5 -0
- package/agent-spine-plugin/src/lib/session-timeline-query.js +2 -0
- package/agent-spine-plugin/src/lib/session-timeline-results.js +35 -10
- package/agent-spine-plugin/src/lib/session-timeline-source-open.js +30 -0
- package/agent-spine-plugin/src/lib/session-timeline.js +122 -75
- package/agent-spine-plugin/src/lib/source-roots.js +3 -2
- package/agent-spine-plugin/src/lib/task-knowledge-context.js +22 -1
- package/agent-spine-plugin/src/lib/timeline-continuation-update.js +100 -0
- package/agent-spine-plugin/src/lib/timeline-tool-guard.js +30 -7
- package/agent-spine-plugin/src/lib/timeline-user-feedback.js +217 -0
- package/agent-spine-plugin/src/lib/timeline-world-capture.js +233 -0
- package/agent-spine-plugin/src/lib/world-knowledge.js +59 -2
- package/agent-spine-plugin/src/lib/world-model.js +64 -9
- package/agent-spine-plugin/src/worker.js +13 -1
- package/bin/blun.js +43 -28
- package/bin/core-bootstrap.js +5 -4
- package/bin/king.js +43 -28
- package/bin/launcher-mode.js +1 -10
- package/bin/launcher-runtime.js +128 -295
- package/bin/managed-node.js +0 -0
- package/bin/managed-plugin-selection.cjs +0 -1
- package/bin/native-module-repair.js +0 -0
- package/bin/node-runtime.js +0 -0
- package/bin/node-version.js +0 -0
- package/bin/plugin-bootstrap.js +56 -120
- package/bin/private-paths.js +11 -34
- package/bin/standard-tools-bootstrap.js +34 -114
- package/bin/turn-thinking-policy.cjs +3 -11
- package/bin/update-copy.js +200 -0
- package/bin/update-lease.js +0 -0
- package/bin/update-notice.js +136 -289
- package/bin/verify-agent-behavior.cjs +122 -0
- package/bin/verify-agent-components.cjs +104 -0
- package/bin/verify-bundled-agent-sources.cjs +57 -0
- package/blun.mjs +143076 -135288
- package/bundled-agent-sources.json +701 -0
- package/package.json +12 -15
- package/standard-skills/translate-native/README.md +1293 -0
- package/standard-skills/translate-native/SKILL.md +172 -22
- package/standard-skills/translate-native/VERSION +1 -1
- package/standard-skills/translate-native/agents/openai.yaml +18 -0
- package/standard-skills/translate-native/assets/icon.svg +8 -0
- package/standard-skills/translate-native/docs/BLUN_CODE_INTEGRATION.md +76 -0
- package/standard-skills/translate-native/docs/PREMORTEM.md +489 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +2035 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1302 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +136 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HEALTH_HTTP.md +130 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +175 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +86 -0
- package/standard-skills/translate-native/integrations/AGENT_RULES.md +32 -0
- package/standard-skills/translate-native/integrations/adapters/blun-code-language-guard.js +514 -0
- package/standard-skills/translate-native/integrations/adapters/node-language-guard.js +230 -0
- package/standard-skills/translate-native/integrations/audit_log.py +327 -0
- package/standard-skills/translate-native/integrations/claude_language_hook.js +1536 -0
- package/standard-skills/translate-native/integrations/commercial_localization_profile.py +42 -0
- package/standard-skills/translate-native/integrations/delivery-policy.example.json +28 -0
- package/standard-skills/translate-native/integrations/enforced_delivery.py +543 -0
- package/standard-skills/translate-native/integrations/guard_service.py +435 -0
- package/standard-skills/translate-native/integrations/language_gateway.py +67 -0
- package/standard-skills/translate-native/integrations/mcp_auth_headers.py +198 -0
- package/standard-skills/translate-native/integrations/mcp_http_gateway.py +429 -0
- package/standard-skills/translate-native/integrations/non_language_html_entities.js +1485 -0
- package/standard-skills/translate-native/integrations/pre_output_guard.py +65 -0
- package/standard-skills/translate-native/integrations/task_router.py +101 -0
- package/standard-skills/translate-native/integrations/website_localization.py +401 -0
- package/standard-skills/translate-native/integrations/website_localization_api.py +581 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark.py +1885 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_campaign.py +1772 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_candidate.py +506 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_http.py +400 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_review_store.py +781 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_reviewer_http.py +500 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_runtime.py +1107 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_suite.py +463 -0
- package/standard-skills/translate-native/integrations/website_localization_cms.py +2835 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_client.py +875 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_dispatch.py +805 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_http.py +588 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_lifecycle_monitor.py +991 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +1441 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +414 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +1073 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_removal_dispatch.py +865 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +583 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +964 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +665 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +1153 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +675 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +1125 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification.py +674 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +444 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +1469 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +1142 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +634 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +804 -0
- package/standard-skills/translate-native/integrations/website_localization_deepl_baseline.py +922 -0
- package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +482 -0
- package/standard-skills/translate-native/integrations/website_localization_health.py +1541 -0
- package/standard-skills/translate-native/integrations/website_localization_health_http.py +372 -0
- package/standard-skills/translate-native/integrations/website_localization_http_provider.py +297 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_http.py +479 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_intake.py +363 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_queue.py +1449 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_store.py +420 -0
- package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +235 -0
- package/standard-skills/translate-native/integrations/website_localization_queue.py +671 -0
- package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +516 -0
- package/standard-skills/translate-native/integrations/website_localization_release.py +928 -0
- package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +1008 -0
- package/standard-skills/translate-native/integrations/website_localization_runner.py +276 -0
- package/standard-skills/translate-native/integrations/website_localization_runtime.py +862 -0
- package/standard-skills/translate-native/integrations/website_localization_service.py +350 -0
- package/standard-skills/translate-native/integrations/website_localization_supervisor.py +511 -0
- package/standard-skills/translate-native/integrations/website_localization_worker.py +663 -0
- package/standard-skills/translate-native/provenance.json +3 -4
- package/standard-skills/translate-native/references/commercial-localization.md +177 -0
- package/standard-skills/translate-native/scripts/blun_language_guard.py +7 -1
- package/standard-skills/translate-native/scripts/check_commercial_review.py +80 -0
- package/standard-skills/translate-native/scripts/commercial_localization_profile.py +333 -0
- package/standard-tools/language-guard/LICENSE +21 -0
- package/standard-tools/language-guard/VERSION +1 -0
- package/standard-tools/language-guard/blun_language_guard.py +7 -1
- package/standard-tools/language-guard/check_commercial_review.py +80 -0
- package/standard-tools/language-guard/commercial_localization_profile.py +333 -0
- package/standard-tools/language-guard/language_gateway.py +62 -0
- package/standard-tools/language-guard/pre_output_guard.py +64 -0
- package/standard-tools/language-guard/provenance.json +4 -11
- package/standard-tools/manifest.json +34 -11
- package/telegram-plugin/commands/access.md +2 -10
- package/telegram-plugin/dist/bridge.mjs +64041 -687
- package/telegram-plugin/dist/mcp-server.mjs +72810 -9027
- package/telegram-plugin/dist/noise.mjs +28 -63511
- package/agent-spine-plugin/CONTRIBUTING.md +0 -52
- package/agent-spine-plugin/SECURITY.md +0 -47
- package/agent-spine-plugin/docs/assignment-continuation.md +0 -48
- package/agent-spine-plugin/docs/releasing.md +0 -85
- package/agent-spine-plugin/docs/structured-completion.md +0 -67
- package/bin/abort-listener-policy.cjs +0 -43
- package/bin/active-steer-priority-policy.cjs +0 -24
- package/bin/agent-api-http-adapter.mjs +0 -446
- package/bin/agent-api-private-http-server.mjs +0 -288
- package/bin/agent-api-runtime.mjs +0 -252
- package/bin/agent-api-service-environment.mjs +0 -236
- package/bin/agent-api-service-host.mjs +0 -209
- package/bin/agent-api-service-process.mjs +0 -171
- package/bin/agent-api-session-registry.mjs +0 -428
- package/bin/agent-api-tool-broker.cjs +0 -248
- package/bin/agent-api-turn-controller.mjs +0 -461
- package/bin/agent-api-usage-journal.cjs +0 -259
- package/bin/agent-resume-snapshot.cjs +0 -241
- package/bin/agentspine-king-goal-inbox.mjs +0 -111
- package/bin/agentspine-king-goal-intake.mjs +0 -106
- package/bin/approval-rejection-stop.cjs +0 -15
- package/bin/assistant-message-offload-policy.cjs +0 -284
- package/bin/baseline-skill-performance-policy.cjs +0 -39
- package/bin/bash-search-scope-policy.cjs +0 -49
- package/bin/codebase-search-runtime.cjs +0 -23
- package/bin/cognitive-action-checkpoint.cjs +0 -1104
- package/bin/cognitive-attention-delivery.cjs +0 -76
- package/bin/cognitive-attention-policy.cjs +0 -143
- package/bin/cognitive-attention-runtime.cjs +0 -91
- package/bin/cognitive-context-projection.cjs +0 -73
- package/bin/cognitive-cross-portal-acceptance.cjs +0 -443
- package/bin/cognitive-effective-view.cjs +0 -77
- package/bin/cognitive-focus-projection.cjs +0 -206
- package/bin/cognitive-focus-scope.cjs +0 -37
- package/bin/cognitive-goal-autostart-policy.cjs +0 -72
- package/bin/cognitive-goal-time-trigger-controller.cjs +0 -146
- package/bin/cognitive-memory-adapter.cjs +0 -282
- package/bin/cognitive-memory-command.cjs +0 -293
- package/bin/cognitive-memory-provider.cjs +0 -92
- package/bin/cognitive-salience-policy.cjs +0 -159
- package/bin/cognitive-state-store.cjs +0 -508
- package/bin/cognitive-turn-lifecycle.cjs +0 -624
- package/bin/cognitive-work-focus.cjs +0 -180
- package/bin/compaction-history-archive.cjs +0 -166
- package/bin/compaction-history-startup.cjs +0 -50
- package/bin/compaction-model-policy.cjs +0 -31
- package/bin/compaction-stage-policy.cjs +0 -21
- package/bin/compaction-transaction-policy.cjs +0 -122
- package/bin/config-write-dedup-policy.cjs +0 -27
- package/bin/context-budget-ledger.cjs +0 -31
- package/bin/context-doctor-policy.cjs +0 -70
- package/bin/context-insight-policy.cjs +0 -36
- package/bin/context-performance-policy.cjs +0 -19
- package/bin/context-pressure-policy.cjs +0 -20
- package/bin/cron-run-output.cjs +0 -45
- package/bin/cron-run-store.cjs +0 -145
- package/bin/curiosity-scout-policy.cjs +0 -49
- package/bin/default-model-output-budget-policy.cjs +0 -28
- package/bin/durable-task-resume-policy.cjs +0 -130
- package/bin/durable-task-resume-runtime.cjs +0 -117
- package/bin/durable-task-resume-store.cjs +0 -88
- package/bin/editable-tool-approval-policy.cjs +0 -540
- package/bin/editable-tool-approval-runtime.cjs +0 -99
- package/bin/effective-system-prompt-cache-policy.cjs +0 -33
- package/bin/error-memory-performance-policy.cjs +0 -113
- package/bin/file-observation-policy.cjs +0 -133
- package/bin/foreground-output-capture-policy.cjs +0 -41
- package/bin/generated-source-health.cjs +0 -142
- package/bin/glob-pattern-policy.cjs +0 -13
- package/bin/goal-completion-evidence-policy.cjs +0 -120
- package/bin/grep-output-limit-policy.cjs +0 -39
- package/bin/historical-media-projection-policy.cjs +0 -48
- package/bin/history-offload-pressure-policy.cjs +0 -33
- package/bin/html-to-research-markdown.cjs +0 -147
- package/bin/identity-context-policy.cjs +0 -764
- package/bin/identity-journal-policy.cjs +0 -107
- package/bin/input-draft-persistence.cjs +0 -77
- package/bin/king-tui-function-contract.json +0 -33
- package/bin/launcher-restart-policy.cjs +0 -150
- package/bin/live-response-repetition-guard.cjs +0 -196
- package/bin/llm-config-log-dedup-policy.cjs +0 -76
- package/bin/loop-event-record-policy.cjs +0 -174
- package/bin/managed-context-startup-policy.cjs +0 -27
- package/bin/media-activity-layout-policy.cjs +0 -34
- package/bin/media-auto-retrieval-policy.cjs +0 -90
- package/bin/media-result-policy.cjs +0 -59
- package/bin/micro-compaction-policy.cjs +0 -145
- package/bin/mistake-relevance-policy.cjs +0 -319
- package/bin/model-retry-progress-policy.cjs +0 -46
- package/bin/native-large-file-io.cjs +0 -42
- package/bin/native-runtime-cache.cjs +0 -76
- package/bin/natural-presence-policy.cjs +0 -28
- package/bin/noninteractive-shell-env-policy.cjs +0 -19
- package/bin/observer-hooks.cjs +0 -14
- package/bin/outbound-claim-provenance.cjs +0 -150
- package/bin/oversized-context-offload-policy.cjs +0 -86
- package/bin/pending-media-policy.cjs +0 -182
- package/bin/pending-token-estimate-policy.cjs +0 -41
- package/bin/personal-memory-consent-policy.cjs +0 -72
- package/bin/personal-memory-performance-policy.cjs +0 -12
- package/bin/personality-choice-policy.cjs +0 -101
- package/bin/personality-memory-adapter.cjs +0 -379
- package/bin/personality-mode.cjs +0 -46
- package/bin/personality-setup-policy.cjs +0 -197
- package/bin/proactive-compaction-policy.cjs +0 -25
- package/bin/profile-identity-resolution.cjs +0 -136
- package/bin/profile-runtime.cjs +0 -318
- package/bin/profile-tool-exclusion-policy.cjs +0 -37
- package/bin/programmatic-context-isolation.cjs +0 -25
- package/bin/programmatic-tool-runtime.mjs +0 -627
- package/bin/provider-idle-timeout-policy.cjs +0 -14
- package/bin/provider-model-refresh-deadline.cjs +0 -53
- package/bin/provider-model-refresh-policy.cjs +0 -107
- package/bin/rate-limit-recovery-policy.cjs +0 -47
- package/bin/read-batch-policy.cjs +0 -32
- package/bin/read-continuation-policy.cjs +0 -59
- package/bin/recurring-cron-history-policy.cjs +0 -124
- package/bin/relationship-continuity-policy.cjs +0 -143
- package/bin/relationship-curiosity-policy.cjs +0 -107
- package/bin/relationship-learning-policy.cjs +0 -168
- package/bin/release-artifact-freeze-policy.cjs +0 -30
- package/bin/reload-plugin-bootstrap.cjs +0 -18
- package/bin/reload-queue-policy.cjs +0 -38
- package/bin/repeated-assistant-response-policy.cjs +0 -232
- package/bin/repeated-injection-projection.cjs +0 -107
- package/bin/repeated-user-message-projection.cjs +0 -8
- package/bin/research-page-result.cjs +0 -74
- package/bin/retry-checkpoint-policy.cjs +0 -13
- package/bin/runtime-exit-ledger.cjs +0 -144
- package/bin/scoped-cron-run-policy.cjs +0 -358
- package/bin/session-checkpoint-policy.cjs +0 -25
- package/bin/session-compaction-policy.cjs +0 -84
- package/bin/session-replay-policy.cjs +0 -20
- package/bin/session-replay-window-policy.cjs +0 -40
- package/bin/session-resume-checkpoint.cjs +0 -254
- package/bin/session-scrollback-archive.cjs +0 -229
- package/bin/skill-activation-performance-policy.cjs +0 -69
- package/bin/skill-listing-performance-policy.cjs +0 -92
- package/bin/soul-organization-policy.cjs +0 -78
- package/bin/soul-preservation-policy.cjs +0 -20
- package/bin/startup-preferences.cjs +0 -131
- package/bin/streaming-flush-performance-policy.cjs +0 -28
- package/bin/structured-agent-swarm-output.cjs +0 -325
- package/bin/structured-subagent-output.cjs +0 -252
- package/bin/subagent-context-fork-policy.cjs +0 -155
- package/bin/subagent-max-tokens-handoff-policy.cjs +0 -69
- package/bin/subagent-parent-responsiveness.cjs +0 -19
- package/bin/subagent-skill-policy.cjs +0 -206
- package/bin/subagent-timeout-policy.cjs +0 -182
- package/bin/subagent-tool-policy.cjs +0 -60
- package/bin/subagent-usage-rollup-policy.cjs +0 -29
- package/bin/system-prompt-context-policy.cjs +0 -124
- package/bin/system-prompt-token-cache-policy.cjs +0 -60
- package/bin/telegram-addressed-focus.cjs +0 -55
- package/bin/telegram-addressed-priority.cjs +0 -12
- package/bin/telegram-approval-relay.cjs +0 -290
- package/bin/telegram-bot-priority.cjs +0 -17
- package/bin/telegram-console-status-policy.cjs +0 -174
- package/bin/telegram-context-projection-policy.cjs +0 -141
- package/bin/telegram-delivery-lifecycle.cjs +0 -125
- package/bin/telegram-direct-focus-policy.cjs +0 -273
- package/bin/telegram-mcp-compatibility.cjs +0 -49
- package/bin/telegram-media-delivery-policy.cjs +0 -42
- package/bin/telegram-private-conversation-policy.cjs +0 -185
- package/bin/telegram-queue-handoff-policy.cjs +0 -73
- package/bin/telegram-remote-status-policy.cjs +0 -120
- package/bin/telegram-session-queue-runtime.mjs +0 -306
- package/bin/telegram-text-chunk-policy.cjs +0 -63
- package/bin/telegram-truncated-reply-policy.cjs +0 -37
- package/bin/telegram-urgent-policy.cjs +0 -45
- package/bin/telemetry-spool-policy.cjs +0 -57
- package/bin/thinking-activity-status-policy.cjs +0 -132
- package/bin/thinking-only-guard.cjs +0 -80
- package/bin/todo-list-turn-policy.cjs +0 -131
- package/bin/tool-call-loop-policy.cjs +0 -51
- package/bin/tool-file-persistence.cjs +0 -141
- package/bin/tool-result-offload-policy.cjs +0 -359
- package/bin/tool-result-offload-telemetry.cjs +0 -12
- package/bin/tool-schema-token-cache-policy.cjs +0 -41
- package/bin/tool-stream-preview-policy.cjs +0 -9
- package/bin/tui-functional-contract.cjs +0 -55
- package/bin/turn-tool-performance-policy.cjs +0 -486
- package/bin/usage-cache-efficiency-policy.cjs +0 -26
- package/bin/user-home-path-policy.cjs +0 -13
- package/bin/user-message-offload-policy.cjs +0 -103
- package/bin/user-prompt-hook-origin-policy.cjs +0 -34
- package/bin/user-tool-record-policy.cjs +0 -7
- package/bin/validated-learning-insight-policy.cjs +0 -58
- package/bin/validated-learning-outcome-trace.cjs +0 -107
- package/bin/validated-learning-performance-policy.cjs +0 -53
- package/bin/validated-learning-signal.cjs +0 -463
- package/bin/windows-bash-dialect-policy.cjs +0 -25
- package/bin/windows-node-crash-dump.cjs +0 -110
- package/bin/write-continuation-policy.cjs +0 -69
- package/codebase-index/README.md +0 -82
- package/codebase-index/codebase_index.py +0 -470
- package/standard-skills/agent-browser/SKILL.md +0 -19
- package/standard-skills/agent-browser/references/runtime.md +0 -8
- package/standard-skills/blun-session-inspector/SKILL.md +0 -41
- package/standard-skills/blun-session-inspector/scripts/inspect-session.cjs +0 -437
- package/standard-skills/design-taste-frontend/SKILL.md +0 -1206
- package/standard-skills/full-output-enforcement/SKILL.md +0 -49
- package/standard-skills/high-end-visual-design/SKILL.md +0 -98
- package/standard-skills/image-to-code/SKILL.md +0 -1228
- package/standard-skills/industrial-brutalist-ui/SKILL.md +0 -92
- package/standard-skills/minimalist-ui/SKILL.md +0 -85
- package/standard-skills/motion-design-taste/SKILL.md +0 -74
- package/standard-skills/playwright-testing/SKILL.md +0 -19
- package/standard-skills/playwright-testing/references/runtime.md +0 -7
- package/standard-skills/premortem/SKILL.md +0 -148
- package/standard-skills/redesign-existing-projects/SKILL.md +0 -178
- package/standard-skills/research-evidence/SKILL.md +0 -39
- package/standard-skills/research-evidence/references/evidence-format.md +0 -104
- package/standard-skills/research-evidence/scripts/evidence-collection.cjs +0 -260
- package/standard-skills/research-evidence/scripts/score-report.cjs +0 -130
- package/standard-skills/screenshot-lesen/SKILL.md +0 -52
- package/standard-skills/stitch-design-taste/DESIGN.md +0 -121
- package/standard-skills/stitch-design-taste/SKILL.md +0 -184
- package/standard-skills/telegram-channel/SKILL.md +0 -18
- package/standard-skills/telegram-channel/references/runtime.md +0 -7
- package/standard-skills/venture-flywheel/SKILL.md +0 -32
- package/standard-skills/venture-flywheel/identity/project-identity.cjs +0 -146
- package/standard-skills/venture-flywheel/policy/capability-engine.cjs +0 -114
- package/standard-skills/venture-flywheel/policy/repository-trust.cjs +0 -229
- package/standard-skills/venture-flywheel/references/BEISPIELE-phase0.md +0 -146
- package/standard-skills/venture-flywheel/references/CAPABILITY-MAP.md +0 -34
- package/standard-skills/venture-flywheel/references/SPEC-phase0-identity-trust.md +0 -77
- package/standard-skills/venture-flywheel/references/SPEC-phase0-state-events.md +0 -93
- package/standard-skills/venture-flywheel/schemas/capability-decision.schema.json +0 -13
- package/standard-skills/venture-flywheel/schemas/execution-event.schema.json +0 -44
- package/standard-skills/venture-flywheel/schemas/project-identity.schema.json +0 -32
- package/standard-skills/venture-flywheel/schemas/repository-trust.schema.json +0 -57
- package/standard-skills/venture-flywheel/schemas/run-transition.schema.json +0 -59
- package/standard-skills/venture-flywheel/state/execution-event.cjs +0 -191
- package/standard-skills/venture-flywheel/state/task-state-machine.cjs +0 -190
- package/standard-skills/web-lesen/SKILL.md +0 -73
- package/standard-skills/web-lesen/scripts/crawl_public.py +0 -379
- package/standard-skills/windows-mcp/SKILL.md +0 -19
- package/standard-skills/windows-mcp/references/runtime.md +0 -9
- package/telegram-plugin/DELIVERY.md +0 -36
- package/telegram-plugin/bin/telegram-approval-relay.cjs +0 -290
- package/telegram-plugin/bin/telegram-console-status-policy.cjs +0 -175
- package/telegram-plugin/bin/telegram-delivery-lifecycle.cjs +0 -125
- package/telegram-plugin/bin/telegram-direct-reply-policy.cjs +0 -48
- package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +0 -122
- package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +0 -186
- package/telegram-plugin/bin/telegram-remote-status-policy.cjs +0 -121
- package/telegram-plugin/bin/telegram-reply-parts.cjs +0 -149
- package/telegram-plugin/bin/telegram-text-chunk-policy.cjs +0 -63
- package/telegram-plugin/bin/telegram-typing-keepalive.cjs +0 -89
- package/telegram-plugin/compat/mcp-server-fa511cd1.mjs +0 -73825
- /package/{bin → scripts}/fix-node-pty-perms.js +0 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Durable source-side outbox for CMS localization change events.
|
|
3
|
+
|
|
4
|
+
The website host owns the SQLite connection and the HTTPS client. One leased
|
|
5
|
+
attempt submits one immutable event. A crash after remote acceptance safely
|
|
6
|
+
replays the same event identity after lease expiry; the server remains the
|
|
7
|
+
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
|
+
from contextlib import contextmanager
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Iterator, Mapping
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
SCHEMA_VERSION = 1
|
|
27
|
+
MAX_ATTEMPTS = 20
|
|
28
|
+
MAX_LEASE_SECONDS = 86_400.0
|
|
29
|
+
MAX_DELAY_SECONDS = 86_400.0
|
|
30
|
+
STATUSES = ("pending", "leased", "retry_wait", "succeeded", "failed")
|
|
31
|
+
TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
|
|
32
|
+
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
33
|
+
ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
|
|
34
|
+
_COLUMNS = (
|
|
35
|
+
"event_id", "payload_json", "payload_sha256", "status", "attempts",
|
|
36
|
+
"max_attempts", "next_attempt_at", "lease_owner", "lease_token",
|
|
37
|
+
"lease_expires_at", "last_error_code", "last_error_detail_hash",
|
|
38
|
+
"remote_plan_id", "remote_job_count", "remote_status",
|
|
39
|
+
"response_sha256", "created_at", "updated_at",
|
|
40
|
+
)
|
|
41
|
+
_META_COLUMNS = ("singleton", "schema_version")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _load_module(name: str, path: Path):
|
|
45
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
46
|
+
if spec is None or spec.loader is None:
|
|
47
|
+
raise RuntimeError(f"cannot load CMS dispatch dependency: {path.name}")
|
|
48
|
+
module = importlib.util.module_from_spec(spec)
|
|
49
|
+
sys.modules[spec.name] = module
|
|
50
|
+
spec.loader.exec_module(module)
|
|
51
|
+
return module
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
55
|
+
_CLIENT = _load_module(
|
|
56
|
+
"blun_website_localization_cms_dispatch_client",
|
|
57
|
+
_ROOT / "integrations" / "website_localization_cms_client.py",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CMSDispatchBlocked(RuntimeError):
|
|
62
|
+
"""Stable, content-free failure of the durable source dispatcher."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, code: str):
|
|
65
|
+
if not isinstance(code, str) or ERROR_CODE.fullmatch(code) is None:
|
|
66
|
+
raise ValueError("CMS dispatch error code is invalid")
|
|
67
|
+
super().__init__(code)
|
|
68
|
+
self.code = code
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class ChangeDispatchClaim:
|
|
73
|
+
event_id: str
|
|
74
|
+
payload_sha256: str
|
|
75
|
+
attempt: int
|
|
76
|
+
max_attempts: int
|
|
77
|
+
lease_owner: str
|
|
78
|
+
lease_token: str
|
|
79
|
+
lease_expires_at: float
|
|
80
|
+
payload_json: str = field(repr=False, compare=False)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class ChangeDispatchStatus:
|
|
85
|
+
event_id: str
|
|
86
|
+
payload_sha256: str
|
|
87
|
+
status: str
|
|
88
|
+
attempts: int
|
|
89
|
+
max_attempts: int
|
|
90
|
+
next_attempt_at: float
|
|
91
|
+
lease_expires_at: float | None
|
|
92
|
+
lease_expired: bool
|
|
93
|
+
last_error_code: str | None
|
|
94
|
+
last_error_detail_hash: str | None
|
|
95
|
+
remote_plan_id: str | None
|
|
96
|
+
remote_job_count: int | None
|
|
97
|
+
remote_status: str | None
|
|
98
|
+
response_sha256: str | None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class ChangeDispatchOutcome:
|
|
103
|
+
event_id: str
|
|
104
|
+
status: str
|
|
105
|
+
attempt: int
|
|
106
|
+
max_attempts: int
|
|
107
|
+
next_attempt_at: float
|
|
108
|
+
error_code: str | None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class ChangeDispatchHealth:
|
|
113
|
+
status: str
|
|
114
|
+
counts: dict[str, int]
|
|
115
|
+
due: int
|
|
116
|
+
expired_leases: int
|
|
117
|
+
failed: int
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _token(value: Any, code: str, *, limit: int = 256) -> str:
|
|
121
|
+
if (
|
|
122
|
+
not isinstance(value, str)
|
|
123
|
+
or len(value) > limit
|
|
124
|
+
or TOKEN.fullmatch(value) is None
|
|
125
|
+
):
|
|
126
|
+
raise CMSDispatchBlocked(code)
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _timestamp(value: Any, code: str) -> float:
|
|
131
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
132
|
+
raise CMSDispatchBlocked(code)
|
|
133
|
+
result = float(value)
|
|
134
|
+
if result < 0 or not math.isfinite(result):
|
|
135
|
+
raise CMSDispatchBlocked(code)
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _duration(value: Any, code: str, *, maximum: float) -> float:
|
|
140
|
+
result = _timestamp(value, code)
|
|
141
|
+
if result <= 0 or result > maximum:
|
|
142
|
+
raise CMSDispatchBlocked(code)
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _canonical_json(value: Any, code: str) -> str:
|
|
147
|
+
try:
|
|
148
|
+
result = json.dumps(
|
|
149
|
+
value, ensure_ascii=False, allow_nan=False, sort_keys=True,
|
|
150
|
+
separators=(",", ":"),
|
|
151
|
+
)
|
|
152
|
+
except (TypeError, ValueError, RecursionError):
|
|
153
|
+
raise CMSDispatchBlocked(code) from None
|
|
154
|
+
if not result or len(result.encode("utf-8")) > _CLIENT._API.MAX_MESSAGE_BYTES:
|
|
155
|
+
raise CMSDispatchBlocked(code)
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _hash(value: str) -> str:
|
|
160
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _change(value: Any) -> tuple[str, str, str]:
|
|
164
|
+
if not isinstance(value, Mapping):
|
|
165
|
+
raise CMSDispatchBlocked("dispatch.change_invalid")
|
|
166
|
+
payload_json = _canonical_json(dict(value), "dispatch.change_invalid")
|
|
167
|
+
try:
|
|
168
|
+
copied = json.loads(payload_json)
|
|
169
|
+
except (json.JSONDecodeError, RecursionError):
|
|
170
|
+
raise CMSDispatchBlocked("dispatch.change_invalid") from None
|
|
171
|
+
if (
|
|
172
|
+
not isinstance(copied, dict)
|
|
173
|
+
or copied.get("schema") != _CLIENT._CMS.CHANGE_SCHEMA
|
|
174
|
+
):
|
|
175
|
+
raise CMSDispatchBlocked("dispatch.change_invalid")
|
|
176
|
+
try:
|
|
177
|
+
_CLIENT._CMS.WebsiteLocalizationCMSBridge._validated_event(None, copied)
|
|
178
|
+
except Exception:
|
|
179
|
+
raise CMSDispatchBlocked("dispatch.change_invalid") from None
|
|
180
|
+
event_id = _token(copied.get("event_id"), "dispatch.change_invalid")
|
|
181
|
+
return event_id, payload_json, _hash(payload_json)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _error(code: Any, detail_hash: Any = None) -> tuple[str, str | None]:
|
|
185
|
+
if not isinstance(code, str) or ERROR_CODE.fullmatch(code) is None:
|
|
186
|
+
raise CMSDispatchBlocked("dispatch.error_invalid")
|
|
187
|
+
if detail_hash is not None and (
|
|
188
|
+
not isinstance(detail_hash, str) or SHA256.fullmatch(detail_hash) is None
|
|
189
|
+
):
|
|
190
|
+
raise CMSDispatchBlocked("dispatch.error_invalid")
|
|
191
|
+
return code, detail_hash
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@contextmanager
|
|
195
|
+
def _transaction(connection: sqlite3.Connection) -> Iterator[None]:
|
|
196
|
+
if connection.in_transaction:
|
|
197
|
+
raise CMSDispatchBlocked("dispatch.transaction_nested")
|
|
198
|
+
try:
|
|
199
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
200
|
+
yield
|
|
201
|
+
connection.commit()
|
|
202
|
+
except Exception:
|
|
203
|
+
if connection.in_transaction:
|
|
204
|
+
connection.rollback()
|
|
205
|
+
raise
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class DurableCMSChangeDispatcher:
|
|
209
|
+
"""Persist, lease, and submit exact CMS change events at least once."""
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
connection: sqlite3.Connection,
|
|
214
|
+
*,
|
|
215
|
+
base_delay_seconds: float | int = 5,
|
|
216
|
+
max_delay_seconds: float | int = 300,
|
|
217
|
+
):
|
|
218
|
+
if not isinstance(connection, sqlite3.Connection):
|
|
219
|
+
raise TypeError("connection must be sqlite3.Connection")
|
|
220
|
+
self.connection = connection
|
|
221
|
+
self.connection.row_factory = sqlite3.Row
|
|
222
|
+
self.base_delay_seconds = _duration(
|
|
223
|
+
base_delay_seconds, "dispatch.delay_invalid",
|
|
224
|
+
maximum=MAX_DELAY_SECONDS,
|
|
225
|
+
)
|
|
226
|
+
self.max_delay_seconds = _duration(
|
|
227
|
+
max_delay_seconds, "dispatch.delay_invalid",
|
|
228
|
+
maximum=MAX_DELAY_SECONDS,
|
|
229
|
+
)
|
|
230
|
+
if self.base_delay_seconds > self.max_delay_seconds:
|
|
231
|
+
raise CMSDispatchBlocked("dispatch.delay_invalid")
|
|
232
|
+
self._initialize()
|
|
233
|
+
|
|
234
|
+
def _initialize(self) -> None:
|
|
235
|
+
with _transaction(self.connection):
|
|
236
|
+
self.connection.execute("""
|
|
237
|
+
CREATE TABLE IF NOT EXISTS cms_source_change_outbox_meta (
|
|
238
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
239
|
+
schema_version INTEGER NOT NULL
|
|
240
|
+
CHECK (schema_version = 1)
|
|
241
|
+
)
|
|
242
|
+
""")
|
|
243
|
+
self.connection.execute("""
|
|
244
|
+
INSERT OR IGNORE INTO cms_source_change_outbox_meta
|
|
245
|
+
VALUES (1, 1)
|
|
246
|
+
""")
|
|
247
|
+
self.connection.execute("""
|
|
248
|
+
CREATE TABLE IF NOT EXISTS cms_source_change_outbox (
|
|
249
|
+
event_id TEXT PRIMARY KEY,
|
|
250
|
+
payload_json TEXT NOT NULL,
|
|
251
|
+
payload_sha256 TEXT NOT NULL,
|
|
252
|
+
status TEXT NOT NULL CHECK (
|
|
253
|
+
status IN ('pending', 'leased', 'retry_wait',
|
|
254
|
+
'succeeded', 'failed')
|
|
255
|
+
),
|
|
256
|
+
attempts INTEGER NOT NULL CHECK (attempts >= 0),
|
|
257
|
+
max_attempts INTEGER NOT NULL CHECK (
|
|
258
|
+
max_attempts >= 1 AND max_attempts <= 20
|
|
259
|
+
),
|
|
260
|
+
next_attempt_at REAL NOT NULL,
|
|
261
|
+
lease_owner TEXT,
|
|
262
|
+
lease_token TEXT,
|
|
263
|
+
lease_expires_at REAL,
|
|
264
|
+
last_error_code TEXT,
|
|
265
|
+
last_error_detail_hash TEXT,
|
|
266
|
+
remote_plan_id TEXT,
|
|
267
|
+
remote_job_count INTEGER,
|
|
268
|
+
remote_status TEXT,
|
|
269
|
+
response_sha256 TEXT,
|
|
270
|
+
created_at REAL NOT NULL,
|
|
271
|
+
updated_at REAL NOT NULL,
|
|
272
|
+
CHECK (
|
|
273
|
+
(status = 'leased' AND lease_owner IS NOT NULL
|
|
274
|
+
AND lease_token IS NOT NULL
|
|
275
|
+
AND lease_expires_at IS NOT NULL)
|
|
276
|
+
OR
|
|
277
|
+
(status <> 'leased' AND lease_owner IS NULL
|
|
278
|
+
AND lease_token IS NULL
|
|
279
|
+
AND lease_expires_at IS NULL)
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
""")
|
|
283
|
+
self.connection.execute("""
|
|
284
|
+
CREATE INDEX IF NOT EXISTS cms_source_change_outbox_due
|
|
285
|
+
ON cms_source_change_outbox (
|
|
286
|
+
status, next_attempt_at, created_at, event_id
|
|
287
|
+
)
|
|
288
|
+
""")
|
|
289
|
+
self._validate_schema()
|
|
290
|
+
|
|
291
|
+
def _validate_schema(self) -> None:
|
|
292
|
+
meta_columns = tuple(
|
|
293
|
+
row["name"] for row in self.connection.execute(
|
|
294
|
+
"PRAGMA table_info(cms_source_change_outbox_meta)"
|
|
295
|
+
).fetchall()
|
|
296
|
+
)
|
|
297
|
+
meta = self.connection.execute(
|
|
298
|
+
"SELECT singleton, schema_version FROM cms_source_change_outbox_meta"
|
|
299
|
+
).fetchall()
|
|
300
|
+
columns = tuple(
|
|
301
|
+
row["name"] for row in self.connection.execute(
|
|
302
|
+
"PRAGMA table_info(cms_source_change_outbox)"
|
|
303
|
+
).fetchall()
|
|
304
|
+
)
|
|
305
|
+
if (
|
|
306
|
+
meta_columns != _META_COLUMNS
|
|
307
|
+
or len(meta) != 1
|
|
308
|
+
or tuple(meta[0]) != (1, SCHEMA_VERSION)
|
|
309
|
+
or columns != _COLUMNS
|
|
310
|
+
):
|
|
311
|
+
raise CMSDispatchBlocked("dispatch.schema_altered")
|
|
312
|
+
|
|
313
|
+
def enqueue(
|
|
314
|
+
self,
|
|
315
|
+
change: Mapping[str, Any],
|
|
316
|
+
*,
|
|
317
|
+
max_attempts: int = 5,
|
|
318
|
+
now: float | int,
|
|
319
|
+
) -> ChangeDispatchStatus:
|
|
320
|
+
self._validate_schema()
|
|
321
|
+
if (
|
|
322
|
+
isinstance(max_attempts, bool)
|
|
323
|
+
or not isinstance(max_attempts, int)
|
|
324
|
+
or not 1 <= max_attempts <= MAX_ATTEMPTS
|
|
325
|
+
):
|
|
326
|
+
raise CMSDispatchBlocked("dispatch.max_attempts_invalid")
|
|
327
|
+
now = _timestamp(now, "dispatch.time_invalid")
|
|
328
|
+
event_id, payload_json, payload_sha256 = _change(change)
|
|
329
|
+
with _transaction(self.connection):
|
|
330
|
+
row = self.connection.execute(
|
|
331
|
+
"SELECT payload_sha256, max_attempts FROM cms_source_change_outbox "
|
|
332
|
+
"WHERE event_id = ?",
|
|
333
|
+
(event_id,),
|
|
334
|
+
).fetchone()
|
|
335
|
+
if row is not None:
|
|
336
|
+
if (
|
|
337
|
+
row["payload_sha256"] != payload_sha256
|
|
338
|
+
or row["max_attempts"] != max_attempts
|
|
339
|
+
):
|
|
340
|
+
raise CMSDispatchBlocked("dispatch.idempotency_collision")
|
|
341
|
+
else:
|
|
342
|
+
self.connection.execute("""
|
|
343
|
+
INSERT INTO cms_source_change_outbox (
|
|
344
|
+
event_id, payload_json, payload_sha256, status,
|
|
345
|
+
attempts, max_attempts, next_attempt_at,
|
|
346
|
+
created_at, updated_at
|
|
347
|
+
) VALUES (?, ?, ?, 'pending', 0, ?, ?, ?, ?)
|
|
348
|
+
""", (
|
|
349
|
+
event_id, payload_json, payload_sha256, max_attempts,
|
|
350
|
+
now, now, now,
|
|
351
|
+
))
|
|
352
|
+
return self.status(event_id, now=now)
|
|
353
|
+
|
|
354
|
+
def claim(
|
|
355
|
+
self,
|
|
356
|
+
worker_id: str,
|
|
357
|
+
*,
|
|
358
|
+
now: float | int,
|
|
359
|
+
lease_seconds: float | int = 600,
|
|
360
|
+
) -> ChangeDispatchClaim | None:
|
|
361
|
+
self._validate_schema()
|
|
362
|
+
worker_id = _token(worker_id, "dispatch.worker_invalid", limit=128)
|
|
363
|
+
now = _timestamp(now, "dispatch.time_invalid")
|
|
364
|
+
lease_seconds = _duration(
|
|
365
|
+
lease_seconds, "dispatch.lease_invalid", maximum=MAX_LEASE_SECONDS,
|
|
366
|
+
)
|
|
367
|
+
claim = None
|
|
368
|
+
integrity_error = None
|
|
369
|
+
with _transaction(self.connection):
|
|
370
|
+
self.connection.execute("""
|
|
371
|
+
UPDATE cms_source_change_outbox
|
|
372
|
+
SET status = 'failed', lease_owner = NULL, lease_token = NULL,
|
|
373
|
+
lease_expires_at = NULL, last_error_code = 'lease_expired',
|
|
374
|
+
last_error_detail_hash = NULL, updated_at = ?
|
|
375
|
+
WHERE status = 'leased' AND lease_expires_at <= ?
|
|
376
|
+
AND attempts >= max_attempts
|
|
377
|
+
""", (now, now))
|
|
378
|
+
self.connection.execute("""
|
|
379
|
+
UPDATE cms_source_change_outbox
|
|
380
|
+
SET status = 'retry_wait', next_attempt_at = ?,
|
|
381
|
+
lease_owner = NULL, lease_token = NULL,
|
|
382
|
+
lease_expires_at = NULL,
|
|
383
|
+
last_error_code = 'lease_expired',
|
|
384
|
+
last_error_detail_hash = NULL, updated_at = ?
|
|
385
|
+
WHERE status = 'leased' AND lease_expires_at <= ?
|
|
386
|
+
AND attempts < max_attempts
|
|
387
|
+
""", (now, now, now))
|
|
388
|
+
row = self.connection.execute("""
|
|
389
|
+
SELECT * FROM cms_source_change_outbox
|
|
390
|
+
WHERE status IN ('pending', 'retry_wait')
|
|
391
|
+
AND next_attempt_at <= ? AND attempts < max_attempts
|
|
392
|
+
ORDER BY created_at, event_id LIMIT 1
|
|
393
|
+
""", (now,)).fetchone()
|
|
394
|
+
if row is not None:
|
|
395
|
+
try:
|
|
396
|
+
self._validated_row(row)
|
|
397
|
+
except CMSDispatchBlocked:
|
|
398
|
+
self.connection.execute("""
|
|
399
|
+
UPDATE cms_source_change_outbox
|
|
400
|
+
SET status = 'failed', last_error_code =
|
|
401
|
+
'dispatch.payload_integrity', updated_at = ?
|
|
402
|
+
WHERE event_id = ?
|
|
403
|
+
""", (now, row["event_id"]))
|
|
404
|
+
integrity_error = "dispatch.payload_integrity"
|
|
405
|
+
else:
|
|
406
|
+
lease_token = secrets.token_urlsafe(32)
|
|
407
|
+
lease_expires_at = now + lease_seconds
|
|
408
|
+
updated = self.connection.execute("""
|
|
409
|
+
UPDATE cms_source_change_outbox
|
|
410
|
+
SET status = 'leased', attempts = attempts + 1,
|
|
411
|
+
lease_owner = ?, lease_token = ?,
|
|
412
|
+
lease_expires_at = ?, last_error_code = NULL,
|
|
413
|
+
last_error_detail_hash = NULL, updated_at = ?
|
|
414
|
+
WHERE event_id = ?
|
|
415
|
+
AND status IN ('pending', 'retry_wait')
|
|
416
|
+
""", (
|
|
417
|
+
worker_id, lease_token, lease_expires_at, now,
|
|
418
|
+
row["event_id"],
|
|
419
|
+
))
|
|
420
|
+
if updated.rowcount != 1:
|
|
421
|
+
raise CMSDispatchBlocked("dispatch.claim_lost")
|
|
422
|
+
claim = ChangeDispatchClaim(
|
|
423
|
+
event_id=row["event_id"],
|
|
424
|
+
payload_sha256=row["payload_sha256"],
|
|
425
|
+
attempt=int(row["attempts"]) + 1,
|
|
426
|
+
max_attempts=int(row["max_attempts"]),
|
|
427
|
+
lease_owner=worker_id,
|
|
428
|
+
lease_token=lease_token,
|
|
429
|
+
lease_expires_at=lease_expires_at,
|
|
430
|
+
payload_json=row["payload_json"],
|
|
431
|
+
)
|
|
432
|
+
if integrity_error is not None:
|
|
433
|
+
raise CMSDispatchBlocked(integrity_error)
|
|
434
|
+
return claim
|
|
435
|
+
|
|
436
|
+
def run_once(
|
|
437
|
+
self,
|
|
438
|
+
client: Any,
|
|
439
|
+
worker_id: str,
|
|
440
|
+
*,
|
|
441
|
+
now: float | int,
|
|
442
|
+
lease_seconds: float | int = 600,
|
|
443
|
+
) -> ChangeDispatchOutcome | None:
|
|
444
|
+
submit = getattr(client, "submit_change", None)
|
|
445
|
+
if not callable(submit):
|
|
446
|
+
raise CMSDispatchBlocked("dispatch.client_invalid")
|
|
447
|
+
lease_seconds = _duration(
|
|
448
|
+
lease_seconds, "dispatch.lease_invalid", maximum=MAX_LEASE_SECONDS,
|
|
449
|
+
)
|
|
450
|
+
timeout = getattr(client, "timeout", None)
|
|
451
|
+
if (
|
|
452
|
+
timeout is not None
|
|
453
|
+
and (
|
|
454
|
+
isinstance(timeout, bool)
|
|
455
|
+
or not isinstance(timeout, (int, float))
|
|
456
|
+
or not math.isfinite(float(timeout))
|
|
457
|
+
or timeout <= 0
|
|
458
|
+
)
|
|
459
|
+
):
|
|
460
|
+
raise CMSDispatchBlocked("dispatch.client_invalid")
|
|
461
|
+
if timeout is not None and lease_seconds <= float(timeout):
|
|
462
|
+
raise CMSDispatchBlocked("dispatch.lease_too_short")
|
|
463
|
+
claim = self.claim(
|
|
464
|
+
worker_id, now=now, lease_seconds=lease_seconds,
|
|
465
|
+
)
|
|
466
|
+
if claim is None:
|
|
467
|
+
return None
|
|
468
|
+
try:
|
|
469
|
+
response = submit(json.loads(claim.payload_json))
|
|
470
|
+
status = self.complete(claim, response, now=now)
|
|
471
|
+
except CMSDispatchBlocked as error:
|
|
472
|
+
if error.code != "dispatch.response_invalid":
|
|
473
|
+
raise
|
|
474
|
+
status = self.fail(claim, error.code, now=now)
|
|
475
|
+
except Exception as error:
|
|
476
|
+
if getattr(error, "cms_client_failure", False) is True:
|
|
477
|
+
code = getattr(error, "code", None)
|
|
478
|
+
retryable = getattr(error, "retryable", None)
|
|
479
|
+
try:
|
|
480
|
+
code, _ = _error(code)
|
|
481
|
+
except CMSDispatchBlocked:
|
|
482
|
+
code, retryable = "dispatch.client_failure", False
|
|
483
|
+
if retryable is True:
|
|
484
|
+
status = self.retry(claim, code, now=now)
|
|
485
|
+
else:
|
|
486
|
+
status = self.fail(claim, code, now=now)
|
|
487
|
+
else:
|
|
488
|
+
status = self.fail(
|
|
489
|
+
claim, "dispatch.client_failure", now=now,
|
|
490
|
+
)
|
|
491
|
+
return ChangeDispatchOutcome(
|
|
492
|
+
event_id=status.event_id,
|
|
493
|
+
status=status.status,
|
|
494
|
+
attempt=status.attempts,
|
|
495
|
+
max_attempts=status.max_attempts,
|
|
496
|
+
next_attempt_at=status.next_attempt_at,
|
|
497
|
+
error_code=status.last_error_code,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def complete(
|
|
501
|
+
self,
|
|
502
|
+
claim: ChangeDispatchClaim,
|
|
503
|
+
response: Any,
|
|
504
|
+
*,
|
|
505
|
+
now: float | int,
|
|
506
|
+
) -> ChangeDispatchStatus:
|
|
507
|
+
self._validate_schema()
|
|
508
|
+
claim = self._claim(claim)
|
|
509
|
+
now = _timestamp(now, "dispatch.time_invalid")
|
|
510
|
+
if not isinstance(response, Mapping):
|
|
511
|
+
raise CMSDispatchBlocked("dispatch.response_invalid")
|
|
512
|
+
response_json = _canonical_json(
|
|
513
|
+
dict(response), "dispatch.response_invalid",
|
|
514
|
+
)
|
|
515
|
+
copied = json.loads(response_json)
|
|
516
|
+
if (
|
|
517
|
+
set(copied) != {
|
|
518
|
+
"schema", "event_id", "plan_id", "job_count",
|
|
519
|
+
"inserted_jobs", "status",
|
|
520
|
+
}
|
|
521
|
+
or copied.get("schema") != _CLIENT._API.API_SCHEMA
|
|
522
|
+
or copied.get("event_id") != claim.event_id
|
|
523
|
+
or not isinstance(copied.get("plan_id"), str)
|
|
524
|
+
or TOKEN.fullmatch(copied["plan_id"]) is None
|
|
525
|
+
or isinstance(copied.get("job_count"), bool)
|
|
526
|
+
or not isinstance(copied.get("job_count"), int)
|
|
527
|
+
or copied["job_count"] <= 0
|
|
528
|
+
or isinstance(copied.get("inserted_jobs"), bool)
|
|
529
|
+
or not isinstance(copied.get("inserted_jobs"), int)
|
|
530
|
+
or not 0 <= copied["inserted_jobs"] <= copied["job_count"]
|
|
531
|
+
or copied.get("status") not in {"enqueued", "superseded", "cancelled"}
|
|
532
|
+
):
|
|
533
|
+
raise CMSDispatchBlocked("dispatch.response_invalid")
|
|
534
|
+
with _transaction(self.connection):
|
|
535
|
+
self._require_live_claim(claim, now)
|
|
536
|
+
updated = self.connection.execute("""
|
|
537
|
+
UPDATE cms_source_change_outbox
|
|
538
|
+
SET status = 'succeeded', next_attempt_at = ?,
|
|
539
|
+
lease_owner = NULL, lease_token = NULL,
|
|
540
|
+
lease_expires_at = NULL, last_error_code = NULL,
|
|
541
|
+
last_error_detail_hash = NULL, remote_plan_id = ?,
|
|
542
|
+
remote_job_count = ?, remote_status = ?,
|
|
543
|
+
response_sha256 = ?, updated_at = ?
|
|
544
|
+
WHERE event_id = ? AND status = 'leased'
|
|
545
|
+
AND lease_owner = ? AND lease_token = ?
|
|
546
|
+
""", (
|
|
547
|
+
now, copied["plan_id"], copied["job_count"], copied["status"],
|
|
548
|
+
_hash(response_json), now, claim.event_id,
|
|
549
|
+
claim.lease_owner, claim.lease_token,
|
|
550
|
+
))
|
|
551
|
+
if updated.rowcount != 1:
|
|
552
|
+
raise CMSDispatchBlocked("dispatch.completion_lost")
|
|
553
|
+
return self.status(claim.event_id, now=now)
|
|
554
|
+
|
|
555
|
+
def retry(
|
|
556
|
+
self,
|
|
557
|
+
claim: ChangeDispatchClaim,
|
|
558
|
+
error_code: str,
|
|
559
|
+
*,
|
|
560
|
+
now: float | int,
|
|
561
|
+
) -> ChangeDispatchStatus:
|
|
562
|
+
self._validate_schema()
|
|
563
|
+
claim = self._claim(claim)
|
|
564
|
+
error_code, detail_hash = _error(error_code)
|
|
565
|
+
now = _timestamp(now, "dispatch.time_invalid")
|
|
566
|
+
with _transaction(self.connection):
|
|
567
|
+
row = self._require_live_claim(claim, now)
|
|
568
|
+
terminal = int(row["attempts"]) >= int(row["max_attempts"])
|
|
569
|
+
status = "failed" if terminal else "retry_wait"
|
|
570
|
+
delay = min(
|
|
571
|
+
self.max_delay_seconds,
|
|
572
|
+
self.base_delay_seconds * (2 ** max(0, claim.attempt - 1)),
|
|
573
|
+
)
|
|
574
|
+
next_attempt_at = now if terminal else now + delay
|
|
575
|
+
self._finish_failure(
|
|
576
|
+
claim, status, error_code, detail_hash, next_attempt_at, now,
|
|
577
|
+
)
|
|
578
|
+
return self.status(claim.event_id, now=now)
|
|
579
|
+
|
|
580
|
+
def fail(
|
|
581
|
+
self,
|
|
582
|
+
claim: ChangeDispatchClaim,
|
|
583
|
+
error_code: str,
|
|
584
|
+
*,
|
|
585
|
+
now: float | int,
|
|
586
|
+
) -> ChangeDispatchStatus:
|
|
587
|
+
self._validate_schema()
|
|
588
|
+
claim = self._claim(claim)
|
|
589
|
+
error_code, detail_hash = _error(error_code)
|
|
590
|
+
now = _timestamp(now, "dispatch.time_invalid")
|
|
591
|
+
with _transaction(self.connection):
|
|
592
|
+
self._require_live_claim(claim, now)
|
|
593
|
+
self._finish_failure(
|
|
594
|
+
claim, "failed", error_code, detail_hash, now, now,
|
|
595
|
+
)
|
|
596
|
+
return self.status(claim.event_id, now=now)
|
|
597
|
+
|
|
598
|
+
def _finish_failure(
|
|
599
|
+
self,
|
|
600
|
+
claim: ChangeDispatchClaim,
|
|
601
|
+
status: str,
|
|
602
|
+
error_code: str,
|
|
603
|
+
detail_hash: str | None,
|
|
604
|
+
next_attempt_at: float,
|
|
605
|
+
now: float,
|
|
606
|
+
) -> None:
|
|
607
|
+
updated = self.connection.execute("""
|
|
608
|
+
UPDATE cms_source_change_outbox
|
|
609
|
+
SET status = ?, next_attempt_at = ?, lease_owner = NULL,
|
|
610
|
+
lease_token = NULL, lease_expires_at = NULL,
|
|
611
|
+
last_error_code = ?, last_error_detail_hash = ?,
|
|
612
|
+
updated_at = ?
|
|
613
|
+
WHERE event_id = ? AND status = 'leased'
|
|
614
|
+
AND lease_owner = ? AND lease_token = ?
|
|
615
|
+
""", (
|
|
616
|
+
status, next_attempt_at, error_code, detail_hash, now,
|
|
617
|
+
claim.event_id, claim.lease_owner, claim.lease_token,
|
|
618
|
+
))
|
|
619
|
+
if updated.rowcount != 1:
|
|
620
|
+
raise CMSDispatchBlocked("dispatch.failure_lost")
|
|
621
|
+
|
|
622
|
+
def status(
|
|
623
|
+
self, event_id: str, *, now: float | int,
|
|
624
|
+
) -> ChangeDispatchStatus:
|
|
625
|
+
self._validate_schema()
|
|
626
|
+
event_id = _token(event_id, "dispatch.event_invalid")
|
|
627
|
+
now = _timestamp(now, "dispatch.time_invalid")
|
|
628
|
+
row = self.connection.execute(
|
|
629
|
+
"SELECT * FROM cms_source_change_outbox WHERE event_id = ?",
|
|
630
|
+
(event_id,),
|
|
631
|
+
).fetchone()
|
|
632
|
+
if row is None:
|
|
633
|
+
raise CMSDispatchBlocked("dispatch.event_missing")
|
|
634
|
+
self._validated_row(row)
|
|
635
|
+
lease_expires_at = row["lease_expires_at"]
|
|
636
|
+
return ChangeDispatchStatus(
|
|
637
|
+
event_id=row["event_id"],
|
|
638
|
+
payload_sha256=row["payload_sha256"],
|
|
639
|
+
status=row["status"],
|
|
640
|
+
attempts=int(row["attempts"]),
|
|
641
|
+
max_attempts=int(row["max_attempts"]),
|
|
642
|
+
next_attempt_at=float(row["next_attempt_at"]),
|
|
643
|
+
lease_expires_at=(
|
|
644
|
+
None if lease_expires_at is None else float(lease_expires_at)
|
|
645
|
+
),
|
|
646
|
+
lease_expired=(
|
|
647
|
+
row["status"] == "leased"
|
|
648
|
+
and float(lease_expires_at) <= now
|
|
649
|
+
),
|
|
650
|
+
last_error_code=row["last_error_code"],
|
|
651
|
+
last_error_detail_hash=row["last_error_detail_hash"],
|
|
652
|
+
remote_plan_id=row["remote_plan_id"],
|
|
653
|
+
remote_job_count=row["remote_job_count"],
|
|
654
|
+
remote_status=row["remote_status"],
|
|
655
|
+
response_sha256=row["response_sha256"],
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
def health(self, *, now: float | int) -> ChangeDispatchHealth:
|
|
659
|
+
now = _timestamp(now, "dispatch.time_invalid")
|
|
660
|
+
self._validate_schema()
|
|
661
|
+
quick = self.connection.execute("PRAGMA quick_check").fetchall()
|
|
662
|
+
if len(quick) != 1 or tuple(quick[0]) != ("ok",):
|
|
663
|
+
raise CMSDispatchBlocked("dispatch.database_integrity")
|
|
664
|
+
rows = self.connection.execute(
|
|
665
|
+
"SELECT * FROM cms_source_change_outbox ORDER BY event_id"
|
|
666
|
+
).fetchall()
|
|
667
|
+
for row in rows:
|
|
668
|
+
self._validated_row(row)
|
|
669
|
+
counts = {status: 0 for status in STATUSES}
|
|
670
|
+
for row in rows:
|
|
671
|
+
counts[row["status"]] += 1
|
|
672
|
+
due = sum(
|
|
673
|
+
1 for row in rows
|
|
674
|
+
if row["status"] in {"pending", "retry_wait"}
|
|
675
|
+
and row["next_attempt_at"] <= now
|
|
676
|
+
)
|
|
677
|
+
expired = sum(
|
|
678
|
+
1 for row in rows
|
|
679
|
+
if row["status"] == "leased" and row["lease_expires_at"] <= now
|
|
680
|
+
)
|
|
681
|
+
failed = counts["failed"]
|
|
682
|
+
return ChangeDispatchHealth(
|
|
683
|
+
status="blocked" if expired or failed else "ok",
|
|
684
|
+
counts=counts,
|
|
685
|
+
due=due,
|
|
686
|
+
expired_leases=expired,
|
|
687
|
+
failed=failed,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
def _validated_row(self, row: sqlite3.Row) -> None:
|
|
691
|
+
try:
|
|
692
|
+
event_id, payload_json, payload_sha256 = _change(
|
|
693
|
+
json.loads(row["payload_json"])
|
|
694
|
+
)
|
|
695
|
+
status = row["status"]
|
|
696
|
+
attempts = row["attempts"]
|
|
697
|
+
maximum = row["max_attempts"]
|
|
698
|
+
next_attempt_at = _timestamp(
|
|
699
|
+
row["next_attempt_at"], "dispatch.state_invalid",
|
|
700
|
+
)
|
|
701
|
+
created_at = _timestamp(
|
|
702
|
+
row["created_at"], "dispatch.state_invalid",
|
|
703
|
+
)
|
|
704
|
+
updated_at = _timestamp(
|
|
705
|
+
row["updated_at"], "dispatch.state_invalid",
|
|
706
|
+
)
|
|
707
|
+
except (json.JSONDecodeError, TypeError, CMSDispatchBlocked):
|
|
708
|
+
raise CMSDispatchBlocked("dispatch.state_invalid") from None
|
|
709
|
+
leases = (row["lease_owner"], row["lease_token"], row["lease_expires_at"])
|
|
710
|
+
leased = status == "leased"
|
|
711
|
+
remote = (
|
|
712
|
+
row["remote_plan_id"], row["remote_job_count"],
|
|
713
|
+
row["remote_status"], row["response_sha256"],
|
|
714
|
+
)
|
|
715
|
+
error = row["last_error_code"]
|
|
716
|
+
if (
|
|
717
|
+
event_id != row["event_id"]
|
|
718
|
+
or payload_json != row["payload_json"]
|
|
719
|
+
or payload_sha256 != row["payload_sha256"]
|
|
720
|
+
or status not in STATUSES
|
|
721
|
+
or isinstance(attempts, bool)
|
|
722
|
+
or not isinstance(attempts, int)
|
|
723
|
+
or isinstance(maximum, bool)
|
|
724
|
+
or not isinstance(maximum, int)
|
|
725
|
+
or not 0 <= attempts <= maximum <= MAX_ATTEMPTS
|
|
726
|
+
or maximum < 1
|
|
727
|
+
or next_attempt_at < created_at
|
|
728
|
+
or updated_at < created_at
|
|
729
|
+
or leased != all(item is not None for item in leases)
|
|
730
|
+
or (not leased and any(item is not None for item in leases))
|
|
731
|
+
or (leased and (
|
|
732
|
+
_token(leases[0], "dispatch.state_invalid", limit=128) != leases[0]
|
|
733
|
+
or _token(leases[1], "dispatch.state_invalid") != leases[1]
|
|
734
|
+
or _timestamp(leases[2], "dispatch.state_invalid") <= updated_at
|
|
735
|
+
))
|
|
736
|
+
or (error is not None and (
|
|
737
|
+
not isinstance(error, str) or ERROR_CODE.fullmatch(error) is None
|
|
738
|
+
))
|
|
739
|
+
or (status in {"retry_wait", "failed"}) != (error is not None)
|
|
740
|
+
or (
|
|
741
|
+
row["last_error_detail_hash"] is not None
|
|
742
|
+
and (
|
|
743
|
+
not isinstance(row["last_error_detail_hash"], str)
|
|
744
|
+
or SHA256.fullmatch(row["last_error_detail_hash"]) is None
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
or (status == "succeeded") != all(item is not None for item in remote)
|
|
748
|
+
or (status != "succeeded" and any(item is not None for item in remote))
|
|
749
|
+
or (status == "succeeded" and (
|
|
750
|
+
_token(remote[0], "dispatch.state_invalid") != remote[0]
|
|
751
|
+
or isinstance(remote[1], bool)
|
|
752
|
+
or not isinstance(remote[1], int)
|
|
753
|
+
or remote[1] <= 0
|
|
754
|
+
or remote[2] not in {"enqueued", "superseded", "cancelled"}
|
|
755
|
+
or not isinstance(remote[3], str)
|
|
756
|
+
or SHA256.fullmatch(remote[3]) is None
|
|
757
|
+
))
|
|
758
|
+
):
|
|
759
|
+
raise CMSDispatchBlocked("dispatch.state_invalid")
|
|
760
|
+
|
|
761
|
+
def _claim(self, claim: Any) -> ChangeDispatchClaim:
|
|
762
|
+
if not isinstance(claim, ChangeDispatchClaim):
|
|
763
|
+
raise CMSDispatchBlocked("dispatch.claim_invalid")
|
|
764
|
+
_token(claim.event_id, "dispatch.claim_invalid")
|
|
765
|
+
_token(claim.lease_owner, "dispatch.claim_invalid", limit=128)
|
|
766
|
+
_token(claim.lease_token, "dispatch.claim_invalid")
|
|
767
|
+
if (
|
|
768
|
+
not isinstance(claim.payload_sha256, str)
|
|
769
|
+
or SHA256.fullmatch(claim.payload_sha256) is None
|
|
770
|
+
or _hash(claim.payload_json) != claim.payload_sha256
|
|
771
|
+
or isinstance(claim.attempt, bool)
|
|
772
|
+
or not isinstance(claim.attempt, int)
|
|
773
|
+
or isinstance(claim.max_attempts, bool)
|
|
774
|
+
or not isinstance(claim.max_attempts, int)
|
|
775
|
+
or not 1 <= claim.attempt <= claim.max_attempts <= MAX_ATTEMPTS
|
|
776
|
+
):
|
|
777
|
+
raise CMSDispatchBlocked("dispatch.claim_invalid")
|
|
778
|
+
_timestamp(claim.lease_expires_at, "dispatch.claim_invalid")
|
|
779
|
+
return claim
|
|
780
|
+
|
|
781
|
+
def _require_live_claim(
|
|
782
|
+
self, claim: ChangeDispatchClaim, now: float,
|
|
783
|
+
) -> sqlite3.Row:
|
|
784
|
+
row = self.connection.execute(
|
|
785
|
+
"SELECT * FROM cms_source_change_outbox WHERE event_id = ?",
|
|
786
|
+
(claim.event_id,),
|
|
787
|
+
).fetchone()
|
|
788
|
+
if row is None:
|
|
789
|
+
raise CMSDispatchBlocked("dispatch.claim_missing")
|
|
790
|
+
self._validated_row(row)
|
|
791
|
+
if (
|
|
792
|
+
row["status"] != "leased"
|
|
793
|
+
or row["payload_sha256"] != claim.payload_sha256
|
|
794
|
+
or row["attempts"] != claim.attempt
|
|
795
|
+
or row["max_attempts"] != claim.max_attempts
|
|
796
|
+
or row["lease_owner"] != claim.lease_owner
|
|
797
|
+
or row["lease_token"] != claim.lease_token
|
|
798
|
+
or row["lease_expires_at"] != claim.lease_expires_at
|
|
799
|
+
):
|
|
800
|
+
raise CMSDispatchBlocked("dispatch.claim_lost")
|
|
801
|
+
if row["lease_expires_at"] <= now:
|
|
802
|
+
raise CMSDispatchBlocked("dispatch.lease_expired")
|
|
803
|
+
if now < row["updated_at"]:
|
|
804
|
+
raise CMSDispatchBlocked("dispatch.clock_regressed")
|
|
805
|
+
return row
|