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_lifecycle_monitor.py
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Durable source-side monitor for accepted CMS localization changes.
|
|
3
|
+
|
|
4
|
+
The monitor starts from the exact success record produced by the durable
|
|
5
|
+
change dispatcher. Each leased poll asks the CMS client for a freshly signed
|
|
6
|
+
lifecycle view; request freshness and response validation remain owned by that
|
|
7
|
+
client. Only content-free identities, counts, error codes, and hashes are
|
|
8
|
+
retained locally.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import secrets
|
|
17
|
+
import sqlite3
|
|
18
|
+
import sys
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Iterator, Mapping
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
SCHEMA_VERSION = 1
|
|
26
|
+
MAX_FAILURES = 20
|
|
27
|
+
MAX_LEASE_SECONDS = 86_400.0
|
|
28
|
+
MAX_DELAY_SECONDS = 86_400.0
|
|
29
|
+
STATES = ("pending", "leased", "watching", "retry_wait", "terminal", "failed")
|
|
30
|
+
TERMINAL_SUCCESS = {"cancelled", "deleted", "published", "superseded"}
|
|
31
|
+
TERMINAL_FAILURE = {
|
|
32
|
+
"deletion_failed", "localization_failed", "publication_blocked",
|
|
33
|
+
"publication_failed",
|
|
34
|
+
}
|
|
35
|
+
REMOTE_STATUSES = TERMINAL_SUCCESS | TERMINAL_FAILURE | {
|
|
36
|
+
"awaiting_approval", "deleting", "processing", "publishing",
|
|
37
|
+
"queue_recovery", "ready",
|
|
38
|
+
}
|
|
39
|
+
_COLUMNS = (
|
|
40
|
+
"event_id", "site_id", "plan_id", "website_version",
|
|
41
|
+
"source_sequence", "job_count", "change_sha256", "binding_sha256", "state",
|
|
42
|
+
"poll_attempts", "consecutive_failures", "max_consecutive_failures",
|
|
43
|
+
"next_poll_at", "lease_owner", "lease_token", "lease_expires_at",
|
|
44
|
+
"last_error_code", "remote_status", "lifecycle_sha256",
|
|
45
|
+
"lifecycle_json",
|
|
46
|
+
"required_locales_json", "approved_locales_json",
|
|
47
|
+
"blocked_locales_json", "queue_counts_json", "delivery_json",
|
|
48
|
+
"tombstone_json", "created_at", "updated_at",
|
|
49
|
+
)
|
|
50
|
+
_META_COLUMNS = ("singleton", "schema_version")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_module(name: str, path: Path):
|
|
54
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
55
|
+
if spec is None or spec.loader is None:
|
|
56
|
+
raise RuntimeError(f"cannot load lifecycle monitor dependency: {path.name}")
|
|
57
|
+
module = importlib.util.module_from_spec(spec)
|
|
58
|
+
sys.modules[spec.name] = module
|
|
59
|
+
spec.loader.exec_module(module)
|
|
60
|
+
return module
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
64
|
+
_DISPATCH = _load_module(
|
|
65
|
+
"blun_website_localization_cms_lifecycle_monitor_dispatch",
|
|
66
|
+
_ROOT / "integrations" / "website_localization_cms_dispatch.py",
|
|
67
|
+
)
|
|
68
|
+
LifecycleMonitorBlocked = _DISPATCH.CMSDispatchBlocked
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class LifecyclePollClaim:
|
|
73
|
+
event_id: str
|
|
74
|
+
site_id: str
|
|
75
|
+
plan_id: str
|
|
76
|
+
website_version: str
|
|
77
|
+
source_sequence: int
|
|
78
|
+
job_count: int
|
|
79
|
+
change_sha256: str
|
|
80
|
+
attempt: int
|
|
81
|
+
max_consecutive_failures: int
|
|
82
|
+
lease_owner: str
|
|
83
|
+
lease_token: str
|
|
84
|
+
lease_expires_at: float
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class LifecycleMonitorStatus:
|
|
89
|
+
event_id: str
|
|
90
|
+
site_id: str
|
|
91
|
+
plan_id: str
|
|
92
|
+
website_version: str
|
|
93
|
+
source_sequence: int
|
|
94
|
+
job_count: int
|
|
95
|
+
change_sha256: str
|
|
96
|
+
binding_sha256: str
|
|
97
|
+
state: str
|
|
98
|
+
poll_attempts: int
|
|
99
|
+
consecutive_failures: int
|
|
100
|
+
max_consecutive_failures: int
|
|
101
|
+
next_poll_at: float
|
|
102
|
+
lease_expires_at: float | None
|
|
103
|
+
lease_expired: bool
|
|
104
|
+
last_error_code: str | None
|
|
105
|
+
remote_status: str | None
|
|
106
|
+
lifecycle_sha256: str | None
|
|
107
|
+
required_locales: tuple[str, ...]
|
|
108
|
+
approved_locales: tuple[str, ...]
|
|
109
|
+
blocked_locales: tuple[tuple[str, str], ...]
|
|
110
|
+
queue_counts: dict[str, int]
|
|
111
|
+
delivery: Mapping[str, Any] | None
|
|
112
|
+
tombstone: Mapping[str, Any] | None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True)
|
|
116
|
+
class LifecyclePollOutcome:
|
|
117
|
+
event_id: str
|
|
118
|
+
state: str
|
|
119
|
+
remote_status: str | None
|
|
120
|
+
attempt: int
|
|
121
|
+
consecutive_failures: int
|
|
122
|
+
next_poll_at: float
|
|
123
|
+
error_code: str | None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class LifecycleMonitorHealth:
|
|
128
|
+
status: str
|
|
129
|
+
counts: dict[str, int]
|
|
130
|
+
due: int
|
|
131
|
+
expired_leases: int
|
|
132
|
+
failed: int
|
|
133
|
+
remote_failures: int
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _value(value: Any, name: str) -> Any:
|
|
137
|
+
if isinstance(value, Mapping):
|
|
138
|
+
return value.get(name)
|
|
139
|
+
return getattr(value, name, None)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _canonical(value: Any, code: str) -> str:
|
|
143
|
+
return _DISPATCH._canonical_json(value, code)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _binding(
|
|
147
|
+
event_id: str,
|
|
148
|
+
site_id: str,
|
|
149
|
+
plan_id: str,
|
|
150
|
+
website_version: str,
|
|
151
|
+
source_sequence: int,
|
|
152
|
+
job_count: int,
|
|
153
|
+
change_sha256: str,
|
|
154
|
+
max_consecutive_failures: int,
|
|
155
|
+
) -> str:
|
|
156
|
+
value = {
|
|
157
|
+
"schema": "blun.cms-source-lifecycle-monitor-binding.v1",
|
|
158
|
+
"event_id": event_id,
|
|
159
|
+
"site_id": site_id,
|
|
160
|
+
"plan_id": plan_id,
|
|
161
|
+
"website_version": website_version,
|
|
162
|
+
"source_sequence": source_sequence,
|
|
163
|
+
"job_count": job_count,
|
|
164
|
+
"change_sha256": change_sha256,
|
|
165
|
+
"max_consecutive_failures": max_consecutive_failures,
|
|
166
|
+
}
|
|
167
|
+
return _DISPATCH._hash(
|
|
168
|
+
_canonical(value, "lifecycle_monitor.binding_invalid")
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _json(value: Any, code: str) -> Any:
|
|
173
|
+
try:
|
|
174
|
+
return json.loads(value)
|
|
175
|
+
except (TypeError, json.JSONDecodeError, RecursionError):
|
|
176
|
+
raise LifecycleMonitorBlocked(code) from None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@contextmanager
|
|
180
|
+
def _transaction(connection: sqlite3.Connection) -> Iterator[None]:
|
|
181
|
+
if connection.in_transaction:
|
|
182
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.transaction_nested")
|
|
183
|
+
try:
|
|
184
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
185
|
+
yield
|
|
186
|
+
connection.commit()
|
|
187
|
+
except Exception:
|
|
188
|
+
if connection.in_transaction:
|
|
189
|
+
connection.rollback()
|
|
190
|
+
raise
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class DurableCMSLifecycleMonitor:
|
|
194
|
+
"""Poll accepted changes until a verified terminal CMS lifecycle state."""
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
connection: sqlite3.Connection,
|
|
199
|
+
*,
|
|
200
|
+
poll_interval_seconds: float | int = 30,
|
|
201
|
+
base_delay_seconds: float | int = 5,
|
|
202
|
+
max_delay_seconds: float | int = 300,
|
|
203
|
+
):
|
|
204
|
+
if not isinstance(connection, sqlite3.Connection):
|
|
205
|
+
raise TypeError("connection must be sqlite3.Connection")
|
|
206
|
+
self.connection = connection
|
|
207
|
+
self.connection.row_factory = sqlite3.Row
|
|
208
|
+
self.poll_interval_seconds = _DISPATCH._duration(
|
|
209
|
+
poll_interval_seconds, "lifecycle_monitor.delay_invalid",
|
|
210
|
+
maximum=MAX_DELAY_SECONDS,
|
|
211
|
+
)
|
|
212
|
+
self.base_delay_seconds = _DISPATCH._duration(
|
|
213
|
+
base_delay_seconds, "lifecycle_monitor.delay_invalid",
|
|
214
|
+
maximum=MAX_DELAY_SECONDS,
|
|
215
|
+
)
|
|
216
|
+
self.max_delay_seconds = _DISPATCH._duration(
|
|
217
|
+
max_delay_seconds, "lifecycle_monitor.delay_invalid",
|
|
218
|
+
maximum=MAX_DELAY_SECONDS,
|
|
219
|
+
)
|
|
220
|
+
if self.base_delay_seconds > self.max_delay_seconds:
|
|
221
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.delay_invalid")
|
|
222
|
+
self._initialize()
|
|
223
|
+
|
|
224
|
+
def _initialize(self) -> None:
|
|
225
|
+
with _transaction(self.connection):
|
|
226
|
+
self.connection.execute("""
|
|
227
|
+
CREATE TABLE IF NOT EXISTS cms_source_lifecycle_monitor_meta (
|
|
228
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
229
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 1)
|
|
230
|
+
)
|
|
231
|
+
""")
|
|
232
|
+
self.connection.execute("""
|
|
233
|
+
INSERT OR IGNORE INTO cms_source_lifecycle_monitor_meta
|
|
234
|
+
VALUES (1, 1)
|
|
235
|
+
""")
|
|
236
|
+
self.connection.execute("""
|
|
237
|
+
CREATE TABLE IF NOT EXISTS cms_source_lifecycle_monitor (
|
|
238
|
+
event_id TEXT PRIMARY KEY,
|
|
239
|
+
site_id TEXT NOT NULL,
|
|
240
|
+
plan_id TEXT NOT NULL,
|
|
241
|
+
website_version TEXT NOT NULL,
|
|
242
|
+
source_sequence INTEGER NOT NULL CHECK (source_sequence > 0),
|
|
243
|
+
job_count INTEGER NOT NULL CHECK (job_count > 0),
|
|
244
|
+
change_sha256 TEXT NOT NULL,
|
|
245
|
+
binding_sha256 TEXT NOT NULL,
|
|
246
|
+
state TEXT NOT NULL CHECK (
|
|
247
|
+
state IN ('pending', 'leased', 'watching',
|
|
248
|
+
'retry_wait', 'terminal', 'failed')
|
|
249
|
+
),
|
|
250
|
+
poll_attempts INTEGER NOT NULL CHECK (poll_attempts >= 0),
|
|
251
|
+
consecutive_failures INTEGER NOT NULL CHECK (
|
|
252
|
+
consecutive_failures >= 0
|
|
253
|
+
),
|
|
254
|
+
max_consecutive_failures INTEGER NOT NULL CHECK (
|
|
255
|
+
max_consecutive_failures >= 1
|
|
256
|
+
AND max_consecutive_failures <= 20
|
|
257
|
+
),
|
|
258
|
+
next_poll_at REAL NOT NULL,
|
|
259
|
+
lease_owner TEXT,
|
|
260
|
+
lease_token TEXT,
|
|
261
|
+
lease_expires_at REAL,
|
|
262
|
+
last_error_code TEXT,
|
|
263
|
+
remote_status TEXT,
|
|
264
|
+
lifecycle_sha256 TEXT,
|
|
265
|
+
lifecycle_json TEXT,
|
|
266
|
+
required_locales_json TEXT,
|
|
267
|
+
approved_locales_json TEXT,
|
|
268
|
+
blocked_locales_json TEXT,
|
|
269
|
+
queue_counts_json TEXT,
|
|
270
|
+
delivery_json TEXT,
|
|
271
|
+
tombstone_json TEXT,
|
|
272
|
+
created_at REAL NOT NULL,
|
|
273
|
+
updated_at REAL NOT NULL,
|
|
274
|
+
CHECK (
|
|
275
|
+
(state = 'leased' AND lease_owner IS NOT NULL
|
|
276
|
+
AND lease_token IS NOT NULL
|
|
277
|
+
AND lease_expires_at IS NOT NULL)
|
|
278
|
+
OR
|
|
279
|
+
(state <> 'leased' AND lease_owner IS NULL
|
|
280
|
+
AND lease_token IS NULL
|
|
281
|
+
AND lease_expires_at IS NULL)
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
""")
|
|
285
|
+
self.connection.execute("""
|
|
286
|
+
CREATE INDEX IF NOT EXISTS cms_source_lifecycle_monitor_due
|
|
287
|
+
ON cms_source_lifecycle_monitor (
|
|
288
|
+
state, next_poll_at, created_at, event_id
|
|
289
|
+
)
|
|
290
|
+
""")
|
|
291
|
+
self._validate_schema()
|
|
292
|
+
|
|
293
|
+
def _validate_schema(self) -> None:
|
|
294
|
+
meta_columns = tuple(
|
|
295
|
+
row["name"] for row in self.connection.execute(
|
|
296
|
+
"PRAGMA table_info(cms_source_lifecycle_monitor_meta)"
|
|
297
|
+
).fetchall()
|
|
298
|
+
)
|
|
299
|
+
meta = self.connection.execute(
|
|
300
|
+
"SELECT singleton, schema_version "
|
|
301
|
+
"FROM cms_source_lifecycle_monitor_meta"
|
|
302
|
+
).fetchall()
|
|
303
|
+
columns = tuple(
|
|
304
|
+
row["name"] for row in self.connection.execute(
|
|
305
|
+
"PRAGMA table_info(cms_source_lifecycle_monitor)"
|
|
306
|
+
).fetchall()
|
|
307
|
+
)
|
|
308
|
+
if (
|
|
309
|
+
meta_columns != _META_COLUMNS
|
|
310
|
+
or len(meta) != 1
|
|
311
|
+
or tuple(meta[0]) != (1, SCHEMA_VERSION)
|
|
312
|
+
or columns != _COLUMNS
|
|
313
|
+
):
|
|
314
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.schema_altered")
|
|
315
|
+
|
|
316
|
+
def register(
|
|
317
|
+
self,
|
|
318
|
+
change: Mapping[str, Any],
|
|
319
|
+
dispatch_status: Any,
|
|
320
|
+
*,
|
|
321
|
+
max_consecutive_failures: int = 5,
|
|
322
|
+
now: float | int,
|
|
323
|
+
) -> LifecycleMonitorStatus:
|
|
324
|
+
"""Bind monitoring to one validated successful dispatch result."""
|
|
325
|
+
self._validate_schema()
|
|
326
|
+
if (
|
|
327
|
+
isinstance(max_consecutive_failures, bool)
|
|
328
|
+
or not isinstance(max_consecutive_failures, int)
|
|
329
|
+
or not 1 <= max_consecutive_failures <= MAX_FAILURES
|
|
330
|
+
):
|
|
331
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.max_failures_invalid")
|
|
332
|
+
now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
|
|
333
|
+
event_id, payload_json, change_sha256 = _DISPATCH._change(change)
|
|
334
|
+
copied = _json(payload_json, "lifecycle_monitor.change_invalid")
|
|
335
|
+
site_id = _DISPATCH._token(
|
|
336
|
+
copied.get("site_id"), "lifecycle_monitor.change_invalid",
|
|
337
|
+
)
|
|
338
|
+
website_version = _DISPATCH._token(
|
|
339
|
+
copied.get("website_version"), "lifecycle_monitor.change_invalid",
|
|
340
|
+
)
|
|
341
|
+
source_sequence = copied.get("source_sequence")
|
|
342
|
+
plan_id = _value(dispatch_status, "remote_plan_id")
|
|
343
|
+
job_count = _value(dispatch_status, "remote_job_count")
|
|
344
|
+
remote_status = _value(dispatch_status, "remote_status")
|
|
345
|
+
dispatch_response_sha256 = _value(dispatch_status, "response_sha256")
|
|
346
|
+
if (
|
|
347
|
+
_value(dispatch_status, "event_id") != event_id
|
|
348
|
+
or _value(dispatch_status, "payload_sha256") != change_sha256
|
|
349
|
+
or _value(dispatch_status, "status") != "succeeded"
|
|
350
|
+
or not isinstance(plan_id, str)
|
|
351
|
+
or _DISPATCH.TOKEN.fullmatch(plan_id) is None
|
|
352
|
+
or isinstance(job_count, bool)
|
|
353
|
+
or not isinstance(job_count, int)
|
|
354
|
+
or job_count <= 0
|
|
355
|
+
or remote_status not in {"enqueued", "superseded", "cancelled"}
|
|
356
|
+
or not isinstance(dispatch_response_sha256, str)
|
|
357
|
+
or _DISPATCH.SHA256.fullmatch(dispatch_response_sha256) is None
|
|
358
|
+
or not isinstance(source_sequence, int)
|
|
359
|
+
or isinstance(source_sequence, bool)
|
|
360
|
+
or source_sequence <= 0
|
|
361
|
+
):
|
|
362
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.dispatch_invalid")
|
|
363
|
+
initial_state = "terminal" if remote_status in {
|
|
364
|
+
"superseded", "cancelled",
|
|
365
|
+
} else "pending"
|
|
366
|
+
initial_remote = remote_status if initial_state == "terminal" else None
|
|
367
|
+
binding_sha256 = _binding(
|
|
368
|
+
event_id, site_id, plan_id, website_version, source_sequence,
|
|
369
|
+
job_count, change_sha256, max_consecutive_failures,
|
|
370
|
+
)
|
|
371
|
+
identity = (
|
|
372
|
+
site_id, plan_id, website_version, source_sequence, job_count,
|
|
373
|
+
change_sha256, binding_sha256, max_consecutive_failures,
|
|
374
|
+
)
|
|
375
|
+
with _transaction(self.connection):
|
|
376
|
+
row = self.connection.execute(
|
|
377
|
+
"SELECT * FROM cms_source_lifecycle_monitor WHERE event_id = ?",
|
|
378
|
+
(event_id,),
|
|
379
|
+
).fetchone()
|
|
380
|
+
if row is not None:
|
|
381
|
+
current = (
|
|
382
|
+
row["site_id"], row["plan_id"], row["website_version"],
|
|
383
|
+
row["source_sequence"], row["job_count"],
|
|
384
|
+
row["change_sha256"], row["binding_sha256"],
|
|
385
|
+
row["max_consecutive_failures"],
|
|
386
|
+
)
|
|
387
|
+
if current != identity:
|
|
388
|
+
raise LifecycleMonitorBlocked(
|
|
389
|
+
"lifecycle_monitor.idempotency_collision"
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
self.connection.execute("""
|
|
393
|
+
INSERT INTO cms_source_lifecycle_monitor (
|
|
394
|
+
event_id, site_id, plan_id, website_version,
|
|
395
|
+
source_sequence, job_count, change_sha256,
|
|
396
|
+
binding_sha256, state,
|
|
397
|
+
poll_attempts, consecutive_failures,
|
|
398
|
+
max_consecutive_failures, next_poll_at,
|
|
399
|
+
remote_status, created_at, updated_at
|
|
400
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?)
|
|
401
|
+
""", (
|
|
402
|
+
event_id, site_id, plan_id, website_version,
|
|
403
|
+
source_sequence, job_count, change_sha256, binding_sha256,
|
|
404
|
+
initial_state,
|
|
405
|
+
max_consecutive_failures, now, initial_remote, now, now,
|
|
406
|
+
))
|
|
407
|
+
return self.status(event_id, now=now)
|
|
408
|
+
|
|
409
|
+
def claim(
|
|
410
|
+
self,
|
|
411
|
+
worker_id: str,
|
|
412
|
+
*,
|
|
413
|
+
now: float | int,
|
|
414
|
+
lease_seconds: float | int = 600,
|
|
415
|
+
) -> LifecyclePollClaim | None:
|
|
416
|
+
self._validate_schema()
|
|
417
|
+
worker_id = _DISPATCH._token(
|
|
418
|
+
worker_id, "lifecycle_monitor.worker_invalid", limit=128,
|
|
419
|
+
)
|
|
420
|
+
now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
|
|
421
|
+
lease_seconds = _DISPATCH._duration(
|
|
422
|
+
lease_seconds, "lifecycle_monitor.lease_invalid",
|
|
423
|
+
maximum=MAX_LEASE_SECONDS,
|
|
424
|
+
)
|
|
425
|
+
integrity_error = None
|
|
426
|
+
claim = None
|
|
427
|
+
with _transaction(self.connection):
|
|
428
|
+
expired = self.connection.execute("""
|
|
429
|
+
SELECT * FROM cms_source_lifecycle_monitor
|
|
430
|
+
WHERE state = 'leased' AND lease_expires_at <= ?
|
|
431
|
+
ORDER BY event_id
|
|
432
|
+
""", (now,)).fetchall()
|
|
433
|
+
for row in expired:
|
|
434
|
+
self._validated_row(row)
|
|
435
|
+
failures = int(row["consecutive_failures"]) + 1
|
|
436
|
+
terminal = failures >= int(row["max_consecutive_failures"])
|
|
437
|
+
delay = self._retry_delay(failures)
|
|
438
|
+
self.connection.execute("""
|
|
439
|
+
UPDATE cms_source_lifecycle_monitor
|
|
440
|
+
SET state = ?, consecutive_failures = ?,
|
|
441
|
+
next_poll_at = ?, lease_owner = NULL,
|
|
442
|
+
lease_token = NULL, lease_expires_at = NULL,
|
|
443
|
+
last_error_code = 'lease_expired', updated_at = ?
|
|
444
|
+
WHERE event_id = ? AND state = 'leased'
|
|
445
|
+
""", (
|
|
446
|
+
"failed" if terminal else "retry_wait", failures,
|
|
447
|
+
now if terminal else now + delay, now, row["event_id"],
|
|
448
|
+
))
|
|
449
|
+
row = self.connection.execute("""
|
|
450
|
+
SELECT * FROM cms_source_lifecycle_monitor
|
|
451
|
+
WHERE state IN ('pending', 'watching', 'retry_wait')
|
|
452
|
+
AND next_poll_at <= ?
|
|
453
|
+
ORDER BY created_at, event_id LIMIT 1
|
|
454
|
+
""", (now,)).fetchone()
|
|
455
|
+
if row is not None:
|
|
456
|
+
try:
|
|
457
|
+
self._validated_row(row)
|
|
458
|
+
except LifecycleMonitorBlocked:
|
|
459
|
+
self.connection.execute("""
|
|
460
|
+
UPDATE cms_source_lifecycle_monitor
|
|
461
|
+
SET state = 'failed', last_error_code =
|
|
462
|
+
'lifecycle_monitor.state_invalid', updated_at = ?
|
|
463
|
+
WHERE event_id = ?
|
|
464
|
+
""", (now, row["event_id"]))
|
|
465
|
+
integrity_error = "lifecycle_monitor.state_invalid"
|
|
466
|
+
else:
|
|
467
|
+
token = secrets.token_urlsafe(32)
|
|
468
|
+
expires = now + lease_seconds
|
|
469
|
+
updated = self.connection.execute("""
|
|
470
|
+
UPDATE cms_source_lifecycle_monitor
|
|
471
|
+
SET state = 'leased', poll_attempts = poll_attempts + 1,
|
|
472
|
+
lease_owner = ?, lease_token = ?,
|
|
473
|
+
lease_expires_at = ?, last_error_code = NULL,
|
|
474
|
+
updated_at = ?
|
|
475
|
+
WHERE event_id = ?
|
|
476
|
+
AND state IN ('pending', 'watching', 'retry_wait')
|
|
477
|
+
""", (worker_id, token, expires, now, row["event_id"]))
|
|
478
|
+
if updated.rowcount != 1:
|
|
479
|
+
raise LifecycleMonitorBlocked(
|
|
480
|
+
"lifecycle_monitor.claim_lost"
|
|
481
|
+
)
|
|
482
|
+
claim = LifecyclePollClaim(
|
|
483
|
+
event_id=row["event_id"], site_id=row["site_id"],
|
|
484
|
+
plan_id=row["plan_id"],
|
|
485
|
+
website_version=row["website_version"],
|
|
486
|
+
source_sequence=int(row["source_sequence"]),
|
|
487
|
+
job_count=int(row["job_count"]),
|
|
488
|
+
change_sha256=row["change_sha256"],
|
|
489
|
+
attempt=int(row["poll_attempts"]) + 1,
|
|
490
|
+
max_consecutive_failures=int(
|
|
491
|
+
row["max_consecutive_failures"]
|
|
492
|
+
),
|
|
493
|
+
lease_owner=worker_id, lease_token=token,
|
|
494
|
+
lease_expires_at=expires,
|
|
495
|
+
)
|
|
496
|
+
if integrity_error is not None:
|
|
497
|
+
raise LifecycleMonitorBlocked(integrity_error)
|
|
498
|
+
return claim
|
|
499
|
+
|
|
500
|
+
def run_once(
|
|
501
|
+
self,
|
|
502
|
+
client: Any,
|
|
503
|
+
worker_id: str,
|
|
504
|
+
*,
|
|
505
|
+
now: float | int,
|
|
506
|
+
lease_seconds: float | int = 600,
|
|
507
|
+
) -> LifecyclePollOutcome | None:
|
|
508
|
+
read = getattr(client, "lifecycle", None)
|
|
509
|
+
if not callable(read):
|
|
510
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.client_invalid")
|
|
511
|
+
lease_seconds = _DISPATCH._duration(
|
|
512
|
+
lease_seconds, "lifecycle_monitor.lease_invalid",
|
|
513
|
+
maximum=MAX_LEASE_SECONDS,
|
|
514
|
+
)
|
|
515
|
+
timeout = getattr(client, "timeout", None)
|
|
516
|
+
if timeout is not None and (
|
|
517
|
+
isinstance(timeout, bool)
|
|
518
|
+
or not isinstance(timeout, (int, float))
|
|
519
|
+
or not math.isfinite(float(timeout))
|
|
520
|
+
or timeout <= 0
|
|
521
|
+
):
|
|
522
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.client_invalid")
|
|
523
|
+
if timeout is not None and lease_seconds <= float(timeout):
|
|
524
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.lease_too_short")
|
|
525
|
+
claim = self.claim(worker_id, now=now, lease_seconds=lease_seconds)
|
|
526
|
+
if claim is None:
|
|
527
|
+
return None
|
|
528
|
+
try:
|
|
529
|
+
# No request_id is supplied: the secure client creates and signs a
|
|
530
|
+
# fresh purpose-bound request for every durable polling attempt.
|
|
531
|
+
response = read(claim.event_id, claim.site_id)
|
|
532
|
+
status = self.complete(claim, response, now=now)
|
|
533
|
+
except LifecycleMonitorBlocked as error:
|
|
534
|
+
if error.code != "lifecycle_monitor.response_invalid":
|
|
535
|
+
raise
|
|
536
|
+
status = self.fail(claim, error.code, now=now)
|
|
537
|
+
except Exception as error:
|
|
538
|
+
if getattr(error, "cms_client_failure", False) is True:
|
|
539
|
+
code = getattr(error, "code", None)
|
|
540
|
+
retryable = getattr(error, "retryable", None)
|
|
541
|
+
try:
|
|
542
|
+
code, _ = _DISPATCH._error(code)
|
|
543
|
+
except LifecycleMonitorBlocked:
|
|
544
|
+
code, retryable = "lifecycle_monitor.client_failure", False
|
|
545
|
+
status = (
|
|
546
|
+
self.retry(claim, code, now=now)
|
|
547
|
+
if retryable is True
|
|
548
|
+
else self.fail(claim, code, now=now)
|
|
549
|
+
)
|
|
550
|
+
else:
|
|
551
|
+
status = self.fail(
|
|
552
|
+
claim, "lifecycle_monitor.client_failure", now=now,
|
|
553
|
+
)
|
|
554
|
+
return LifecyclePollOutcome(
|
|
555
|
+
event_id=status.event_id, state=status.state,
|
|
556
|
+
remote_status=status.remote_status, attempt=status.poll_attempts,
|
|
557
|
+
consecutive_failures=status.consecutive_failures,
|
|
558
|
+
next_poll_at=status.next_poll_at,
|
|
559
|
+
error_code=status.last_error_code,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
def complete(
|
|
563
|
+
self,
|
|
564
|
+
claim: LifecyclePollClaim,
|
|
565
|
+
response: Any,
|
|
566
|
+
*,
|
|
567
|
+
now: float | int,
|
|
568
|
+
) -> LifecycleMonitorStatus:
|
|
569
|
+
self._validate_schema()
|
|
570
|
+
claim = self._claim(claim)
|
|
571
|
+
now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
|
|
572
|
+
copied, response_json = self._validated_response(claim, response)
|
|
573
|
+
remote_status = copied["status"]
|
|
574
|
+
terminal = remote_status in TERMINAL_SUCCESS | TERMINAL_FAILURE
|
|
575
|
+
snapshot = (
|
|
576
|
+
_canonical(copied["required_locales"], "lifecycle_monitor.response_invalid"),
|
|
577
|
+
_canonical(copied["approved_locales"], "lifecycle_monitor.response_invalid"),
|
|
578
|
+
_canonical(copied["blocked_locales"], "lifecycle_monitor.response_invalid"),
|
|
579
|
+
_canonical(copied["queue_counts"], "lifecycle_monitor.response_invalid"),
|
|
580
|
+
_canonical(copied["delivery"], "lifecycle_monitor.response_invalid"),
|
|
581
|
+
_canonical(copied["tombstone"], "lifecycle_monitor.response_invalid"),
|
|
582
|
+
)
|
|
583
|
+
with _transaction(self.connection):
|
|
584
|
+
self._require_live_claim(claim, now)
|
|
585
|
+
updated = self.connection.execute("""
|
|
586
|
+
UPDATE cms_source_lifecycle_monitor
|
|
587
|
+
SET state = ?, consecutive_failures = 0,
|
|
588
|
+
next_poll_at = ?, lease_owner = NULL, lease_token = NULL,
|
|
589
|
+
lease_expires_at = NULL, last_error_code = NULL,
|
|
590
|
+
remote_status = ?, lifecycle_sha256 = ?, lifecycle_json = ?,
|
|
591
|
+
required_locales_json = ?, approved_locales_json = ?,
|
|
592
|
+
blocked_locales_json = ?, queue_counts_json = ?,
|
|
593
|
+
delivery_json = ?, tombstone_json = ?, updated_at = ?
|
|
594
|
+
WHERE event_id = ? AND state = 'leased'
|
|
595
|
+
AND lease_owner = ? AND lease_token = ?
|
|
596
|
+
""", (
|
|
597
|
+
"terminal" if terminal else "watching",
|
|
598
|
+
now if terminal else now + self.poll_interval_seconds,
|
|
599
|
+
remote_status, _DISPATCH._hash(response_json), response_json,
|
|
600
|
+
*snapshot, now,
|
|
601
|
+
claim.event_id, claim.lease_owner, claim.lease_token,
|
|
602
|
+
))
|
|
603
|
+
if updated.rowcount != 1:
|
|
604
|
+
raise LifecycleMonitorBlocked(
|
|
605
|
+
"lifecycle_monitor.completion_lost"
|
|
606
|
+
)
|
|
607
|
+
return self.status(claim.event_id, now=now)
|
|
608
|
+
|
|
609
|
+
def retry(
|
|
610
|
+
self, claim: LifecyclePollClaim, error_code: str, *, now: float | int,
|
|
611
|
+
) -> LifecycleMonitorStatus:
|
|
612
|
+
return self._finish_failure(claim, error_code, retryable=True, now=now)
|
|
613
|
+
|
|
614
|
+
def fail(
|
|
615
|
+
self, claim: LifecyclePollClaim, error_code: str, *, now: float | int,
|
|
616
|
+
) -> LifecycleMonitorStatus:
|
|
617
|
+
return self._finish_failure(claim, error_code, retryable=False, now=now)
|
|
618
|
+
|
|
619
|
+
def _finish_failure(
|
|
620
|
+
self,
|
|
621
|
+
claim: LifecyclePollClaim,
|
|
622
|
+
error_code: str,
|
|
623
|
+
*,
|
|
624
|
+
retryable: bool,
|
|
625
|
+
now: float | int,
|
|
626
|
+
) -> LifecycleMonitorStatus:
|
|
627
|
+
self._validate_schema()
|
|
628
|
+
claim = self._claim(claim)
|
|
629
|
+
error_code, _ = _DISPATCH._error(error_code)
|
|
630
|
+
now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
|
|
631
|
+
with _transaction(self.connection):
|
|
632
|
+
row = self._require_live_claim(claim, now)
|
|
633
|
+
failures = int(row["consecutive_failures"]) + 1
|
|
634
|
+
terminal = not retryable or failures >= int(
|
|
635
|
+
row["max_consecutive_failures"]
|
|
636
|
+
)
|
|
637
|
+
updated = self.connection.execute("""
|
|
638
|
+
UPDATE cms_source_lifecycle_monitor
|
|
639
|
+
SET state = ?, consecutive_failures = ?, next_poll_at = ?,
|
|
640
|
+
lease_owner = NULL, lease_token = NULL,
|
|
641
|
+
lease_expires_at = NULL, last_error_code = ?, updated_at = ?
|
|
642
|
+
WHERE event_id = ? AND state = 'leased'
|
|
643
|
+
AND lease_owner = ? AND lease_token = ?
|
|
644
|
+
""", (
|
|
645
|
+
"failed" if terminal else "retry_wait", failures,
|
|
646
|
+
now if terminal else now + self._retry_delay(failures),
|
|
647
|
+
error_code, now, claim.event_id, claim.lease_owner,
|
|
648
|
+
claim.lease_token,
|
|
649
|
+
))
|
|
650
|
+
if updated.rowcount != 1:
|
|
651
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.failure_lost")
|
|
652
|
+
return self.status(claim.event_id, now=now)
|
|
653
|
+
|
|
654
|
+
def status(
|
|
655
|
+
self, event_id: str, *, now: float | int,
|
|
656
|
+
) -> LifecycleMonitorStatus:
|
|
657
|
+
self._validate_schema()
|
|
658
|
+
event_id = _DISPATCH._token(
|
|
659
|
+
event_id, "lifecycle_monitor.event_invalid",
|
|
660
|
+
)
|
|
661
|
+
now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
|
|
662
|
+
row = self.connection.execute(
|
|
663
|
+
"SELECT * FROM cms_source_lifecycle_monitor WHERE event_id = ?",
|
|
664
|
+
(event_id,),
|
|
665
|
+
).fetchone()
|
|
666
|
+
if row is None:
|
|
667
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.event_missing")
|
|
668
|
+
self._validated_row(row)
|
|
669
|
+
lease = row["lease_expires_at"]
|
|
670
|
+
return LifecycleMonitorStatus(
|
|
671
|
+
event_id=row["event_id"], site_id=row["site_id"],
|
|
672
|
+
plan_id=row["plan_id"], website_version=row["website_version"],
|
|
673
|
+
source_sequence=int(row["source_sequence"]),
|
|
674
|
+
job_count=int(row["job_count"]),
|
|
675
|
+
change_sha256=row["change_sha256"],
|
|
676
|
+
binding_sha256=row["binding_sha256"], state=row["state"],
|
|
677
|
+
poll_attempts=int(row["poll_attempts"]),
|
|
678
|
+
consecutive_failures=int(row["consecutive_failures"]),
|
|
679
|
+
max_consecutive_failures=int(row["max_consecutive_failures"]),
|
|
680
|
+
next_poll_at=float(row["next_poll_at"]),
|
|
681
|
+
lease_expires_at=None if lease is None else float(lease),
|
|
682
|
+
lease_expired=(
|
|
683
|
+
row["state"] == "leased" and float(lease) <= now
|
|
684
|
+
),
|
|
685
|
+
last_error_code=row["last_error_code"],
|
|
686
|
+
remote_status=row["remote_status"],
|
|
687
|
+
lifecycle_sha256=row["lifecycle_sha256"],
|
|
688
|
+
required_locales=tuple(self._stored(row, "required_locales_json", [])),
|
|
689
|
+
approved_locales=tuple(self._stored(row, "approved_locales_json", [])),
|
|
690
|
+
blocked_locales=tuple(
|
|
691
|
+
tuple(item) for item in self._stored(
|
|
692
|
+
row, "blocked_locales_json", [],
|
|
693
|
+
)
|
|
694
|
+
),
|
|
695
|
+
queue_counts=dict(self._stored(row, "queue_counts_json", {})),
|
|
696
|
+
delivery=self._stored(row, "delivery_json", None),
|
|
697
|
+
tombstone=self._stored(row, "tombstone_json", None),
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
def health(self, *, now: float | int) -> LifecycleMonitorHealth:
|
|
701
|
+
now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
|
|
702
|
+
self._validate_schema()
|
|
703
|
+
quick = self.connection.execute("PRAGMA quick_check").fetchall()
|
|
704
|
+
if len(quick) != 1 or tuple(quick[0]) != ("ok",):
|
|
705
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.database_integrity")
|
|
706
|
+
rows = self.connection.execute(
|
|
707
|
+
"SELECT * FROM cms_source_lifecycle_monitor ORDER BY event_id"
|
|
708
|
+
).fetchall()
|
|
709
|
+
for row in rows:
|
|
710
|
+
self._validated_row(row)
|
|
711
|
+
counts = {state: 0 for state in STATES}
|
|
712
|
+
for row in rows:
|
|
713
|
+
counts[row["state"]] += 1
|
|
714
|
+
due = sum(
|
|
715
|
+
1 for row in rows
|
|
716
|
+
if row["state"] in {"pending", "watching", "retry_wait"}
|
|
717
|
+
and row["next_poll_at"] <= now
|
|
718
|
+
)
|
|
719
|
+
expired = sum(
|
|
720
|
+
1 for row in rows
|
|
721
|
+
if row["state"] == "leased" and row["lease_expires_at"] <= now
|
|
722
|
+
)
|
|
723
|
+
remote_failures = sum(
|
|
724
|
+
1 for row in rows if row["remote_status"] in TERMINAL_FAILURE
|
|
725
|
+
)
|
|
726
|
+
failed = counts["failed"]
|
|
727
|
+
return LifecycleMonitorHealth(
|
|
728
|
+
status="blocked" if expired or failed or remote_failures else "ok",
|
|
729
|
+
counts=counts, due=due, expired_leases=expired, failed=failed,
|
|
730
|
+
remote_failures=remote_failures,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
def _validated_response(
|
|
734
|
+
self, claim: LifecyclePollClaim, response: Any,
|
|
735
|
+
) -> tuple[dict[str, Any], str]:
|
|
736
|
+
if not isinstance(response, Mapping):
|
|
737
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.response_invalid")
|
|
738
|
+
response_json = _canonical(
|
|
739
|
+
dict(response), "lifecycle_monitor.response_invalid",
|
|
740
|
+
)
|
|
741
|
+
copied = _json(response_json, "lifecycle_monitor.response_invalid")
|
|
742
|
+
expected = {
|
|
743
|
+
"schema", "request_id", "event_id", "site_id", "plan_id",
|
|
744
|
+
"website_version", "source_sequence", "status",
|
|
745
|
+
"required_locales", "approved_locales", "blocked_locales",
|
|
746
|
+
"queue_counts", "delivery", "tombstone",
|
|
747
|
+
}
|
|
748
|
+
client = _DISPATCH._CLIENT
|
|
749
|
+
if (
|
|
750
|
+
not isinstance(copied, dict)
|
|
751
|
+
or set(copied) != expected
|
|
752
|
+
or copied.get("schema") != client._API.LIFECYCLE_RESPONSE_SCHEMA
|
|
753
|
+
or copied.get("event_id") != claim.event_id
|
|
754
|
+
or copied.get("site_id") != claim.site_id
|
|
755
|
+
or copied.get("plan_id") != claim.plan_id
|
|
756
|
+
or copied.get("website_version") != claim.website_version
|
|
757
|
+
or copied.get("source_sequence") != claim.source_sequence
|
|
758
|
+
or copied.get("status") not in REMOTE_STATUSES - {"superseded"}
|
|
759
|
+
or not client.CMSLocalizationHTTPClient._valid_lifecycle(copied)
|
|
760
|
+
or len(copied.get("required_locales", [])) != claim.job_count
|
|
761
|
+
):
|
|
762
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.response_invalid")
|
|
763
|
+
return copied, response_json
|
|
764
|
+
|
|
765
|
+
def _validated_row(self, row: sqlite3.Row) -> None:
|
|
766
|
+
try:
|
|
767
|
+
event_id = _DISPATCH._token(
|
|
768
|
+
row["event_id"], "lifecycle_monitor.state_invalid",
|
|
769
|
+
)
|
|
770
|
+
site_id = _DISPATCH._token(
|
|
771
|
+
row["site_id"], "lifecycle_monitor.state_invalid",
|
|
772
|
+
)
|
|
773
|
+
plan_id = _DISPATCH._token(
|
|
774
|
+
row["plan_id"], "lifecycle_monitor.state_invalid",
|
|
775
|
+
)
|
|
776
|
+
website_version = _DISPATCH._token(
|
|
777
|
+
row["website_version"], "lifecycle_monitor.state_invalid",
|
|
778
|
+
)
|
|
779
|
+
created = _DISPATCH._timestamp(
|
|
780
|
+
row["created_at"], "lifecycle_monitor.state_invalid",
|
|
781
|
+
)
|
|
782
|
+
updated = _DISPATCH._timestamp(
|
|
783
|
+
row["updated_at"], "lifecycle_monitor.state_invalid",
|
|
784
|
+
)
|
|
785
|
+
next_poll = _DISPATCH._timestamp(
|
|
786
|
+
row["next_poll_at"], "lifecycle_monitor.state_invalid",
|
|
787
|
+
)
|
|
788
|
+
except (TypeError, LifecycleMonitorBlocked):
|
|
789
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.state_invalid") from None
|
|
790
|
+
state = row["state"]
|
|
791
|
+
attempts = row["poll_attempts"]
|
|
792
|
+
failures = row["consecutive_failures"]
|
|
793
|
+
maximum = row["max_consecutive_failures"]
|
|
794
|
+
source_sequence = row["source_sequence"]
|
|
795
|
+
job_count = row["job_count"]
|
|
796
|
+
error = row["last_error_code"]
|
|
797
|
+
remote = row["remote_status"]
|
|
798
|
+
leases = (row["lease_owner"], row["lease_token"], row["lease_expires_at"])
|
|
799
|
+
snapshot_columns = (
|
|
800
|
+
"lifecycle_sha256", "lifecycle_json", "required_locales_json",
|
|
801
|
+
"approved_locales_json", "blocked_locales_json",
|
|
802
|
+
"queue_counts_json", "delivery_json", "tombstone_json",
|
|
803
|
+
)
|
|
804
|
+
snapshot = tuple(row[name] for name in snapshot_columns)
|
|
805
|
+
has_snapshot = all(item is not None for item in snapshot)
|
|
806
|
+
immediate_terminal = (
|
|
807
|
+
state == "terminal" and remote in {"cancelled", "superseded"}
|
|
808
|
+
and not any(item is not None for item in snapshot)
|
|
809
|
+
)
|
|
810
|
+
if has_snapshot:
|
|
811
|
+
response = self._stored(row, "lifecycle_json", None)
|
|
812
|
+
required = self._stored(row, "required_locales_json", None)
|
|
813
|
+
approved = self._stored(row, "approved_locales_json", None)
|
|
814
|
+
blocked = self._stored(row, "blocked_locales_json", None)
|
|
815
|
+
counts = self._stored(row, "queue_counts_json", None)
|
|
816
|
+
delivery = self._stored(row, "delivery_json", None)
|
|
817
|
+
tombstone = self._stored(row, "tombstone_json", None)
|
|
818
|
+
expected_response_keys = {
|
|
819
|
+
"schema", "request_id", "event_id", "site_id", "plan_id",
|
|
820
|
+
"website_version", "source_sequence", "status",
|
|
821
|
+
"required_locales", "approved_locales", "blocked_locales",
|
|
822
|
+
"queue_counts", "delivery", "tombstone",
|
|
823
|
+
}
|
|
824
|
+
valid_snapshot = (
|
|
825
|
+
isinstance(response, dict)
|
|
826
|
+
and set(response) == expected_response_keys
|
|
827
|
+
and response.get("schema")
|
|
828
|
+
== _DISPATCH._CLIENT._API.LIFECYCLE_RESPONSE_SCHEMA
|
|
829
|
+
and isinstance(response.get("request_id"), str)
|
|
830
|
+
and _DISPATCH.TOKEN.fullmatch(response["request_id"]) is not None
|
|
831
|
+
and response.get("event_id") == event_id
|
|
832
|
+
and response.get("site_id") == site_id
|
|
833
|
+
and response.get("plan_id") == plan_id
|
|
834
|
+
and response.get("website_version") == website_version
|
|
835
|
+
and response.get("source_sequence") == source_sequence
|
|
836
|
+
and response.get("status") == remote
|
|
837
|
+
and response.get("required_locales") == required
|
|
838
|
+
and response.get("approved_locales") == approved
|
|
839
|
+
and response.get("blocked_locales") == blocked
|
|
840
|
+
and response.get("queue_counts") == counts
|
|
841
|
+
and response.get("delivery") == delivery
|
|
842
|
+
and response.get("tombstone") == tombstone
|
|
843
|
+
and _DISPATCH._CLIENT.CMSLocalizationHTTPClient._valid_lifecycle(
|
|
844
|
+
response
|
|
845
|
+
)
|
|
846
|
+
and len(required) == job_count
|
|
847
|
+
and row["lifecycle_sha256"]
|
|
848
|
+
== _DISPATCH._hash(row["lifecycle_json"])
|
|
849
|
+
)
|
|
850
|
+
else:
|
|
851
|
+
valid_snapshot = not any(item is not None for item in snapshot)
|
|
852
|
+
if (
|
|
853
|
+
event_id != row["event_id"] or site_id != row["site_id"]
|
|
854
|
+
or plan_id != row["plan_id"]
|
|
855
|
+
or website_version != row["website_version"]
|
|
856
|
+
or state not in STATES
|
|
857
|
+
or isinstance(source_sequence, bool)
|
|
858
|
+
or not isinstance(source_sequence, int) or source_sequence <= 0
|
|
859
|
+
or isinstance(job_count, bool) or not isinstance(job_count, int)
|
|
860
|
+
or job_count <= 0
|
|
861
|
+
or not isinstance(row["change_sha256"], str)
|
|
862
|
+
or _DISPATCH.SHA256.fullmatch(row["change_sha256"]) is None
|
|
863
|
+
or not isinstance(row["binding_sha256"], str)
|
|
864
|
+
or _DISPATCH.SHA256.fullmatch(row["binding_sha256"]) is None
|
|
865
|
+
or row["binding_sha256"] != _binding(
|
|
866
|
+
event_id, site_id, plan_id, website_version,
|
|
867
|
+
source_sequence, job_count, row["change_sha256"], maximum,
|
|
868
|
+
)
|
|
869
|
+
or isinstance(attempts, bool) or not isinstance(attempts, int)
|
|
870
|
+
or attempts < 0
|
|
871
|
+
or isinstance(failures, bool) or not isinstance(failures, int)
|
|
872
|
+
or isinstance(maximum, bool) or not isinstance(maximum, int)
|
|
873
|
+
or not 0 <= failures <= maximum <= MAX_FAILURES or maximum < 1
|
|
874
|
+
or next_poll < created or updated < created
|
|
875
|
+
or (state == "leased") != all(item is not None for item in leases)
|
|
876
|
+
or (state != "leased" and any(item is not None for item in leases))
|
|
877
|
+
or (state == "leased" and (
|
|
878
|
+
_DISPATCH._token(
|
|
879
|
+
leases[0], "lifecycle_monitor.state_invalid", limit=128,
|
|
880
|
+
) != leases[0]
|
|
881
|
+
or _DISPATCH._token(
|
|
882
|
+
leases[1], "lifecycle_monitor.state_invalid",
|
|
883
|
+
) != leases[1]
|
|
884
|
+
or _DISPATCH._timestamp(
|
|
885
|
+
leases[2], "lifecycle_monitor.state_invalid",
|
|
886
|
+
) <= updated
|
|
887
|
+
))
|
|
888
|
+
or (error is not None and (
|
|
889
|
+
not isinstance(error, str)
|
|
890
|
+
or _DISPATCH.ERROR_CODE.fullmatch(error) is None
|
|
891
|
+
))
|
|
892
|
+
or (state in {"retry_wait", "failed"}) != (error is not None)
|
|
893
|
+
or (remote is not None and remote not in REMOTE_STATUSES)
|
|
894
|
+
or not valid_snapshot
|
|
895
|
+
or (not has_snapshot and remote is not None and not immediate_terminal)
|
|
896
|
+
or (has_snapshot and (
|
|
897
|
+
not isinstance(row["lifecycle_sha256"], str)
|
|
898
|
+
or _DISPATCH.SHA256.fullmatch(row["lifecycle_sha256"]) is None
|
|
899
|
+
))
|
|
900
|
+
or (remote is None and has_snapshot)
|
|
901
|
+
or (has_snapshot and remote in TERMINAL_SUCCESS | TERMINAL_FAILURE
|
|
902
|
+
and state != "terminal")
|
|
903
|
+
or (state == "watching" and (
|
|
904
|
+
not has_snapshot or remote in TERMINAL_SUCCESS | TERMINAL_FAILURE
|
|
905
|
+
))
|
|
906
|
+
or (state == "terminal" and not (
|
|
907
|
+
immediate_terminal
|
|
908
|
+
or (has_snapshot and remote in TERMINAL_SUCCESS | TERMINAL_FAILURE)
|
|
909
|
+
))
|
|
910
|
+
or (state == "pending" and (remote is not None or attempts != 0))
|
|
911
|
+
):
|
|
912
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.state_invalid")
|
|
913
|
+
|
|
914
|
+
def _claim(self, claim: Any) -> LifecyclePollClaim:
|
|
915
|
+
if not isinstance(claim, LifecyclePollClaim):
|
|
916
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.claim_invalid")
|
|
917
|
+
_DISPATCH._token(claim.event_id, "lifecycle_monitor.claim_invalid")
|
|
918
|
+
_DISPATCH._token(claim.site_id, "lifecycle_monitor.claim_invalid")
|
|
919
|
+
_DISPATCH._token(claim.plan_id, "lifecycle_monitor.claim_invalid")
|
|
920
|
+
_DISPATCH._token(
|
|
921
|
+
claim.website_version, "lifecycle_monitor.claim_invalid",
|
|
922
|
+
)
|
|
923
|
+
_DISPATCH._token(
|
|
924
|
+
claim.lease_owner, "lifecycle_monitor.claim_invalid", limit=128,
|
|
925
|
+
)
|
|
926
|
+
_DISPATCH._token(claim.lease_token, "lifecycle_monitor.claim_invalid")
|
|
927
|
+
if (
|
|
928
|
+
isinstance(claim.source_sequence, bool)
|
|
929
|
+
or not isinstance(claim.source_sequence, int)
|
|
930
|
+
or claim.source_sequence <= 0
|
|
931
|
+
or isinstance(claim.job_count, bool)
|
|
932
|
+
or not isinstance(claim.job_count, int) or claim.job_count <= 0
|
|
933
|
+
or not isinstance(claim.change_sha256, str)
|
|
934
|
+
or _DISPATCH.SHA256.fullmatch(claim.change_sha256) is None
|
|
935
|
+
or isinstance(claim.attempt, bool)
|
|
936
|
+
or not isinstance(claim.attempt, int) or claim.attempt <= 0
|
|
937
|
+
or isinstance(claim.max_consecutive_failures, bool)
|
|
938
|
+
or not isinstance(claim.max_consecutive_failures, int)
|
|
939
|
+
or not 1 <= claim.max_consecutive_failures <= MAX_FAILURES
|
|
940
|
+
):
|
|
941
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.claim_invalid")
|
|
942
|
+
_DISPATCH._timestamp(
|
|
943
|
+
claim.lease_expires_at, "lifecycle_monitor.claim_invalid",
|
|
944
|
+
)
|
|
945
|
+
return claim
|
|
946
|
+
|
|
947
|
+
def _require_live_claim(
|
|
948
|
+
self, claim: LifecyclePollClaim, now: float,
|
|
949
|
+
) -> sqlite3.Row:
|
|
950
|
+
row = self.connection.execute(
|
|
951
|
+
"SELECT * FROM cms_source_lifecycle_monitor WHERE event_id = ?",
|
|
952
|
+
(claim.event_id,),
|
|
953
|
+
).fetchone()
|
|
954
|
+
if row is None:
|
|
955
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.claim_missing")
|
|
956
|
+
self._validated_row(row)
|
|
957
|
+
if (
|
|
958
|
+
row["state"] != "leased"
|
|
959
|
+
or row["site_id"] != claim.site_id
|
|
960
|
+
or row["plan_id"] != claim.plan_id
|
|
961
|
+
or row["website_version"] != claim.website_version
|
|
962
|
+
or row["source_sequence"] != claim.source_sequence
|
|
963
|
+
or row["job_count"] != claim.job_count
|
|
964
|
+
or row["change_sha256"] != claim.change_sha256
|
|
965
|
+
or row["poll_attempts"] != claim.attempt
|
|
966
|
+
or row["max_consecutive_failures"] != claim.max_consecutive_failures
|
|
967
|
+
or row["lease_owner"] != claim.lease_owner
|
|
968
|
+
or row["lease_token"] != claim.lease_token
|
|
969
|
+
or row["lease_expires_at"] != claim.lease_expires_at
|
|
970
|
+
):
|
|
971
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.claim_lost")
|
|
972
|
+
if row["lease_expires_at"] <= now:
|
|
973
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.lease_expired")
|
|
974
|
+
if now < row["updated_at"]:
|
|
975
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.clock_regressed")
|
|
976
|
+
return row
|
|
977
|
+
|
|
978
|
+
def _stored(self, row: sqlite3.Row, name: str, empty: Any) -> Any:
|
|
979
|
+
raw = row[name]
|
|
980
|
+
if raw is None:
|
|
981
|
+
return empty
|
|
982
|
+
value = _json(raw, "lifecycle_monitor.state_invalid")
|
|
983
|
+
if _canonical(value, "lifecycle_monitor.state_invalid") != raw:
|
|
984
|
+
raise LifecycleMonitorBlocked("lifecycle_monitor.state_invalid")
|
|
985
|
+
return value
|
|
986
|
+
|
|
987
|
+
def _retry_delay(self, failures: int) -> float:
|
|
988
|
+
return min(
|
|
989
|
+
self.max_delay_seconds,
|
|
990
|
+
self.base_delay_seconds * (2 ** max(0, failures - 1)),
|
|
991
|
+
)
|