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,875 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Secure source-side HTTPS client for the public CMS localization API.
|
|
3
|
+
|
|
4
|
+
The caller supplies already-versioned change, cancellation, and tombstone
|
|
5
|
+
objects. This adapter signs their canonical UTF-8 bytes, performs exactly one
|
|
6
|
+
HTTP attempt against a fixed origin, and binds every response to the operation
|
|
7
|
+
and request that produced it. Retry scheduling remains a host responsibility.
|
|
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 socket
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
import urllib.error
|
|
22
|
+
import urllib.parse
|
|
23
|
+
import urllib.request
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Callable, Mapping, Protocol
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
MAX_ORIGIN_LENGTH = 2048
|
|
30
|
+
MAX_HEADER_VALUE_LENGTH = 4096
|
|
31
|
+
MAX_RESPONSE_BYTES = 4_000_000
|
|
32
|
+
HEADER_NAME = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$")
|
|
33
|
+
TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
|
|
34
|
+
SIGNATURE_VALUE = re.compile(r"^[A-Za-z0-9_.:/+=-]{1,4096}$")
|
|
35
|
+
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
36
|
+
ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
|
|
37
|
+
RESERVED_HEADERS = {
|
|
38
|
+
"accept", "accept-encoding", "connection", "content-length",
|
|
39
|
+
"content-type", "host", "transfer-encoding",
|
|
40
|
+
"x-localization-key-id", "x-localization-signature",
|
|
41
|
+
"x-localization-signature-algorithm",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_module(name: str, path: Path):
|
|
46
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
47
|
+
if spec is None or spec.loader is None:
|
|
48
|
+
raise RuntimeError(f"cannot load required CMS client dependency: {path.name}")
|
|
49
|
+
module = importlib.util.module_from_spec(spec)
|
|
50
|
+
sys.modules[spec.name] = module
|
|
51
|
+
spec.loader.exec_module(module)
|
|
52
|
+
return module
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
56
|
+
_API = _load_module(
|
|
57
|
+
"blun_website_localization_cms_client_api",
|
|
58
|
+
_ROOT / "integrations" / "website_localization_api.py",
|
|
59
|
+
)
|
|
60
|
+
_CMS = _load_module(
|
|
61
|
+
"blun_website_localization_cms_client_cms",
|
|
62
|
+
_ROOT / "integrations" / "website_localization_cms.py",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CMSClientFailed(RuntimeError):
|
|
67
|
+
"""Stable, content-free client failure with an explicit retry decision."""
|
|
68
|
+
|
|
69
|
+
cms_client_failure = True
|
|
70
|
+
|
|
71
|
+
def __init__(self, code: str, *, retryable: bool):
|
|
72
|
+
if not isinstance(code, str) or ERROR_CODE.fullmatch(code) is None:
|
|
73
|
+
raise ValueError("CMS client error code is invalid")
|
|
74
|
+
if not isinstance(retryable, bool):
|
|
75
|
+
raise ValueError("CMS client retryability must be boolean")
|
|
76
|
+
super().__init__(code)
|
|
77
|
+
self.code = code
|
|
78
|
+
self.retryable = retryable
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class HTTPResult:
|
|
83
|
+
status: int
|
|
84
|
+
headers: tuple[tuple[str, str], ...]
|
|
85
|
+
body: bytes
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class HTTPTransport(Protocol):
|
|
89
|
+
def post(
|
|
90
|
+
self,
|
|
91
|
+
url: str,
|
|
92
|
+
headers: Mapping[str, str],
|
|
93
|
+
body: bytes,
|
|
94
|
+
*,
|
|
95
|
+
timeout: float,
|
|
96
|
+
) -> HTTPResult: ...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
100
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class URLTransport:
|
|
105
|
+
"""Perform one bounded stdlib POST while refusing redirects."""
|
|
106
|
+
|
|
107
|
+
def __init__(self):
|
|
108
|
+
self._opener = urllib.request.build_opener(_NoRedirect)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _read(response: Any) -> bytes:
|
|
112
|
+
try:
|
|
113
|
+
return response.read(MAX_RESPONSE_BYTES + 1)
|
|
114
|
+
except (TimeoutError, socket.timeout, OSError):
|
|
115
|
+
raise CMSClientFailed("network", retryable=True) from None
|
|
116
|
+
|
|
117
|
+
def post(
|
|
118
|
+
self,
|
|
119
|
+
url: str,
|
|
120
|
+
headers: Mapping[str, str],
|
|
121
|
+
body: bytes,
|
|
122
|
+
*,
|
|
123
|
+
timeout: float,
|
|
124
|
+
) -> HTTPResult:
|
|
125
|
+
request = urllib.request.Request(
|
|
126
|
+
url, data=body, headers=dict(headers), method="POST",
|
|
127
|
+
)
|
|
128
|
+
try:
|
|
129
|
+
response = self._opener.open(request, timeout=timeout)
|
|
130
|
+
except urllib.error.HTTPError as error:
|
|
131
|
+
try:
|
|
132
|
+
return HTTPResult(
|
|
133
|
+
int(error.code),
|
|
134
|
+
tuple(error.headers.items()) if error.headers is not None else (),
|
|
135
|
+
self._read(error),
|
|
136
|
+
)
|
|
137
|
+
finally:
|
|
138
|
+
error.close()
|
|
139
|
+
except (urllib.error.URLError, TimeoutError, socket.timeout, OSError):
|
|
140
|
+
raise CMSClientFailed("network", retryable=True) from None
|
|
141
|
+
try:
|
|
142
|
+
return HTTPResult(
|
|
143
|
+
int(response.status), tuple(response.headers.items()),
|
|
144
|
+
self._read(response),
|
|
145
|
+
)
|
|
146
|
+
finally:
|
|
147
|
+
response.close()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _canonical_json(value: Any, *, code: str) -> bytes:
|
|
151
|
+
try:
|
|
152
|
+
encoded = json.dumps(
|
|
153
|
+
value,
|
|
154
|
+
ensure_ascii=False,
|
|
155
|
+
allow_nan=False,
|
|
156
|
+
sort_keys=True,
|
|
157
|
+
separators=(",", ":"),
|
|
158
|
+
).encode("utf-8")
|
|
159
|
+
except (TypeError, ValueError, RecursionError):
|
|
160
|
+
raise CMSClientFailed(code, retryable=False) from None
|
|
161
|
+
if not encoded or len(encoded) > _API.MAX_MESSAGE_BYTES:
|
|
162
|
+
raise CMSClientFailed(code, retryable=False)
|
|
163
|
+
return encoded
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _pairs(items):
|
|
167
|
+
result = {}
|
|
168
|
+
for key, value in items:
|
|
169
|
+
if key in result:
|
|
170
|
+
raise ValueError("duplicate JSON key")
|
|
171
|
+
result[key] = value
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _constant(_value):
|
|
176
|
+
raise ValueError("non-finite JSON number")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _token(value: Any, *, code: str) -> str:
|
|
180
|
+
if not isinstance(value, str) or TOKEN.fullmatch(value) is None:
|
|
181
|
+
raise CMSClientFailed(code, retryable=False)
|
|
182
|
+
return value
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _valid_token(value: Any) -> bool:
|
|
186
|
+
return isinstance(value, str) and TOKEN.fullmatch(value) is not None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _valid_sha256(value: Any, *, optional: bool = False) -> bool:
|
|
190
|
+
return (optional and value is None) or (
|
|
191
|
+
isinstance(value, str) and SHA256.fullmatch(value) is not None
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _valid_nonnegative_number(value: Any) -> bool:
|
|
196
|
+
return (
|
|
197
|
+
not isinstance(value, bool)
|
|
198
|
+
and isinstance(value, (int, float))
|
|
199
|
+
and math.isfinite(float(value))
|
|
200
|
+
and value >= 0
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _timestamp(value: Any, *, code: str) -> float:
|
|
205
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
206
|
+
raise CMSClientFailed(code, retryable=False)
|
|
207
|
+
result = float(value)
|
|
208
|
+
if result < 0 or not math.isfinite(result):
|
|
209
|
+
raise CMSClientFailed(code, retryable=False)
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _origin(value: Any, allow_loopback_http: bool) -> str:
|
|
214
|
+
if (
|
|
215
|
+
not isinstance(value, str)
|
|
216
|
+
or value != value.strip()
|
|
217
|
+
or not value
|
|
218
|
+
or not value.isascii()
|
|
219
|
+
or len(value) > MAX_ORIGIN_LENGTH
|
|
220
|
+
or any(ord(character) <= 32 or ord(character) == 127 for character in value)
|
|
221
|
+
):
|
|
222
|
+
raise ValueError("origin is invalid")
|
|
223
|
+
try:
|
|
224
|
+
parsed = urllib.parse.urlsplit(value)
|
|
225
|
+
hostname = parsed.hostname
|
|
226
|
+
port = parsed.port
|
|
227
|
+
except ValueError:
|
|
228
|
+
raise ValueError("origin is invalid") from None
|
|
229
|
+
if (
|
|
230
|
+
not hostname
|
|
231
|
+
or not hostname.isascii()
|
|
232
|
+
or parsed.username is not None
|
|
233
|
+
or parsed.password is not None
|
|
234
|
+
or parsed.query
|
|
235
|
+
or parsed.fragment
|
|
236
|
+
or parsed.path not in {"", "/"}
|
|
237
|
+
):
|
|
238
|
+
raise ValueError("origin must contain only scheme and authority")
|
|
239
|
+
if parsed.scheme == "https":
|
|
240
|
+
pass
|
|
241
|
+
elif (
|
|
242
|
+
parsed.scheme == "http"
|
|
243
|
+
and allow_loopback_http
|
|
244
|
+
and hostname.lower() in {"localhost", "127.0.0.1", "::1"}
|
|
245
|
+
):
|
|
246
|
+
pass
|
|
247
|
+
else:
|
|
248
|
+
raise ValueError("origin must use HTTPS")
|
|
249
|
+
if port is not None and not 1 <= port <= 65535:
|
|
250
|
+
raise ValueError("origin is invalid")
|
|
251
|
+
return value.rstrip("/")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _authentication_headers(
|
|
255
|
+
provider: Callable[[], Mapping[str, str]],
|
|
256
|
+
) -> dict[str, str]:
|
|
257
|
+
try:
|
|
258
|
+
supplied = provider()
|
|
259
|
+
except Exception:
|
|
260
|
+
raise CMSClientFailed("authentication", retryable=False) from None
|
|
261
|
+
if not isinstance(supplied, Mapping):
|
|
262
|
+
raise CMSClientFailed("authentication", retryable=False)
|
|
263
|
+
result: dict[str, str] = {}
|
|
264
|
+
normalized_names: set[str] = set()
|
|
265
|
+
for name, value in supplied.items():
|
|
266
|
+
normalized = name.lower() if isinstance(name, str) else ""
|
|
267
|
+
if (
|
|
268
|
+
not isinstance(name, str)
|
|
269
|
+
or HEADER_NAME.fullmatch(name) is None
|
|
270
|
+
or normalized in RESERVED_HEADERS
|
|
271
|
+
or normalized in normalized_names
|
|
272
|
+
or not isinstance(value, str)
|
|
273
|
+
or not value
|
|
274
|
+
or not value.isascii()
|
|
275
|
+
or len(value) > MAX_HEADER_VALUE_LENGTH
|
|
276
|
+
or any(ord(character) < 32 or ord(character) == 127 for character in value)
|
|
277
|
+
):
|
|
278
|
+
raise CMSClientFailed("authentication", retryable=False)
|
|
279
|
+
normalized_names.add(normalized)
|
|
280
|
+
result[name] = value
|
|
281
|
+
return result
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _signature(value: Any) -> tuple[str, str, str]:
|
|
285
|
+
try:
|
|
286
|
+
algorithm = value.algorithm
|
|
287
|
+
key_id = value.key_id
|
|
288
|
+
signature = value.signature
|
|
289
|
+
except Exception:
|
|
290
|
+
raise CMSClientFailed("signing", retryable=False) from None
|
|
291
|
+
if (
|
|
292
|
+
not isinstance(algorithm, str)
|
|
293
|
+
or TOKEN.fullmatch(algorithm) is None
|
|
294
|
+
or not isinstance(key_id, str)
|
|
295
|
+
or TOKEN.fullmatch(key_id) is None
|
|
296
|
+
or not isinstance(signature, str)
|
|
297
|
+
or SIGNATURE_VALUE.fullmatch(signature) is None
|
|
298
|
+
):
|
|
299
|
+
raise CMSClientFailed("signing", retryable=False)
|
|
300
|
+
return algorithm, key_id, signature
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _response_headers(value: Any) -> dict[str, str]:
|
|
304
|
+
if not isinstance(value, tuple):
|
|
305
|
+
raise CMSClientFailed("transport_invalid", retryable=True)
|
|
306
|
+
result: dict[str, str] = {}
|
|
307
|
+
for item in value:
|
|
308
|
+
if (
|
|
309
|
+
not isinstance(item, tuple)
|
|
310
|
+
or len(item) != 2
|
|
311
|
+
or not all(isinstance(part, str) for part in item)
|
|
312
|
+
):
|
|
313
|
+
raise CMSClientFailed("transport_invalid", retryable=True)
|
|
314
|
+
name, content = item[0].lower(), item[1].strip()
|
|
315
|
+
if name in {"content-type", "content-length"}:
|
|
316
|
+
if name in result:
|
|
317
|
+
raise CMSClientFailed("response_headers", retryable=True)
|
|
318
|
+
result[name] = content
|
|
319
|
+
return result
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _copy_request(value: Any, *, schema: str, code: str) -> dict[str, Any]:
|
|
323
|
+
if not isinstance(value, Mapping):
|
|
324
|
+
raise CMSClientFailed(code, retryable=False)
|
|
325
|
+
raw = _canonical_json(dict(value), code=code)
|
|
326
|
+
try:
|
|
327
|
+
copied = json.loads(raw.decode("utf-8"), object_pairs_hook=_pairs)
|
|
328
|
+
except (UnicodeDecodeError, json.JSONDecodeError, ValueError, RecursionError):
|
|
329
|
+
raise CMSClientFailed(code, retryable=False) from None
|
|
330
|
+
if not isinstance(copied, dict) or copied.get("schema") != schema:
|
|
331
|
+
raise CMSClientFailed(code, retryable=False)
|
|
332
|
+
return copied
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class CMSLocalizationHTTPClient:
|
|
336
|
+
"""Call every public CMS API operation through one exact signed boundary."""
|
|
337
|
+
|
|
338
|
+
_OPERATIONS = {
|
|
339
|
+
"change": (_API.CHANGE_PATH, _CMS.CHANGE_SCHEMA),
|
|
340
|
+
"cancellation": (_API.CANCELLATION_PATH, _CMS.CANCELLATION_SCHEMA),
|
|
341
|
+
"tombstone": (_API.TOMBSTONE_PATH, _CMS.TOMBSTONE_SCHEMA),
|
|
342
|
+
"status": (_API.STATUS_PATH, _API.STATUS_REQUEST_SCHEMA),
|
|
343
|
+
"lifecycle": (_API.LIFECYCLE_PATH, _API.LIFECYCLE_REQUEST_SCHEMA),
|
|
344
|
+
"capabilities": (
|
|
345
|
+
_API.CAPABILITIES_PATH, _API.CAPABILITIES_REQUEST_SCHEMA,
|
|
346
|
+
),
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
def __init__(
|
|
350
|
+
self,
|
|
351
|
+
origin: str,
|
|
352
|
+
authentication_headers: Callable[[], Mapping[str, str]],
|
|
353
|
+
signing_authority: Any,
|
|
354
|
+
*,
|
|
355
|
+
transport: HTTPTransport | None = None,
|
|
356
|
+
timeout: float = 30.0,
|
|
357
|
+
allow_loopback_http: bool = False,
|
|
358
|
+
clock: Callable[[], float | int] = time.time,
|
|
359
|
+
request_id_factory: Callable[[], str] | None = None,
|
|
360
|
+
):
|
|
361
|
+
if not isinstance(allow_loopback_http, bool):
|
|
362
|
+
raise TypeError("allow_loopback_http must be boolean")
|
|
363
|
+
self.origin = _origin(origin, allow_loopback_http)
|
|
364
|
+
if not callable(authentication_headers):
|
|
365
|
+
raise TypeError("authentication_headers must be callable")
|
|
366
|
+
if not callable(getattr(signing_authority, "sign", None)):
|
|
367
|
+
raise TypeError("signing_authority must provide sign")
|
|
368
|
+
if not callable(clock):
|
|
369
|
+
raise TypeError("clock must be callable")
|
|
370
|
+
if (
|
|
371
|
+
isinstance(timeout, bool)
|
|
372
|
+
or not isinstance(timeout, (int, float))
|
|
373
|
+
or not 0 < timeout <= 300
|
|
374
|
+
):
|
|
375
|
+
raise ValueError("timeout is outside the supported range")
|
|
376
|
+
self.authentication_headers = authentication_headers
|
|
377
|
+
self.signing_authority = signing_authority
|
|
378
|
+
self.transport = URLTransport() if transport is None else transport
|
|
379
|
+
if not callable(getattr(self.transport, "post", None)):
|
|
380
|
+
raise TypeError("transport must provide post")
|
|
381
|
+
self.timeout = float(timeout)
|
|
382
|
+
self.clock = clock
|
|
383
|
+
self.request_id_factory = (
|
|
384
|
+
(lambda: secrets.token_hex(16))
|
|
385
|
+
if request_id_factory is None else request_id_factory
|
|
386
|
+
)
|
|
387
|
+
if not callable(self.request_id_factory):
|
|
388
|
+
raise TypeError("request_id_factory must be callable")
|
|
389
|
+
|
|
390
|
+
def submit_change(self, change: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
391
|
+
request = _copy_request(
|
|
392
|
+
change, schema=_CMS.CHANGE_SCHEMA, code="change_invalid",
|
|
393
|
+
)
|
|
394
|
+
response = self._post("change", request, {200, 202})
|
|
395
|
+
expected = {
|
|
396
|
+
"schema", "event_id", "plan_id", "job_count", "inserted_jobs",
|
|
397
|
+
"status",
|
|
398
|
+
}
|
|
399
|
+
if (
|
|
400
|
+
set(response) != expected
|
|
401
|
+
or response.get("schema") != _API.API_SCHEMA
|
|
402
|
+
or response.get("event_id") != request.get("event_id")
|
|
403
|
+
or not _valid_token(response.get("plan_id"))
|
|
404
|
+
or isinstance(response.get("job_count"), bool)
|
|
405
|
+
or not isinstance(response.get("job_count"), int)
|
|
406
|
+
or response["job_count"] <= 0
|
|
407
|
+
or isinstance(response.get("inserted_jobs"), bool)
|
|
408
|
+
or not isinstance(response.get("inserted_jobs"), int)
|
|
409
|
+
or not 0 <= response["inserted_jobs"] <= response["job_count"]
|
|
410
|
+
or response.get("status") not in {"enqueued", "superseded", "cancelled"}
|
|
411
|
+
):
|
|
412
|
+
raise CMSClientFailed("response_binding", retryable=True)
|
|
413
|
+
return response
|
|
414
|
+
|
|
415
|
+
def cancel(self, cancellation: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
416
|
+
request = _copy_request(
|
|
417
|
+
cancellation,
|
|
418
|
+
schema=_CMS.CANCELLATION_SCHEMA,
|
|
419
|
+
code="cancellation_invalid",
|
|
420
|
+
)
|
|
421
|
+
response = self._post("cancellation", request, {200, 202})
|
|
422
|
+
if (
|
|
423
|
+
set(response) != {
|
|
424
|
+
"schema", "cancellation_id", "event_id", "status",
|
|
425
|
+
"newly_cancelled",
|
|
426
|
+
}
|
|
427
|
+
or response.get("schema") != _API.API_SCHEMA
|
|
428
|
+
or response.get("cancellation_id") != request.get("cancellation_id")
|
|
429
|
+
or response.get("event_id") != request.get("event_id")
|
|
430
|
+
or response.get("status") != "cancelled"
|
|
431
|
+
or not isinstance(response.get("newly_cancelled"), bool)
|
|
432
|
+
):
|
|
433
|
+
raise CMSClientFailed("response_binding", retryable=True)
|
|
434
|
+
return response
|
|
435
|
+
|
|
436
|
+
def request_tombstone(self, tombstone: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
437
|
+
request = _copy_request(
|
|
438
|
+
tombstone, schema=_CMS.TOMBSTONE_SCHEMA, code="tombstone_invalid",
|
|
439
|
+
)
|
|
440
|
+
response = self._post("tombstone", request, {200, 202})
|
|
441
|
+
if (
|
|
442
|
+
set(response) != {
|
|
443
|
+
"schema", "tombstone_id", "event_id", "delivery_id",
|
|
444
|
+
"status", "newly_requested",
|
|
445
|
+
}
|
|
446
|
+
or response.get("schema") != _API.API_SCHEMA
|
|
447
|
+
or response.get("tombstone_id") != request.get("tombstone_id")
|
|
448
|
+
or response.get("event_id") != request.get("event_id")
|
|
449
|
+
or not _valid_token(response.get("delivery_id"))
|
|
450
|
+
or response.get("status") not in {
|
|
451
|
+
"pending", "leased", "retry_wait", "succeeded", "failed",
|
|
452
|
+
}
|
|
453
|
+
or not isinstance(response.get("newly_requested"), bool)
|
|
454
|
+
):
|
|
455
|
+
raise CMSClientFailed("response_binding", retryable=True)
|
|
456
|
+
return response
|
|
457
|
+
|
|
458
|
+
def status(
|
|
459
|
+
self, event_id: str, site_id: str, *, request_id: str | None = None,
|
|
460
|
+
) -> Mapping[str, Any]:
|
|
461
|
+
request = self._read_request(
|
|
462
|
+
_API.STATUS_REQUEST_SCHEMA, event_id, site_id, request_id,
|
|
463
|
+
)
|
|
464
|
+
response = self._post("status", request, {200})
|
|
465
|
+
expected = {
|
|
466
|
+
"schema", "status", "request_id", "event_id", "site_id",
|
|
467
|
+
"plan_id", "website_version", "source_sequence", "job_count",
|
|
468
|
+
"counts", "locales", "cancelled", "queue_recovery_pending",
|
|
469
|
+
}
|
|
470
|
+
if (
|
|
471
|
+
set(response) != expected
|
|
472
|
+
or response.get("schema") != _API.API_SCHEMA
|
|
473
|
+
or response.get("status") != "PROGRESS"
|
|
474
|
+
or response.get("request_id") != request["request_id"]
|
|
475
|
+
or response.get("event_id") != event_id
|
|
476
|
+
or response.get("site_id") != site_id
|
|
477
|
+
or not self._valid_progress(response)
|
|
478
|
+
):
|
|
479
|
+
raise CMSClientFailed("response_binding", retryable=True)
|
|
480
|
+
return response
|
|
481
|
+
|
|
482
|
+
def lifecycle(
|
|
483
|
+
self, event_id: str, site_id: str, *, request_id: str | None = None,
|
|
484
|
+
) -> Mapping[str, Any]:
|
|
485
|
+
request = self._read_request(
|
|
486
|
+
_API.LIFECYCLE_REQUEST_SCHEMA, event_id, site_id, request_id,
|
|
487
|
+
)
|
|
488
|
+
response = self._post("lifecycle", request, {200})
|
|
489
|
+
expected = {
|
|
490
|
+
"schema", "request_id", "event_id", "site_id", "plan_id",
|
|
491
|
+
"website_version", "source_sequence", "status",
|
|
492
|
+
"required_locales", "approved_locales", "blocked_locales",
|
|
493
|
+
"queue_counts", "delivery", "tombstone",
|
|
494
|
+
}
|
|
495
|
+
if (
|
|
496
|
+
set(response) != expected
|
|
497
|
+
or response.get("schema") != _API.LIFECYCLE_RESPONSE_SCHEMA
|
|
498
|
+
or response.get("request_id") != request["request_id"]
|
|
499
|
+
or response.get("event_id") != event_id
|
|
500
|
+
or response.get("site_id") != site_id
|
|
501
|
+
or not self._valid_lifecycle(response)
|
|
502
|
+
):
|
|
503
|
+
raise CMSClientFailed("response_binding", retryable=True)
|
|
504
|
+
return response
|
|
505
|
+
|
|
506
|
+
def capabilities(self, *, request_id: str | None = None) -> Mapping[str, Any]:
|
|
507
|
+
request = {
|
|
508
|
+
"schema": _API.CAPABILITIES_REQUEST_SCHEMA,
|
|
509
|
+
"request_id": self._request_id(request_id),
|
|
510
|
+
"requested_at": _timestamp(self.clock(), code="clock_invalid"),
|
|
511
|
+
}
|
|
512
|
+
response = self._post("capabilities", request, {200})
|
|
513
|
+
if (
|
|
514
|
+
set(response) != {
|
|
515
|
+
"schema", "status", "request_id", "capabilities", "api_contract",
|
|
516
|
+
}
|
|
517
|
+
or response.get("schema") != _API.API_SCHEMA
|
|
518
|
+
or response.get("status") != "CAPABILITIES"
|
|
519
|
+
or response.get("request_id") != request["request_id"]
|
|
520
|
+
or not self._valid_capabilities(response)
|
|
521
|
+
):
|
|
522
|
+
raise CMSClientFailed("response_binding", retryable=True)
|
|
523
|
+
return response
|
|
524
|
+
|
|
525
|
+
def _read_request(
|
|
526
|
+
self,
|
|
527
|
+
schema: str,
|
|
528
|
+
event_id: Any,
|
|
529
|
+
site_id: Any,
|
|
530
|
+
request_id: str | None,
|
|
531
|
+
) -> dict[str, Any]:
|
|
532
|
+
return {
|
|
533
|
+
"schema": schema,
|
|
534
|
+
"request_id": self._request_id(request_id),
|
|
535
|
+
"event_id": _token(event_id, code="request_invalid"),
|
|
536
|
+
"site_id": _token(site_id, code="request_invalid"),
|
|
537
|
+
"requested_at": _timestamp(self.clock(), code="clock_invalid"),
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
def _request_id(self, supplied: str | None) -> str:
|
|
541
|
+
if supplied is None:
|
|
542
|
+
try:
|
|
543
|
+
supplied = self.request_id_factory()
|
|
544
|
+
except Exception:
|
|
545
|
+
raise CMSClientFailed("request_id_invalid", retryable=False) from None
|
|
546
|
+
return _token(supplied, code="request_id_invalid")
|
|
547
|
+
|
|
548
|
+
def _post(
|
|
549
|
+
self, operation: str, request: Mapping[str, Any], success: set[int],
|
|
550
|
+
) -> dict[str, Any]:
|
|
551
|
+
path, schema = self._OPERATIONS[operation]
|
|
552
|
+
if request.get("schema") != schema:
|
|
553
|
+
raise CMSClientFailed("request_invalid", retryable=False)
|
|
554
|
+
body = _canonical_json(dict(request), code="request_invalid")
|
|
555
|
+
try:
|
|
556
|
+
raw_signature = self.signing_authority.sign(body)
|
|
557
|
+
except Exception:
|
|
558
|
+
raise CMSClientFailed("signing", retryable=False) from None
|
|
559
|
+
algorithm, key_id, signature = _signature(raw_signature)
|
|
560
|
+
headers = _authentication_headers(self.authentication_headers)
|
|
561
|
+
headers.update({
|
|
562
|
+
"Accept": "application/json",
|
|
563
|
+
"Accept-Encoding": "identity",
|
|
564
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
565
|
+
"X-Localization-Signature-Algorithm": algorithm,
|
|
566
|
+
"X-Localization-Key-Id": key_id,
|
|
567
|
+
"X-Localization-Signature": signature,
|
|
568
|
+
})
|
|
569
|
+
try:
|
|
570
|
+
result = self.transport.post(
|
|
571
|
+
self.origin + path, headers, body, timeout=self.timeout,
|
|
572
|
+
)
|
|
573
|
+
except CMSClientFailed:
|
|
574
|
+
raise
|
|
575
|
+
except Exception:
|
|
576
|
+
raise CMSClientFailed("network", retryable=True) from None
|
|
577
|
+
return self._parse_response(result, success)
|
|
578
|
+
|
|
579
|
+
@staticmethod
|
|
580
|
+
def _parse_response(result: Any, success: set[int]) -> dict[str, Any]:
|
|
581
|
+
try:
|
|
582
|
+
status = result.status
|
|
583
|
+
raw_headers = result.headers
|
|
584
|
+
body = result.body
|
|
585
|
+
except Exception:
|
|
586
|
+
raise CMSClientFailed("transport_invalid", retryable=True) from None
|
|
587
|
+
if (
|
|
588
|
+
isinstance(status, bool)
|
|
589
|
+
or not isinstance(status, int)
|
|
590
|
+
or not 100 <= status <= 599
|
|
591
|
+
):
|
|
592
|
+
raise CMSClientFailed("transport_invalid", retryable=True)
|
|
593
|
+
if 300 <= status <= 399:
|
|
594
|
+
raise CMSClientFailed("redirect", retryable=False)
|
|
595
|
+
headers = _response_headers(raw_headers)
|
|
596
|
+
content_type = headers.get("content-type", "").lower().replace(" ", "")
|
|
597
|
+
if content_type not in {"application/json", "application/json;charset=utf-8"}:
|
|
598
|
+
raise CMSClientFailed("response_content_type", retryable=True)
|
|
599
|
+
if (
|
|
600
|
+
not isinstance(body, bytes)
|
|
601
|
+
or not body
|
|
602
|
+
or len(body) > MAX_RESPONSE_BYTES
|
|
603
|
+
):
|
|
604
|
+
raise CMSClientFailed("response_size", retryable=True)
|
|
605
|
+
declared = headers.get("content-length")
|
|
606
|
+
if declared is not None and (
|
|
607
|
+
not declared.isascii()
|
|
608
|
+
or not declared.isdecimal()
|
|
609
|
+
or int(declared) != len(body)
|
|
610
|
+
):
|
|
611
|
+
raise CMSClientFailed("response_size", retryable=True)
|
|
612
|
+
try:
|
|
613
|
+
text = body.decode("utf-8")
|
|
614
|
+
if text.startswith("\ufeff"):
|
|
615
|
+
raise ValueError("BOM rejected")
|
|
616
|
+
value = json.loads(
|
|
617
|
+
text, object_pairs_hook=_pairs, parse_constant=_constant,
|
|
618
|
+
)
|
|
619
|
+
except (UnicodeDecodeError, json.JSONDecodeError, ValueError, RecursionError):
|
|
620
|
+
raise CMSClientFailed("response_json", retryable=True) from None
|
|
621
|
+
if (
|
|
622
|
+
not isinstance(value, dict)
|
|
623
|
+
or _canonical_json(value, code="response_json") != body
|
|
624
|
+
):
|
|
625
|
+
raise CMSClientFailed("response_json", retryable=True)
|
|
626
|
+
if status not in success:
|
|
627
|
+
if (
|
|
628
|
+
set(value) == {"schema", "status", "error"}
|
|
629
|
+
and value.get("schema") == _API.API_SCHEMA
|
|
630
|
+
and value.get("status") == "BLOCK"
|
|
631
|
+
and isinstance(value.get("error"), str)
|
|
632
|
+
and ERROR_CODE.fullmatch(value["error"]) is not None
|
|
633
|
+
):
|
|
634
|
+
retryable = status in {408, 425, 429} or 500 <= status <= 599
|
|
635
|
+
raise CMSClientFailed(value["error"], retryable=retryable)
|
|
636
|
+
raise CMSClientFailed("http_status", retryable=500 <= status <= 599)
|
|
637
|
+
return value
|
|
638
|
+
|
|
639
|
+
@staticmethod
|
|
640
|
+
def _valid_progress(value: Mapping[str, Any]) -> bool:
|
|
641
|
+
counts = value.get("counts")
|
|
642
|
+
locales = value.get("locales")
|
|
643
|
+
recovery = value.get("queue_recovery_pending")
|
|
644
|
+
locale_keys = {
|
|
645
|
+
"job_id", "target_locale", "status", "attempts", "max_attempts",
|
|
646
|
+
"next_attempt_at", "lease_expires_at", "lease_expired",
|
|
647
|
+
"last_error_code", "last_error_detail_hash", "result_sha256",
|
|
648
|
+
}
|
|
649
|
+
queue_statuses = {"pending", "leased", "retry_wait", "succeeded", "failed"}
|
|
650
|
+
synthetic_statuses = {"cancelled", "awaiting_queue_resume"}
|
|
651
|
+
|
|
652
|
+
def valid_locale(item: Any) -> bool:
|
|
653
|
+
if not isinstance(item, dict) or set(item) != locale_keys:
|
|
654
|
+
return False
|
|
655
|
+
status = item.get("status")
|
|
656
|
+
attempts = item.get("attempts")
|
|
657
|
+
maximum = item.get("max_attempts")
|
|
658
|
+
lease = item.get("lease_expires_at")
|
|
659
|
+
error = item.get("last_error_code")
|
|
660
|
+
synthetic = status in synthetic_statuses
|
|
661
|
+
return (
|
|
662
|
+
_valid_token(item.get("job_id"))
|
|
663
|
+
and _valid_token(item.get("target_locale"))
|
|
664
|
+
and status in queue_statuses | synthetic_statuses
|
|
665
|
+
and isinstance(attempts, int)
|
|
666
|
+
and not isinstance(attempts, bool)
|
|
667
|
+
and isinstance(maximum, int)
|
|
668
|
+
and not isinstance(maximum, bool)
|
|
669
|
+
and 0 <= attempts <= maximum
|
|
670
|
+
and ((synthetic and maximum == 0) or (not synthetic and maximum > 0))
|
|
671
|
+
and _valid_nonnegative_number(item.get("next_attempt_at"))
|
|
672
|
+
and (lease is None or _valid_nonnegative_number(lease))
|
|
673
|
+
and isinstance(item.get("lease_expired"), bool)
|
|
674
|
+
and (status == "leased") == (lease is not None)
|
|
675
|
+
and (error is None or (
|
|
676
|
+
isinstance(error, str) and ERROR_CODE.fullmatch(error) is not None
|
|
677
|
+
))
|
|
678
|
+
and _valid_sha256(
|
|
679
|
+
item.get("last_error_detail_hash"), optional=True,
|
|
680
|
+
)
|
|
681
|
+
and _valid_sha256(item.get("result_sha256"), optional=True)
|
|
682
|
+
and (status == "succeeded")
|
|
683
|
+
== (item.get("result_sha256") is not None)
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
return (
|
|
687
|
+
_valid_token(value.get("plan_id"))
|
|
688
|
+
and _valid_token(value.get("website_version"))
|
|
689
|
+
and isinstance(value.get("source_sequence"), int)
|
|
690
|
+
and not isinstance(value.get("source_sequence"), bool)
|
|
691
|
+
and value["source_sequence"] > 0
|
|
692
|
+
and isinstance(value.get("job_count"), int)
|
|
693
|
+
and not isinstance(value.get("job_count"), bool)
|
|
694
|
+
and value["job_count"] >= 0
|
|
695
|
+
and isinstance(counts, dict)
|
|
696
|
+
and set(counts) in (queue_statuses, queue_statuses | {"cancelled"})
|
|
697
|
+
and ("cancelled" not in counts or value.get("cancelled") is True)
|
|
698
|
+
and all(
|
|
699
|
+
isinstance(count, int)
|
|
700
|
+
and not isinstance(count, bool)
|
|
701
|
+
and count >= 0
|
|
702
|
+
for count in counts.values()
|
|
703
|
+
)
|
|
704
|
+
and isinstance(recovery, bool)
|
|
705
|
+
and sum(counts.values()) == (0 if recovery else value["job_count"])
|
|
706
|
+
and isinstance(locales, list)
|
|
707
|
+
and len(locales) == value["job_count"]
|
|
708
|
+
and all(valid_locale(item) for item in locales)
|
|
709
|
+
and [item["target_locale"] for item in locales]
|
|
710
|
+
== sorted({item["target_locale"] for item in locales})
|
|
711
|
+
and isinstance(value.get("cancelled"), bool)
|
|
712
|
+
and not (value["cancelled"] and recovery)
|
|
713
|
+
and all(
|
|
714
|
+
item["status"] == "awaiting_queue_resume" for item in locales
|
|
715
|
+
) == recovery
|
|
716
|
+
and (
|
|
717
|
+
all(item["status"] != "cancelled" for item in locales)
|
|
718
|
+
or value["cancelled"]
|
|
719
|
+
)
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
@staticmethod
|
|
723
|
+
def _valid_lifecycle(value: Mapping[str, Any]) -> bool:
|
|
724
|
+
required = value.get("required_locales")
|
|
725
|
+
approved = value.get("approved_locales")
|
|
726
|
+
blocked = value.get("blocked_locales")
|
|
727
|
+
counts = value.get("queue_counts")
|
|
728
|
+
statuses = {
|
|
729
|
+
"queue_recovery", "cancelled", "deleted", "deletion_failed",
|
|
730
|
+
"deleting", "published", "publication_failed",
|
|
731
|
+
"publication_blocked", "publishing", "ready",
|
|
732
|
+
"localization_failed", "awaiting_approval", "processing",
|
|
733
|
+
}
|
|
734
|
+
queue_statuses = {"pending", "leased", "retry_wait", "succeeded", "failed"}
|
|
735
|
+
|
|
736
|
+
def valid_attempt_state(item: Any, *, tombstone: bool) -> bool:
|
|
737
|
+
keys = {
|
|
738
|
+
"delivery_id", "status", "attempts", "max_attempts",
|
|
739
|
+
"next_attempt_at", "lease_expires_at", "lease_expired",
|
|
740
|
+
"last_error_code", "last_error_detail_hash",
|
|
741
|
+
}
|
|
742
|
+
if tombstone:
|
|
743
|
+
keys.add("tombstone_id")
|
|
744
|
+
if not isinstance(item, dict) or set(item) != keys:
|
|
745
|
+
return False
|
|
746
|
+
status = item.get("status")
|
|
747
|
+
attempts = item.get("attempts")
|
|
748
|
+
maximum = item.get("max_attempts")
|
|
749
|
+
lease = item.get("lease_expires_at")
|
|
750
|
+
error = item.get("last_error_code")
|
|
751
|
+
return (
|
|
752
|
+
_valid_token(item.get("delivery_id"))
|
|
753
|
+
and (not tombstone or _valid_token(item.get("tombstone_id")))
|
|
754
|
+
and status in queue_statuses
|
|
755
|
+
and isinstance(attempts, int)
|
|
756
|
+
and not isinstance(attempts, bool)
|
|
757
|
+
and isinstance(maximum, int)
|
|
758
|
+
and not isinstance(maximum, bool)
|
|
759
|
+
and maximum >= 1
|
|
760
|
+
and 0 <= attempts <= maximum
|
|
761
|
+
and _valid_nonnegative_number(item.get("next_attempt_at"))
|
|
762
|
+
and (lease is None or _valid_nonnegative_number(lease))
|
|
763
|
+
and isinstance(item.get("lease_expired"), bool)
|
|
764
|
+
and (status == "leased") == (lease is not None)
|
|
765
|
+
and (error is None or (
|
|
766
|
+
isinstance(error, str) and ERROR_CODE.fullmatch(error) is not None
|
|
767
|
+
))
|
|
768
|
+
and _valid_sha256(
|
|
769
|
+
item.get("last_error_detail_hash"), optional=True,
|
|
770
|
+
)
|
|
771
|
+
and (status in {"retry_wait", "failed"}) == (error is not None)
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
return (
|
|
775
|
+
_valid_token(value.get("plan_id"))
|
|
776
|
+
and _valid_token(value.get("website_version"))
|
|
777
|
+
and isinstance(value.get("source_sequence"), int)
|
|
778
|
+
and not isinstance(value.get("source_sequence"), bool)
|
|
779
|
+
and value["source_sequence"] > 0
|
|
780
|
+
and value.get("status") in statuses
|
|
781
|
+
and isinstance(required, list)
|
|
782
|
+
and all(_valid_token(item) for item in required)
|
|
783
|
+
and required == sorted(set(required))
|
|
784
|
+
and isinstance(approved, list)
|
|
785
|
+
and all(_valid_token(item) for item in approved)
|
|
786
|
+
and approved == sorted(set(approved))
|
|
787
|
+
and set(approved).issubset(required)
|
|
788
|
+
and isinstance(blocked, list)
|
|
789
|
+
and all(
|
|
790
|
+
isinstance(item, list)
|
|
791
|
+
and len(item) == 2
|
|
792
|
+
and item[0] in required
|
|
793
|
+
and _valid_token(item[0])
|
|
794
|
+
and isinstance(item[1], str)
|
|
795
|
+
and ERROR_CODE.fullmatch(item[1]) is not None
|
|
796
|
+
for item in blocked
|
|
797
|
+
)
|
|
798
|
+
and blocked == [list(item) for item in sorted({tuple(item) for item in blocked})]
|
|
799
|
+
and isinstance(counts, dict)
|
|
800
|
+
and set(counts) in (queue_statuses, queue_statuses | {"cancelled"})
|
|
801
|
+
and ("cancelled" not in counts or value.get("status") == "cancelled")
|
|
802
|
+
and all(
|
|
803
|
+
isinstance(count, int)
|
|
804
|
+
and not isinstance(count, bool)
|
|
805
|
+
and count >= 0
|
|
806
|
+
for count in counts.values()
|
|
807
|
+
)
|
|
808
|
+
and (
|
|
809
|
+
(value.get("status") == "queue_recovery" and sum(counts.values()) == 0)
|
|
810
|
+
or (value.get("status") != "queue_recovery" and sum(counts.values()) == len(required))
|
|
811
|
+
)
|
|
812
|
+
and (value.get("delivery") is None or valid_attempt_state(
|
|
813
|
+
value["delivery"], tombstone=False,
|
|
814
|
+
))
|
|
815
|
+
and (value.get("tombstone") is None or valid_attempt_state(
|
|
816
|
+
value["tombstone"], tombstone=True,
|
|
817
|
+
))
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
@classmethod
|
|
821
|
+
def _valid_capabilities(cls, value: Mapping[str, Any]) -> bool:
|
|
822
|
+
capabilities = value.get("capabilities")
|
|
823
|
+
contract = value.get("api_contract")
|
|
824
|
+
if not isinstance(capabilities, dict) or not isinstance(contract, dict):
|
|
825
|
+
return False
|
|
826
|
+
unsigned_capabilities = dict(capabilities)
|
|
827
|
+
digest = unsigned_capabilities.pop("sha256", None)
|
|
828
|
+
unsigned_contract = dict(contract)
|
|
829
|
+
contract_digest = unsigned_contract.pop("sha256", None)
|
|
830
|
+
if (
|
|
831
|
+
not isinstance(digest, str)
|
|
832
|
+
or SHA256.fullmatch(digest) is None
|
|
833
|
+
or digest
|
|
834
|
+
!= hashlib.sha256(
|
|
835
|
+
_canonical_json(unsigned_capabilities, code="response_binding")
|
|
836
|
+
).hexdigest()
|
|
837
|
+
or not isinstance(contract_digest, str)
|
|
838
|
+
or SHA256.fullmatch(contract_digest) is None
|
|
839
|
+
or contract_digest
|
|
840
|
+
!= hashlib.sha256(
|
|
841
|
+
_canonical_json(unsigned_contract, code="response_binding")
|
|
842
|
+
).hexdigest()
|
|
843
|
+
or set(contract) != {
|
|
844
|
+
"schema", "api_schema", "error_schema", "operations", "sha256",
|
|
845
|
+
}
|
|
846
|
+
or contract.get("schema") != _API.API_CAPABILITIES_SCHEMA
|
|
847
|
+
or contract.get("api_schema") != _API.API_SCHEMA
|
|
848
|
+
or contract.get("error_schema") != _API.API_SCHEMA
|
|
849
|
+
):
|
|
850
|
+
return False
|
|
851
|
+
operations = contract.get("operations")
|
|
852
|
+
if not isinstance(operations, list) or len(operations) != len(cls._OPERATIONS):
|
|
853
|
+
return False
|
|
854
|
+
for item, (name, (path, request_schema)) in zip(
|
|
855
|
+
operations, cls._OPERATIONS.items(),
|
|
856
|
+
):
|
|
857
|
+
if (
|
|
858
|
+
not isinstance(item, dict)
|
|
859
|
+
or set(item) != {
|
|
860
|
+
"name", "method", "path", "request_schema",
|
|
861
|
+
"response_schema", "enabled",
|
|
862
|
+
}
|
|
863
|
+
or item.get("name") != name
|
|
864
|
+
or item.get("method") != "POST"
|
|
865
|
+
or item.get("path") != path
|
|
866
|
+
or item.get("request_schema") != request_schema
|
|
867
|
+
or item.get("response_schema")
|
|
868
|
+
!= (
|
|
869
|
+
_API.LIFECYCLE_RESPONSE_SCHEMA
|
|
870
|
+
if name == "lifecycle" else _API.API_SCHEMA
|
|
871
|
+
)
|
|
872
|
+
or not isinstance(item.get("enabled"), bool)
|
|
873
|
+
):
|
|
874
|
+
return False
|
|
875
|
+
return True
|