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,1142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Safe composition root for the durable terminal-notification receiver."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import os
|
|
11
|
+
import sqlite3
|
|
12
|
+
import stat
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable, Mapping
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_module(name: str, path: Path):
|
|
22
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
23
|
+
if spec is None or spec.loader is None:
|
|
24
|
+
raise RuntimeError(f"cannot load terminal receiver dependency: {path.name}")
|
|
25
|
+
module = importlib.util.module_from_spec(spec)
|
|
26
|
+
sys.modules[spec.name] = module
|
|
27
|
+
spec.loader.exec_module(module)
|
|
28
|
+
return module
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
32
|
+
_RECEIVER = _load_module(
|
|
33
|
+
"blun_website_localization_terminal_receiver_runtime_dependency",
|
|
34
|
+
_ROOT
|
|
35
|
+
/ "integrations"
|
|
36
|
+
/ "website_localization_cms_terminal_notification_receiver.py",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DurableTerminalReceiverRuntimeBlocked(RuntimeError):
|
|
41
|
+
"""Private composition failure that must not cross the HTTP boundary."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class DurableTerminalReceiverRuntimeHealth:
|
|
46
|
+
status: str
|
|
47
|
+
state: str
|
|
48
|
+
received: int
|
|
49
|
+
processing_counts: dict[str, int]
|
|
50
|
+
processing_due: int
|
|
51
|
+
expired_leases: int
|
|
52
|
+
failed: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _database_path(value: Any) -> str:
|
|
56
|
+
try:
|
|
57
|
+
result = os.fspath(value)
|
|
58
|
+
except TypeError as error:
|
|
59
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
60
|
+
"database path is invalid"
|
|
61
|
+
) from error
|
|
62
|
+
if isinstance(result, bytes):
|
|
63
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
64
|
+
"database path must be Unicode"
|
|
65
|
+
)
|
|
66
|
+
if (
|
|
67
|
+
not isinstance(result, str)
|
|
68
|
+
or not result
|
|
69
|
+
or "\x00" in result
|
|
70
|
+
or result.startswith("file:")
|
|
71
|
+
):
|
|
72
|
+
raise DurableTerminalReceiverRuntimeBlocked("database path is invalid")
|
|
73
|
+
if result == ":memory:":
|
|
74
|
+
return result
|
|
75
|
+
if (
|
|
76
|
+
os.name != "posix"
|
|
77
|
+
or not os.path.isabs(result)
|
|
78
|
+
or os.path.normpath(result) != result
|
|
79
|
+
):
|
|
80
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
81
|
+
"database path must be a canonical absolute POSIX path"
|
|
82
|
+
)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _validate_database_parent(database_path: str) -> None:
|
|
87
|
+
current = os.path.dirname(database_path)
|
|
88
|
+
first = True
|
|
89
|
+
while True:
|
|
90
|
+
try:
|
|
91
|
+
current_stat = os.lstat(current)
|
|
92
|
+
except OSError as error:
|
|
93
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
94
|
+
"terminal receiver database directory is unavailable"
|
|
95
|
+
) from error
|
|
96
|
+
mode = stat.S_IMODE(current_stat.st_mode)
|
|
97
|
+
sticky_root_directory = (
|
|
98
|
+
current_stat.st_uid == 0
|
|
99
|
+
and bool(mode & stat.S_ISVTX)
|
|
100
|
+
and bool(mode & 0o002)
|
|
101
|
+
)
|
|
102
|
+
if (
|
|
103
|
+
not stat.S_ISDIR(current_stat.st_mode)
|
|
104
|
+
or current_stat.st_uid not in {0, os.geteuid()}
|
|
105
|
+
or (first and current_stat.st_uid != os.geteuid())
|
|
106
|
+
or (mode & 0o022 and not sticky_root_directory)
|
|
107
|
+
):
|
|
108
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
109
|
+
"terminal receiver database directory is not private"
|
|
110
|
+
)
|
|
111
|
+
parent = os.path.dirname(current)
|
|
112
|
+
if parent == current:
|
|
113
|
+
break
|
|
114
|
+
current = parent
|
|
115
|
+
first = False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _validate_database_file(
|
|
119
|
+
database_path: str,
|
|
120
|
+
expected_identity: tuple[int, int] | None = None,
|
|
121
|
+
) -> tuple[int, int]:
|
|
122
|
+
try:
|
|
123
|
+
database_stat = os.lstat(database_path)
|
|
124
|
+
except OSError as error:
|
|
125
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
126
|
+
"terminal receiver database file is unavailable"
|
|
127
|
+
) from error
|
|
128
|
+
identity = (database_stat.st_dev, database_stat.st_ino)
|
|
129
|
+
if (
|
|
130
|
+
not stat.S_ISREG(database_stat.st_mode)
|
|
131
|
+
or database_stat.st_uid != os.geteuid()
|
|
132
|
+
or database_stat.st_nlink != 1
|
|
133
|
+
or stat.S_IMODE(database_stat.st_mode) != 0o600
|
|
134
|
+
or (expected_identity is not None and identity != expected_identity)
|
|
135
|
+
):
|
|
136
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
137
|
+
"terminal receiver database file is not private"
|
|
138
|
+
)
|
|
139
|
+
return identity
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _prepare_database_file(database_path: str) -> Callable[[], None]:
|
|
143
|
+
if database_path == ":memory:":
|
|
144
|
+
return lambda: None
|
|
145
|
+
_validate_database_parent(database_path)
|
|
146
|
+
try:
|
|
147
|
+
identity = _validate_database_file(database_path)
|
|
148
|
+
except DurableTerminalReceiverRuntimeBlocked as error:
|
|
149
|
+
if error.__cause__ is None or not isinstance(
|
|
150
|
+
error.__cause__, FileNotFoundError
|
|
151
|
+
):
|
|
152
|
+
raise
|
|
153
|
+
flags = os.O_CREAT | os.O_EXCL | os.O_RDWR
|
|
154
|
+
flags |= getattr(os, "O_CLOEXEC", 0)
|
|
155
|
+
flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
156
|
+
try:
|
|
157
|
+
descriptor = os.open(database_path, flags, 0o600)
|
|
158
|
+
except FileExistsError:
|
|
159
|
+
identity = _validate_database_file(database_path)
|
|
160
|
+
except OSError as create_error:
|
|
161
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
162
|
+
"terminal receiver database file could not be created"
|
|
163
|
+
) from create_error
|
|
164
|
+
else:
|
|
165
|
+
try:
|
|
166
|
+
os.fchmod(descriptor, 0o600)
|
|
167
|
+
created_stat = os.fstat(descriptor)
|
|
168
|
+
identity = (created_stat.st_dev, created_stat.st_ino)
|
|
169
|
+
if (
|
|
170
|
+
not stat.S_ISREG(created_stat.st_mode)
|
|
171
|
+
or created_stat.st_uid != os.geteuid()
|
|
172
|
+
or created_stat.st_nlink != 1
|
|
173
|
+
or stat.S_IMODE(created_stat.st_mode) != 0o600
|
|
174
|
+
):
|
|
175
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
176
|
+
"terminal receiver database file was not created privately"
|
|
177
|
+
)
|
|
178
|
+
finally:
|
|
179
|
+
os.close(descriptor)
|
|
180
|
+
|
|
181
|
+
def guard() -> None:
|
|
182
|
+
_validate_database_parent(database_path)
|
|
183
|
+
_validate_database_file(database_path, identity)
|
|
184
|
+
|
|
185
|
+
guard()
|
|
186
|
+
return guard
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _preflight_application(
|
|
190
|
+
authenticate: Callable[[Mapping[str, Any], Mapping[str, str]], Any],
|
|
191
|
+
*,
|
|
192
|
+
origin: str,
|
|
193
|
+
clock: Callable[[], float | int],
|
|
194
|
+
path: str,
|
|
195
|
+
require_https: bool,
|
|
196
|
+
) -> None:
|
|
197
|
+
dummy_inbox = object.__new__(_RECEIVER.DurableCMSTerminalNotificationInbox)
|
|
198
|
+
try:
|
|
199
|
+
_RECEIVER.CMSTerminalNotificationReceiverApplication(
|
|
200
|
+
dummy_inbox,
|
|
201
|
+
authenticate,
|
|
202
|
+
origin=origin,
|
|
203
|
+
clock=clock,
|
|
204
|
+
path=path,
|
|
205
|
+
require_https=require_https,
|
|
206
|
+
)
|
|
207
|
+
except (TypeError, ValueError) as error:
|
|
208
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
209
|
+
"terminal receiver configuration is invalid"
|
|
210
|
+
) from error
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _preflight_processing(
|
|
214
|
+
max_attempts: int,
|
|
215
|
+
base_delay_seconds: float | int,
|
|
216
|
+
max_delay_seconds: float | int,
|
|
217
|
+
) -> None:
|
|
218
|
+
if (
|
|
219
|
+
isinstance(max_attempts, bool)
|
|
220
|
+
or not isinstance(max_attempts, int)
|
|
221
|
+
or not 1 <= max_attempts <= 20
|
|
222
|
+
):
|
|
223
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
224
|
+
"terminal processing attempts are invalid"
|
|
225
|
+
)
|
|
226
|
+
try:
|
|
227
|
+
base_delay = _RECEIVER._duration(
|
|
228
|
+
base_delay_seconds, "processing_delay_invalid"
|
|
229
|
+
)
|
|
230
|
+
max_delay = _RECEIVER._duration(
|
|
231
|
+
max_delay_seconds, "processing_delay_invalid"
|
|
232
|
+
)
|
|
233
|
+
except Exception as error:
|
|
234
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
235
|
+
"terminal processing delay is invalid"
|
|
236
|
+
) from error
|
|
237
|
+
if base_delay > max_delay:
|
|
238
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
239
|
+
"terminal processing delay range is invalid"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class DurableTerminalNotificationReceiverRuntime:
|
|
244
|
+
"""Own one protected SQLite connection and its complete WSGI boundary."""
|
|
245
|
+
|
|
246
|
+
def __init__(
|
|
247
|
+
self,
|
|
248
|
+
connection: sqlite3.Connection,
|
|
249
|
+
inbox: Any,
|
|
250
|
+
application: Any,
|
|
251
|
+
database_guard: Callable[[], None],
|
|
252
|
+
clock: Callable[[], float | int],
|
|
253
|
+
):
|
|
254
|
+
self._connection = connection
|
|
255
|
+
self.inbox = inbox
|
|
256
|
+
self.application = application
|
|
257
|
+
self._database_guard = database_guard
|
|
258
|
+
self._clock = clock
|
|
259
|
+
self._lock = threading.RLock()
|
|
260
|
+
self._owner_pid = os.getpid()
|
|
261
|
+
self._closed = False
|
|
262
|
+
self._worker_state = "unmanaged"
|
|
263
|
+
self._worker_error_code: str | None = None
|
|
264
|
+
self._worker_stop = threading.Event()
|
|
265
|
+
self._worker_thread: threading.Thread | None = None
|
|
266
|
+
|
|
267
|
+
def _assert_owner(self) -> None:
|
|
268
|
+
if os.getpid() != self._owner_pid:
|
|
269
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
270
|
+
"terminal receiver runtime belongs to another process"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _require_open(self) -> None:
|
|
274
|
+
self._assert_owner()
|
|
275
|
+
if self._closed:
|
|
276
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
277
|
+
"terminal receiver runtime is closed"
|
|
278
|
+
)
|
|
279
|
+
try:
|
|
280
|
+
self._database_guard()
|
|
281
|
+
except Exception as error:
|
|
282
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
283
|
+
"terminal receiver database is unsafe"
|
|
284
|
+
) from error
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _unavailable(start_response: Callable[..., Any]):
|
|
288
|
+
value = {
|
|
289
|
+
"schema": _RECEIVER.ERROR_SCHEMA,
|
|
290
|
+
"status": "BLOCK",
|
|
291
|
+
"error_code": "notification_receiver.runtime_unavailable",
|
|
292
|
+
}
|
|
293
|
+
try:
|
|
294
|
+
body = json.dumps(
|
|
295
|
+
value, ensure_ascii=False, allow_nan=False, sort_keys=True,
|
|
296
|
+
separators=(",", ":"),
|
|
297
|
+
).encode("utf-8")
|
|
298
|
+
start_response("503 Service Unavailable", (
|
|
299
|
+
("Content-Type", "application/json; charset=utf-8"),
|
|
300
|
+
("Content-Length", str(len(body))),
|
|
301
|
+
("Cache-Control", "no-store"),
|
|
302
|
+
("X-Content-Type-Options", "nosniff"),
|
|
303
|
+
("Referrer-Policy", "no-referrer"),
|
|
304
|
+
))
|
|
305
|
+
return [body]
|
|
306
|
+
except Exception:
|
|
307
|
+
return []
|
|
308
|
+
|
|
309
|
+
def __call__(self, environ: Mapping[str, Any], start_response: Callable[..., Any]):
|
|
310
|
+
try:
|
|
311
|
+
self._assert_owner()
|
|
312
|
+
with self._lock:
|
|
313
|
+
self._require_open()
|
|
314
|
+
if isinstance(environ, Mapping) and environ.get("PATH_INFO") in {
|
|
315
|
+
_RECEIVER.STATUS_PATH, _RECEIVER.READINESS_PATH,
|
|
316
|
+
_RECEIVER.CAPABILITIES_PATH, _RECEIVER.HEALTH_PATH,
|
|
317
|
+
}:
|
|
318
|
+
return self._control_request(environ, start_response)
|
|
319
|
+
self.require_worker_ready()
|
|
320
|
+
return self.application(environ, start_response)
|
|
321
|
+
except Exception:
|
|
322
|
+
return self._unavailable(start_response)
|
|
323
|
+
|
|
324
|
+
def _control_request(
|
|
325
|
+
self,
|
|
326
|
+
environ: Mapping[str, Any],
|
|
327
|
+
start_response: Callable[..., Any],
|
|
328
|
+
):
|
|
329
|
+
"""Serve authenticated, content-free receiver control routes."""
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
path = environ.get("PATH_INFO")
|
|
333
|
+
if not isinstance(environ, Mapping):
|
|
334
|
+
_RECEIVER._blocked("environment_invalid", 400)
|
|
335
|
+
if self.application.require_https and (
|
|
336
|
+
environ.get("wsgi.url_scheme") != "https"
|
|
337
|
+
):
|
|
338
|
+
_RECEIVER._blocked("https_required", 400)
|
|
339
|
+
if environ.get("QUERY_STRING") not in {None, ""}:
|
|
340
|
+
_RECEIVER._blocked("query_rejected", 400)
|
|
341
|
+
if environ.get("HTTP_TRANSFER_ENCODING") not in {None, ""}:
|
|
342
|
+
_RECEIVER._blocked("framing_invalid", 400)
|
|
343
|
+
headers = self.application._headers(environ)
|
|
344
|
+
if path == _RECEIVER.CAPABILITIES_PATH:
|
|
345
|
+
return self._capabilities_request(
|
|
346
|
+
environ, headers, start_response,
|
|
347
|
+
)
|
|
348
|
+
if path == _RECEIVER.HEALTH_PATH:
|
|
349
|
+
return self._health_request(environ, headers, start_response)
|
|
350
|
+
if path == _RECEIVER.READINESS_PATH:
|
|
351
|
+
return self._readiness_request(
|
|
352
|
+
environ, headers, start_response,
|
|
353
|
+
)
|
|
354
|
+
if path == _RECEIVER.STATUS_PATH:
|
|
355
|
+
return self._status_request(environ, headers, start_response)
|
|
356
|
+
_RECEIVER._blocked("path_not_found", 404)
|
|
357
|
+
except _RECEIVER.TerminalNotificationReceiverBlocked as failure:
|
|
358
|
+
return self.application._send(start_response, failure.status, {
|
|
359
|
+
"schema": _RECEIVER.ERROR_SCHEMA,
|
|
360
|
+
"status": "BLOCK",
|
|
361
|
+
"error_code": failure.code,
|
|
362
|
+
})
|
|
363
|
+
except Exception:
|
|
364
|
+
return self._unavailable(start_response)
|
|
365
|
+
|
|
366
|
+
def _authenticate_control(
|
|
367
|
+
self,
|
|
368
|
+
request: dict[str, Any],
|
|
369
|
+
headers: dict[str, str],
|
|
370
|
+
site_id: str | None,
|
|
371
|
+
scope: str,
|
|
372
|
+
) -> None:
|
|
373
|
+
try:
|
|
374
|
+
principal = self.application.authenticate(
|
|
375
|
+
dict(request), dict(headers)
|
|
376
|
+
)
|
|
377
|
+
except Exception:
|
|
378
|
+
_RECEIVER._blocked("authentication_unavailable", 503)
|
|
379
|
+
_RECEIVER._principal(principal, site_id, scope)
|
|
380
|
+
|
|
381
|
+
def _capabilities_request(
|
|
382
|
+
self,
|
|
383
|
+
environ: Mapping[str, Any],
|
|
384
|
+
headers: dict[str, str],
|
|
385
|
+
start_response: Callable[..., Any],
|
|
386
|
+
):
|
|
387
|
+
if environ.get("REQUEST_METHOD") != "GET":
|
|
388
|
+
_RECEIVER._blocked("method_not_allowed", 405)
|
|
389
|
+
if headers.get("content-length") not in {None, "0"}:
|
|
390
|
+
_RECEIVER._blocked("body_invalid", 400)
|
|
391
|
+
if "content-type" in headers:
|
|
392
|
+
_RECEIVER._blocked("content_type", 415)
|
|
393
|
+
body_sha256 = hashlib.sha256(b"").hexdigest()
|
|
394
|
+
request = {
|
|
395
|
+
"schema": _RECEIVER.AUTH_SCHEMA,
|
|
396
|
+
"method": "GET",
|
|
397
|
+
"origin": self.application.origin,
|
|
398
|
+
"path": _RECEIVER.CAPABILITIES_PATH,
|
|
399
|
+
"body_sha256": body_sha256,
|
|
400
|
+
}
|
|
401
|
+
self._authenticate_control(
|
|
402
|
+
request, headers, None, _RECEIVER.CAPABILITIES_SCOPE,
|
|
403
|
+
)
|
|
404
|
+
return self.application._send(start_response, 200, {
|
|
405
|
+
"schema": _RECEIVER.CAPABILITIES_RESPONSE_SCHEMA,
|
|
406
|
+
"capabilities": _RECEIVER.capabilities_payload(
|
|
407
|
+
self.application.path
|
|
408
|
+
),
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
def _readiness_request(
|
|
412
|
+
self,
|
|
413
|
+
environ: Mapping[str, Any],
|
|
414
|
+
headers: dict[str, str],
|
|
415
|
+
start_response: Callable[..., Any],
|
|
416
|
+
):
|
|
417
|
+
if environ.get("REQUEST_METHOD") != "GET":
|
|
418
|
+
_RECEIVER._blocked("method_not_allowed", 405)
|
|
419
|
+
if headers.get("content-length") not in {None, "0"}:
|
|
420
|
+
_RECEIVER._blocked("body_invalid", 400)
|
|
421
|
+
if "content-type" in headers:
|
|
422
|
+
_RECEIVER._blocked("content_type", 415)
|
|
423
|
+
body_sha256 = hashlib.sha256(b"").hexdigest()
|
|
424
|
+
request = {
|
|
425
|
+
"schema": _RECEIVER.AUTH_SCHEMA,
|
|
426
|
+
"method": "GET",
|
|
427
|
+
"origin": self.application.origin,
|
|
428
|
+
"path": _RECEIVER.READINESS_PATH,
|
|
429
|
+
"body_sha256": body_sha256,
|
|
430
|
+
}
|
|
431
|
+
self._authenticate_control(
|
|
432
|
+
request, headers, None, _RECEIVER.READINESS_SCOPE,
|
|
433
|
+
)
|
|
434
|
+
report = {
|
|
435
|
+
**self.worker_readiness(),
|
|
436
|
+
"capabilities_sha256": _RECEIVER.capabilities_payload(
|
|
437
|
+
self.application.path
|
|
438
|
+
)["sha256"],
|
|
439
|
+
}
|
|
440
|
+
status = 200 if report["status"] == "ready" else 503
|
|
441
|
+
return self.application._send(start_response, status, report)
|
|
442
|
+
|
|
443
|
+
def _health_request(
|
|
444
|
+
self,
|
|
445
|
+
environ: Mapping[str, Any],
|
|
446
|
+
headers: dict[str, str],
|
|
447
|
+
start_response: Callable[..., Any],
|
|
448
|
+
):
|
|
449
|
+
if environ.get("REQUEST_METHOD") != "GET":
|
|
450
|
+
_RECEIVER._blocked("method_not_allowed", 405)
|
|
451
|
+
if headers.get("content-length") not in {None, "0"}:
|
|
452
|
+
_RECEIVER._blocked("body_invalid", 400)
|
|
453
|
+
if "content-type" in headers:
|
|
454
|
+
_RECEIVER._blocked("content_type", 415)
|
|
455
|
+
body_sha256 = hashlib.sha256(b"").hexdigest()
|
|
456
|
+
request = {
|
|
457
|
+
"schema": _RECEIVER.AUTH_SCHEMA,
|
|
458
|
+
"method": "GET",
|
|
459
|
+
"origin": self.application.origin,
|
|
460
|
+
"path": _RECEIVER.HEALTH_PATH,
|
|
461
|
+
"body_sha256": body_sha256,
|
|
462
|
+
}
|
|
463
|
+
self._authenticate_control(
|
|
464
|
+
request, headers, None, _RECEIVER.HEALTH_SCOPE,
|
|
465
|
+
)
|
|
466
|
+
report = self.worker_health()
|
|
467
|
+
counts = report.get("processing_counts") if isinstance(
|
|
468
|
+
report, Mapping
|
|
469
|
+
) else None
|
|
470
|
+
metrics = tuple(
|
|
471
|
+
report.get(name) if isinstance(report, Mapping) else None
|
|
472
|
+
for name in (
|
|
473
|
+
"received", "processing_due", "expired_leases", "failed",
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
metrics_available = counts is not None
|
|
477
|
+
worker_ok = isinstance(report, Mapping) and report.get(
|
|
478
|
+
"worker_state"
|
|
479
|
+
) in {"unmanaged", "running"}
|
|
480
|
+
healthy = (
|
|
481
|
+
isinstance(report, Mapping)
|
|
482
|
+
and report.get("status") == "ok"
|
|
483
|
+
and report.get("inbox_status") == "ok"
|
|
484
|
+
and report.get("error_code") is None
|
|
485
|
+
and worker_ok
|
|
486
|
+
)
|
|
487
|
+
valid_counts = (
|
|
488
|
+
isinstance(counts, dict)
|
|
489
|
+
and set(counts) == set(_RECEIVER.PROCESSING_STATUSES)
|
|
490
|
+
and all(
|
|
491
|
+
isinstance(value, int) and not isinstance(value, bool)
|
|
492
|
+
and value >= 0
|
|
493
|
+
for value in counts.values()
|
|
494
|
+
)
|
|
495
|
+
)
|
|
496
|
+
if not (
|
|
497
|
+
isinstance(report, dict)
|
|
498
|
+
and set(report) == set(_RECEIVER.HEALTH_RESPONSE_FIELDS)
|
|
499
|
+
and report.get("schema") == _RECEIVER.HEALTH_RESPONSE_SCHEMA
|
|
500
|
+
and report.get("status") in {"ok", "blocked"}
|
|
501
|
+
and report.get("runtime_state") == "open"
|
|
502
|
+
and report.get("worker_state") in {
|
|
503
|
+
"unmanaged", "starting", "running", "stopping", "stopped",
|
|
504
|
+
"failed",
|
|
505
|
+
}
|
|
506
|
+
and report.get("inbox_status") in {"ok", "blocked"}
|
|
507
|
+
and (report.get("error_code") is None or _RECEIVER._error(
|
|
508
|
+
report.get("error_code")
|
|
509
|
+
))
|
|
510
|
+
and healthy == (report.get("status") == "ok")
|
|
511
|
+
and (report.get("status") == "ok") == (
|
|
512
|
+
report.get("error_code") is None
|
|
513
|
+
)
|
|
514
|
+
and (
|
|
515
|
+
(metrics_available and valid_counts and all(
|
|
516
|
+
isinstance(value, int) and not isinstance(value, bool)
|
|
517
|
+
and value >= 0
|
|
518
|
+
for value in metrics
|
|
519
|
+
))
|
|
520
|
+
or (
|
|
521
|
+
not metrics_available
|
|
522
|
+
and counts is None
|
|
523
|
+
and all(value is None for value in metrics)
|
|
524
|
+
and report.get("status") == "blocked"
|
|
525
|
+
and report.get("inbox_status") == "blocked"
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
and (
|
|
529
|
+
not metrics_available
|
|
530
|
+
or (
|
|
531
|
+
sum(counts.values()) == report["received"]
|
|
532
|
+
and report["failed"] == counts["failed"]
|
|
533
|
+
and report["processing_due"] <= (
|
|
534
|
+
counts["pending"] + counts["retry_wait"]
|
|
535
|
+
)
|
|
536
|
+
and report["expired_leases"] <= counts["leased"]
|
|
537
|
+
and (report["inbox_status"] == "ok") == (
|
|
538
|
+
report["failed"] == 0
|
|
539
|
+
and report["expired_leases"] == 0
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
):
|
|
544
|
+
_RECEIVER._blocked("health_invalid", 503)
|
|
545
|
+
response = {
|
|
546
|
+
**report,
|
|
547
|
+
"capabilities_sha256": _RECEIVER.capabilities_payload(
|
|
548
|
+
self.application.path
|
|
549
|
+
)["sha256"],
|
|
550
|
+
}
|
|
551
|
+
status = 200 if report["status"] == "ok" else 503
|
|
552
|
+
return self.application._send(start_response, status, response)
|
|
553
|
+
|
|
554
|
+
def _status_request(
|
|
555
|
+
self,
|
|
556
|
+
environ: Mapping[str, Any],
|
|
557
|
+
headers: dict[str, str],
|
|
558
|
+
start_response: Callable[..., Any],
|
|
559
|
+
):
|
|
560
|
+
if environ.get("REQUEST_METHOD") != "POST":
|
|
561
|
+
_RECEIVER._blocked("method_not_allowed", 405)
|
|
562
|
+
if headers.get("content-type", "").lower().replace(" ", "") != (
|
|
563
|
+
"application/json;charset=utf-8"
|
|
564
|
+
):
|
|
565
|
+
_RECEIVER._blocked("content_type", 415)
|
|
566
|
+
length = headers.get("content-length")
|
|
567
|
+
if (
|
|
568
|
+
not isinstance(length, str)
|
|
569
|
+
or not length.isascii()
|
|
570
|
+
or not length.isdecimal()
|
|
571
|
+
):
|
|
572
|
+
_RECEIVER._blocked("content_length_required", 411)
|
|
573
|
+
size = int(length)
|
|
574
|
+
if size <= 0:
|
|
575
|
+
_RECEIVER._blocked("body_invalid", 400)
|
|
576
|
+
if size > _RECEIVER.MAX_BODY_BYTES:
|
|
577
|
+
_RECEIVER._blocked("body_too_large", 413)
|
|
578
|
+
try:
|
|
579
|
+
body = environ["wsgi.input"].read(size)
|
|
580
|
+
except Exception:
|
|
581
|
+
body = None
|
|
582
|
+
if not isinstance(body, bytes) or len(body) != size:
|
|
583
|
+
_RECEIVER._blocked("body_invalid", 400)
|
|
584
|
+
try:
|
|
585
|
+
text = body.decode("utf-8")
|
|
586
|
+
if text.startswith("\ufeff"):
|
|
587
|
+
raise ValueError("BOM rejected")
|
|
588
|
+
value = json.loads(
|
|
589
|
+
text,
|
|
590
|
+
object_pairs_hook=_RECEIVER._pairs,
|
|
591
|
+
parse_constant=_RECEIVER._constant,
|
|
592
|
+
)
|
|
593
|
+
except (
|
|
594
|
+
UnicodeDecodeError, json.JSONDecodeError, ValueError,
|
|
595
|
+
RecursionError,
|
|
596
|
+
):
|
|
597
|
+
_RECEIVER._blocked("json_invalid", 400)
|
|
598
|
+
if not (
|
|
599
|
+
isinstance(value, dict)
|
|
600
|
+
and set(value) == {"schema", "event_id", "site_id"}
|
|
601
|
+
and value.get("schema") == _RECEIVER.STATUS_REQUEST_SCHEMA
|
|
602
|
+
and _RECEIVER._token(value.get("event_id"))
|
|
603
|
+
and _RECEIVER._token(value.get("site_id"))
|
|
604
|
+
and _RECEIVER._canonical(value) == body
|
|
605
|
+
):
|
|
606
|
+
_RECEIVER._blocked("request_invalid", 400)
|
|
607
|
+
body_sha256 = hashlib.sha256(body).hexdigest()
|
|
608
|
+
if headers.get("x-localization-terminal-status-sha256") != body_sha256:
|
|
609
|
+
_RECEIVER._blocked("header_binding_invalid", 400)
|
|
610
|
+
request = {
|
|
611
|
+
"schema": _RECEIVER.AUTH_SCHEMA,
|
|
612
|
+
"method": "POST",
|
|
613
|
+
"origin": self.application.origin,
|
|
614
|
+
"path": _RECEIVER.STATUS_PATH,
|
|
615
|
+
"event_id": value["event_id"],
|
|
616
|
+
"site_id": value["site_id"],
|
|
617
|
+
"body_sha256": body_sha256,
|
|
618
|
+
}
|
|
619
|
+
self._authenticate_control(
|
|
620
|
+
request, headers, value["site_id"], _RECEIVER.STATUS_SCOPE,
|
|
621
|
+
)
|
|
622
|
+
processing = self.inbox.processing_status(
|
|
623
|
+
value["event_id"], now=self._clock(),
|
|
624
|
+
)
|
|
625
|
+
if processing.site_id != value["site_id"]:
|
|
626
|
+
_RECEIVER._blocked("not_found", 404)
|
|
627
|
+
response = {
|
|
628
|
+
"schema": _RECEIVER.STATUS_RESPONSE_SCHEMA,
|
|
629
|
+
"notification_id": processing.notification_id,
|
|
630
|
+
"event_id": processing.event_id,
|
|
631
|
+
"site_id": processing.site_id,
|
|
632
|
+
"terminal_status": processing.terminal_status,
|
|
633
|
+
"notification_sha256": processing.payload_sha256,
|
|
634
|
+
"processing_status": processing.status,
|
|
635
|
+
"attempts": processing.attempts,
|
|
636
|
+
"max_attempts": processing.max_attempts,
|
|
637
|
+
"next_attempt_at": processing.next_attempt_at,
|
|
638
|
+
"lease_expires_at": processing.lease_expires_at,
|
|
639
|
+
"lease_expired": processing.lease_expired,
|
|
640
|
+
"last_error_code": processing.last_error_code,
|
|
641
|
+
"processed_at": processing.processed_at,
|
|
642
|
+
"capabilities_sha256": _RECEIVER.capabilities_payload(
|
|
643
|
+
self.application.path
|
|
644
|
+
)["sha256"],
|
|
645
|
+
}
|
|
646
|
+
return self.application._send(start_response, 200, response)
|
|
647
|
+
|
|
648
|
+
def status(self, event_id: str):
|
|
649
|
+
self._assert_owner()
|
|
650
|
+
with self._lock:
|
|
651
|
+
self._require_open()
|
|
652
|
+
try:
|
|
653
|
+
return self.inbox.status(event_id)
|
|
654
|
+
except Exception as error:
|
|
655
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
656
|
+
"terminal receiver status is unavailable"
|
|
657
|
+
) from error
|
|
658
|
+
|
|
659
|
+
def health(self) -> DurableTerminalReceiverRuntimeHealth:
|
|
660
|
+
self._assert_owner()
|
|
661
|
+
with self._lock:
|
|
662
|
+
self._require_open()
|
|
663
|
+
try:
|
|
664
|
+
health = self.inbox.health(now=self._clock())
|
|
665
|
+
except Exception as error:
|
|
666
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
667
|
+
"terminal receiver health is unavailable"
|
|
668
|
+
) from error
|
|
669
|
+
if (
|
|
670
|
+
health.status not in {"ok", "blocked"}
|
|
671
|
+
or not isinstance(health.received, int)
|
|
672
|
+
or not isinstance(health.counts, dict)
|
|
673
|
+
or set(health.counts) != set(_RECEIVER.PROCESSING_STATUSES)
|
|
674
|
+
or any(
|
|
675
|
+
isinstance(value, bool) or not isinstance(value, int)
|
|
676
|
+
or value < 0
|
|
677
|
+
for value in health.counts.values()
|
|
678
|
+
)
|
|
679
|
+
or any(
|
|
680
|
+
isinstance(value, bool) or not isinstance(value, int)
|
|
681
|
+
or value < 0
|
|
682
|
+
for value in (
|
|
683
|
+
health.due, health.expired_leases, health.failed,
|
|
684
|
+
)
|
|
685
|
+
)
|
|
686
|
+
):
|
|
687
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
688
|
+
"terminal receiver health is invalid"
|
|
689
|
+
)
|
|
690
|
+
return DurableTerminalReceiverRuntimeHealth(
|
|
691
|
+
health.status,
|
|
692
|
+
"open",
|
|
693
|
+
health.received,
|
|
694
|
+
dict(health.counts),
|
|
695
|
+
health.due,
|
|
696
|
+
health.expired_leases,
|
|
697
|
+
health.failed,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
def process_next(
|
|
701
|
+
self,
|
|
702
|
+
callback: Callable[[Mapping[str, Any]], Any],
|
|
703
|
+
worker_id: str,
|
|
704
|
+
*,
|
|
705
|
+
now: float | int | None = None,
|
|
706
|
+
lease_seconds: float | int = 600,
|
|
707
|
+
):
|
|
708
|
+
"""Run at most one lease-bound host processing callback."""
|
|
709
|
+
|
|
710
|
+
self._assert_owner()
|
|
711
|
+
with self._lock:
|
|
712
|
+
self._require_open()
|
|
713
|
+
try:
|
|
714
|
+
return self.inbox.run_next_processing(
|
|
715
|
+
callback,
|
|
716
|
+
worker_id,
|
|
717
|
+
now=self._clock() if now is None else now,
|
|
718
|
+
lease_seconds=lease_seconds,
|
|
719
|
+
)
|
|
720
|
+
except Exception as error:
|
|
721
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
722
|
+
"terminal receiver processing is unavailable"
|
|
723
|
+
) from error
|
|
724
|
+
|
|
725
|
+
@staticmethod
|
|
726
|
+
def _worker_configuration(
|
|
727
|
+
callback: Any,
|
|
728
|
+
worker_id: Any,
|
|
729
|
+
lease_seconds: float | int,
|
|
730
|
+
active_delay_seconds: float | int,
|
|
731
|
+
idle_delay_seconds: float | int,
|
|
732
|
+
blocked_delay_seconds: float | int,
|
|
733
|
+
) -> tuple[float, float, float, float]:
|
|
734
|
+
if (
|
|
735
|
+
not callable(callback)
|
|
736
|
+
or not isinstance(worker_id, str)
|
|
737
|
+
or not _RECEIVER._token(worker_id)
|
|
738
|
+
or len(worker_id) > 128
|
|
739
|
+
):
|
|
740
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
741
|
+
"terminal receiver worker configuration is invalid"
|
|
742
|
+
)
|
|
743
|
+
try:
|
|
744
|
+
values = tuple(
|
|
745
|
+
_RECEIVER._duration(value, "worker_duration_invalid")
|
|
746
|
+
for value in (
|
|
747
|
+
lease_seconds,
|
|
748
|
+
active_delay_seconds,
|
|
749
|
+
idle_delay_seconds,
|
|
750
|
+
blocked_delay_seconds,
|
|
751
|
+
)
|
|
752
|
+
)
|
|
753
|
+
except Exception as error:
|
|
754
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
755
|
+
"terminal receiver worker configuration is invalid"
|
|
756
|
+
) from error
|
|
757
|
+
return values
|
|
758
|
+
|
|
759
|
+
@staticmethod
|
|
760
|
+
def _join_timeout(value: float | int) -> float:
|
|
761
|
+
if (
|
|
762
|
+
isinstance(value, bool)
|
|
763
|
+
or not isinstance(value, (int, float))
|
|
764
|
+
or not math.isfinite(float(value))
|
|
765
|
+
or not 0 < float(value) <= 300
|
|
766
|
+
):
|
|
767
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
768
|
+
"terminal receiver worker timeout is invalid"
|
|
769
|
+
)
|
|
770
|
+
return float(value)
|
|
771
|
+
|
|
772
|
+
def _managed_worker(
|
|
773
|
+
self,
|
|
774
|
+
stop: threading.Event,
|
|
775
|
+
callback: Callable[[Mapping[str, Any]], Any],
|
|
776
|
+
worker_id: str,
|
|
777
|
+
configuration: tuple[float, float, float, float],
|
|
778
|
+
) -> None:
|
|
779
|
+
try:
|
|
780
|
+
with self._lock:
|
|
781
|
+
if self._worker_state != "starting" or self._worker_stop is not stop:
|
|
782
|
+
return
|
|
783
|
+
self._worker_state = "running"
|
|
784
|
+
lease, active, idle, blocked = configuration
|
|
785
|
+
while not stop.is_set():
|
|
786
|
+
outcome = self.process_next(
|
|
787
|
+
callback, worker_id, lease_seconds=lease,
|
|
788
|
+
)
|
|
789
|
+
delay = (
|
|
790
|
+
idle if outcome is None
|
|
791
|
+
else blocked if outcome.status in {"retry_wait", "failed"}
|
|
792
|
+
else active
|
|
793
|
+
)
|
|
794
|
+
stop.wait(delay)
|
|
795
|
+
except Exception:
|
|
796
|
+
with self._lock:
|
|
797
|
+
self._worker_error_code = (
|
|
798
|
+
"notification_receiver.worker_blocked"
|
|
799
|
+
)
|
|
800
|
+
self._worker_state = "failed"
|
|
801
|
+
return
|
|
802
|
+
with self._lock:
|
|
803
|
+
if self._worker_state in {"running", "stopping"}:
|
|
804
|
+
self._worker_state = "stopped"
|
|
805
|
+
|
|
806
|
+
def start_worker(
|
|
807
|
+
self,
|
|
808
|
+
callback: Callable[[Mapping[str, Any]], Any],
|
|
809
|
+
worker_id: str,
|
|
810
|
+
*,
|
|
811
|
+
lease_seconds: float | int = 600,
|
|
812
|
+
active_delay_seconds: float | int = 0.05,
|
|
813
|
+
idle_delay_seconds: float | int = 1,
|
|
814
|
+
blocked_delay_seconds: float | int = 5,
|
|
815
|
+
) -> None:
|
|
816
|
+
"""Start one process-owned, interruptible processing worker."""
|
|
817
|
+
|
|
818
|
+
configuration = self._worker_configuration(
|
|
819
|
+
callback,
|
|
820
|
+
worker_id,
|
|
821
|
+
lease_seconds,
|
|
822
|
+
active_delay_seconds,
|
|
823
|
+
idle_delay_seconds,
|
|
824
|
+
blocked_delay_seconds,
|
|
825
|
+
)
|
|
826
|
+
self._assert_owner()
|
|
827
|
+
with self._lock:
|
|
828
|
+
self._require_open()
|
|
829
|
+
if self._worker_state in {"starting", "running"}:
|
|
830
|
+
return
|
|
831
|
+
if self._worker_state in {"stopping", "failed"}:
|
|
832
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
833
|
+
"terminal receiver worker is blocked"
|
|
834
|
+
)
|
|
835
|
+
stop = threading.Event()
|
|
836
|
+
thread = threading.Thread(
|
|
837
|
+
target=self._managed_worker,
|
|
838
|
+
args=(stop, callback, worker_id, configuration),
|
|
839
|
+
name="cms-terminal-receiver-worker",
|
|
840
|
+
daemon=False,
|
|
841
|
+
)
|
|
842
|
+
self._worker_stop = stop
|
|
843
|
+
self._worker_thread = thread
|
|
844
|
+
self._worker_error_code = None
|
|
845
|
+
self._worker_state = "starting"
|
|
846
|
+
try:
|
|
847
|
+
thread.start()
|
|
848
|
+
except Exception as error:
|
|
849
|
+
self._worker_thread = None
|
|
850
|
+
self._worker_state = "failed"
|
|
851
|
+
self._worker_error_code = (
|
|
852
|
+
"notification_receiver.worker_blocked"
|
|
853
|
+
)
|
|
854
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
855
|
+
"terminal receiver worker is blocked"
|
|
856
|
+
) from error
|
|
857
|
+
|
|
858
|
+
def stop_worker(self, *, timeout_seconds: float | int = 30) -> None:
|
|
859
|
+
"""Stop and join the managed worker without closing its database."""
|
|
860
|
+
|
|
861
|
+
timeout = self._join_timeout(timeout_seconds)
|
|
862
|
+
self._assert_owner()
|
|
863
|
+
thread = self._worker_thread
|
|
864
|
+
state = self._worker_state
|
|
865
|
+
if thread is None or state in {"unmanaged", "stopped"}:
|
|
866
|
+
return
|
|
867
|
+
if thread is threading.current_thread():
|
|
868
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
869
|
+
"terminal receiver worker cannot stop itself"
|
|
870
|
+
)
|
|
871
|
+
if state != "failed":
|
|
872
|
+
self._worker_state = "stopping"
|
|
873
|
+
self._worker_stop.set()
|
|
874
|
+
thread.join(timeout)
|
|
875
|
+
if thread.is_alive():
|
|
876
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
877
|
+
"terminal receiver worker stop timed out"
|
|
878
|
+
)
|
|
879
|
+
with self._lock:
|
|
880
|
+
if self._worker_state == "stopping":
|
|
881
|
+
self._worker_state = "stopped"
|
|
882
|
+
|
|
883
|
+
def require_worker_ready(self) -> None:
|
|
884
|
+
"""Block hosted HTTP intake unless its processor is healthy."""
|
|
885
|
+
|
|
886
|
+
self._assert_owner()
|
|
887
|
+
if self._closed:
|
|
888
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
889
|
+
"terminal receiver runtime is closed"
|
|
890
|
+
)
|
|
891
|
+
if self._worker_state == "unmanaged":
|
|
892
|
+
return
|
|
893
|
+
if self._worker_state != "running" or self._worker_error_code is not None:
|
|
894
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
895
|
+
"terminal receiver worker is not ready"
|
|
896
|
+
)
|
|
897
|
+
if self.health().status != "ok":
|
|
898
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
899
|
+
"terminal receiver storage is blocked"
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
def worker_readiness(self) -> dict[str, Any]:
|
|
903
|
+
"""Return a content-free readiness snapshot for a host supervisor."""
|
|
904
|
+
|
|
905
|
+
self._assert_owner()
|
|
906
|
+
with self._lock:
|
|
907
|
+
state = "closed" if self._closed else self._worker_state
|
|
908
|
+
error_code = self._worker_error_code
|
|
909
|
+
if state != "running":
|
|
910
|
+
return {
|
|
911
|
+
"schema": "blun.cms-terminal-receiver-readiness.v1",
|
|
912
|
+
"status": "not_ready",
|
|
913
|
+
"worker_state": state,
|
|
914
|
+
"inbox_status": None,
|
|
915
|
+
"error_code": error_code or (
|
|
916
|
+
"notification_receiver.worker_not_ready"
|
|
917
|
+
),
|
|
918
|
+
}
|
|
919
|
+
try:
|
|
920
|
+
inbox_status = self.health().status
|
|
921
|
+
except Exception:
|
|
922
|
+
inbox_status = "blocked"
|
|
923
|
+
ready = inbox_status == "ok"
|
|
924
|
+
return {
|
|
925
|
+
"schema": "blun.cms-terminal-receiver-readiness.v1",
|
|
926
|
+
"status": "ready" if ready else "not_ready",
|
|
927
|
+
"worker_state": "running",
|
|
928
|
+
"inbox_status": inbox_status,
|
|
929
|
+
"error_code": None if ready else (
|
|
930
|
+
"notification_receiver.storage_blocked"
|
|
931
|
+
),
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
def worker_health(self) -> dict[str, Any]:
|
|
935
|
+
"""Return aggregate runtime, worker, and durable inbox health."""
|
|
936
|
+
|
|
937
|
+
self._assert_owner()
|
|
938
|
+
with self._lock:
|
|
939
|
+
self._require_open()
|
|
940
|
+
worker_state = self._worker_state
|
|
941
|
+
worker_error = self._worker_error_code
|
|
942
|
+
try:
|
|
943
|
+
inbox = self.health()
|
|
944
|
+
except Exception:
|
|
945
|
+
return {
|
|
946
|
+
"schema": _RECEIVER.HEALTH_RESPONSE_SCHEMA,
|
|
947
|
+
"status": "blocked",
|
|
948
|
+
"runtime_state": "open",
|
|
949
|
+
"worker_state": worker_state,
|
|
950
|
+
"inbox_status": "blocked",
|
|
951
|
+
"received": None,
|
|
952
|
+
"processing_counts": None,
|
|
953
|
+
"processing_due": None,
|
|
954
|
+
"expired_leases": None,
|
|
955
|
+
"failed": None,
|
|
956
|
+
"error_code": "notification_receiver.storage_blocked",
|
|
957
|
+
}
|
|
958
|
+
worker_ok = (
|
|
959
|
+
worker_state in {"unmanaged", "running"}
|
|
960
|
+
and worker_error is None
|
|
961
|
+
)
|
|
962
|
+
healthy = worker_ok and inbox.status == "ok"
|
|
963
|
+
if worker_error is not None:
|
|
964
|
+
error_code = worker_error
|
|
965
|
+
elif not worker_ok:
|
|
966
|
+
error_code = "notification_receiver.worker_not_ready"
|
|
967
|
+
elif inbox.status != "ok":
|
|
968
|
+
error_code = "notification_receiver.storage_blocked"
|
|
969
|
+
else:
|
|
970
|
+
error_code = None
|
|
971
|
+
return {
|
|
972
|
+
"schema": _RECEIVER.HEALTH_RESPONSE_SCHEMA,
|
|
973
|
+
"status": "ok" if healthy else "blocked",
|
|
974
|
+
"runtime_state": "open",
|
|
975
|
+
"worker_state": worker_state,
|
|
976
|
+
"inbox_status": inbox.status,
|
|
977
|
+
"received": inbox.received,
|
|
978
|
+
"processing_counts": dict(inbox.processing_counts),
|
|
979
|
+
"processing_due": inbox.processing_due,
|
|
980
|
+
"expired_leases": inbox.expired_leases,
|
|
981
|
+
"failed": inbox.failed,
|
|
982
|
+
"error_code": error_code,
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
def close(self, *, worker_timeout_seconds: float | int = 30) -> None:
|
|
986
|
+
self._assert_owner()
|
|
987
|
+
self.stop_worker(timeout_seconds=worker_timeout_seconds)
|
|
988
|
+
with self._lock:
|
|
989
|
+
if not self._closed:
|
|
990
|
+
self._connection.close()
|
|
991
|
+
self._closed = True
|
|
992
|
+
|
|
993
|
+
@property
|
|
994
|
+
def state(self) -> str:
|
|
995
|
+
if os.getpid() != self._owner_pid:
|
|
996
|
+
return "foreign-process"
|
|
997
|
+
with self._lock:
|
|
998
|
+
return "closed" if self._closed else "open"
|
|
999
|
+
|
|
1000
|
+
@property
|
|
1001
|
+
def worker_state(self) -> str:
|
|
1002
|
+
if os.getpid() != self._owner_pid:
|
|
1003
|
+
return "foreign-process"
|
|
1004
|
+
with self._lock:
|
|
1005
|
+
return "closed" if self._closed else self._worker_state
|
|
1006
|
+
|
|
1007
|
+
def __enter__(self) -> "DurableTerminalNotificationReceiverRuntime":
|
|
1008
|
+
self._require_open()
|
|
1009
|
+
return self
|
|
1010
|
+
|
|
1011
|
+
def __exit__(self, *_exc: Any) -> None:
|
|
1012
|
+
self.close()
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def open_durable_terminal_notification_receiver(
|
|
1016
|
+
database: str | os.PathLike[str],
|
|
1017
|
+
authenticate: Callable[[Mapping[str, Any], Mapping[str, str]], Any],
|
|
1018
|
+
*,
|
|
1019
|
+
origin: str,
|
|
1020
|
+
clock: Callable[[], float | int] = time.time,
|
|
1021
|
+
path: str = _RECEIVER.DEFAULT_PATH,
|
|
1022
|
+
require_https: bool = True,
|
|
1023
|
+
processing_max_attempts: int = 5,
|
|
1024
|
+
processing_base_delay_seconds: float | int = 5,
|
|
1025
|
+
processing_max_delay_seconds: float | int = 300,
|
|
1026
|
+
) -> DurableTerminalNotificationReceiverRuntime:
|
|
1027
|
+
"""Validate all configuration before opening one protected SQLite worker."""
|
|
1028
|
+
|
|
1029
|
+
database_path = _database_path(database)
|
|
1030
|
+
_preflight_application(
|
|
1031
|
+
authenticate,
|
|
1032
|
+
origin=origin,
|
|
1033
|
+
clock=clock,
|
|
1034
|
+
path=path,
|
|
1035
|
+
require_https=require_https,
|
|
1036
|
+
)
|
|
1037
|
+
_preflight_processing(
|
|
1038
|
+
processing_max_attempts,
|
|
1039
|
+
processing_base_delay_seconds,
|
|
1040
|
+
processing_max_delay_seconds,
|
|
1041
|
+
)
|
|
1042
|
+
database_guard = _prepare_database_file(database_path)
|
|
1043
|
+
try:
|
|
1044
|
+
connection = sqlite3.connect(
|
|
1045
|
+
database_path,
|
|
1046
|
+
timeout=5.0,
|
|
1047
|
+
isolation_level=None,
|
|
1048
|
+
check_same_thread=False,
|
|
1049
|
+
)
|
|
1050
|
+
except (OSError, sqlite3.Error) as error:
|
|
1051
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
1052
|
+
"terminal receiver database could not be opened"
|
|
1053
|
+
) from error
|
|
1054
|
+
try:
|
|
1055
|
+
connection.execute("PRAGMA busy_timeout = 5000")
|
|
1056
|
+
connection.execute("PRAGMA foreign_keys = ON")
|
|
1057
|
+
connection.execute("PRAGMA secure_delete = ON")
|
|
1058
|
+
if connection.execute("PRAGMA foreign_keys").fetchone()[0] != 1:
|
|
1059
|
+
raise sqlite3.DatabaseError("foreign keys unavailable")
|
|
1060
|
+
if connection.execute("PRAGMA secure_delete").fetchone()[0] != 1:
|
|
1061
|
+
raise sqlite3.DatabaseError("secure deletion unavailable")
|
|
1062
|
+
database_guard()
|
|
1063
|
+
inbox = _RECEIVER.DurableCMSTerminalNotificationInbox(
|
|
1064
|
+
connection,
|
|
1065
|
+
database_guard=database_guard,
|
|
1066
|
+
processing_max_attempts=processing_max_attempts,
|
|
1067
|
+
processing_base_delay_seconds=processing_base_delay_seconds,
|
|
1068
|
+
processing_max_delay_seconds=processing_max_delay_seconds,
|
|
1069
|
+
)
|
|
1070
|
+
application = _RECEIVER.CMSTerminalNotificationReceiverApplication(
|
|
1071
|
+
inbox,
|
|
1072
|
+
authenticate,
|
|
1073
|
+
origin=origin,
|
|
1074
|
+
clock=clock,
|
|
1075
|
+
path=path,
|
|
1076
|
+
require_https=require_https,
|
|
1077
|
+
)
|
|
1078
|
+
except Exception as error:
|
|
1079
|
+
connection.close()
|
|
1080
|
+
raise DurableTerminalReceiverRuntimeBlocked(
|
|
1081
|
+
"terminal receiver runtime initialization failed"
|
|
1082
|
+
) from error
|
|
1083
|
+
return DurableTerminalNotificationReceiverRuntime(
|
|
1084
|
+
connection, inbox, application, database_guard, clock,
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def open_hosted_durable_terminal_notification_receiver(
|
|
1089
|
+
database: str | os.PathLike[str],
|
|
1090
|
+
authenticate: Callable[[Mapping[str, Any], Mapping[str, str]], Any],
|
|
1091
|
+
callback: Callable[[Mapping[str, Any]], Any],
|
|
1092
|
+
*,
|
|
1093
|
+
origin: str,
|
|
1094
|
+
worker_id: str,
|
|
1095
|
+
clock: Callable[[], float | int] = time.time,
|
|
1096
|
+
path: str = _RECEIVER.DEFAULT_PATH,
|
|
1097
|
+
require_https: bool = True,
|
|
1098
|
+
processing_max_attempts: int = 5,
|
|
1099
|
+
processing_base_delay_seconds: float | int = 5,
|
|
1100
|
+
processing_max_delay_seconds: float | int = 300,
|
|
1101
|
+
lease_seconds: float | int = 600,
|
|
1102
|
+
active_delay_seconds: float | int = 0.05,
|
|
1103
|
+
idle_delay_seconds: float | int = 1,
|
|
1104
|
+
blocked_delay_seconds: float | int = 5,
|
|
1105
|
+
) -> DurableTerminalNotificationReceiverRuntime:
|
|
1106
|
+
"""Open a durable receiver and start its supervised processor."""
|
|
1107
|
+
|
|
1108
|
+
configuration = (
|
|
1109
|
+
callback,
|
|
1110
|
+
worker_id,
|
|
1111
|
+
lease_seconds,
|
|
1112
|
+
active_delay_seconds,
|
|
1113
|
+
idle_delay_seconds,
|
|
1114
|
+
blocked_delay_seconds,
|
|
1115
|
+
)
|
|
1116
|
+
DurableTerminalNotificationReceiverRuntime._worker_configuration(
|
|
1117
|
+
*configuration
|
|
1118
|
+
)
|
|
1119
|
+
runtime = open_durable_terminal_notification_receiver(
|
|
1120
|
+
database,
|
|
1121
|
+
authenticate,
|
|
1122
|
+
origin=origin,
|
|
1123
|
+
clock=clock,
|
|
1124
|
+
path=path,
|
|
1125
|
+
require_https=require_https,
|
|
1126
|
+
processing_max_attempts=processing_max_attempts,
|
|
1127
|
+
processing_base_delay_seconds=processing_base_delay_seconds,
|
|
1128
|
+
processing_max_delay_seconds=processing_max_delay_seconds,
|
|
1129
|
+
)
|
|
1130
|
+
try:
|
|
1131
|
+
runtime.start_worker(
|
|
1132
|
+
callback,
|
|
1133
|
+
worker_id,
|
|
1134
|
+
lease_seconds=lease_seconds,
|
|
1135
|
+
active_delay_seconds=active_delay_seconds,
|
|
1136
|
+
idle_delay_seconds=idle_delay_seconds,
|
|
1137
|
+
blocked_delay_seconds=blocked_delay_seconds,
|
|
1138
|
+
)
|
|
1139
|
+
except Exception:
|
|
1140
|
+
runtime.close()
|
|
1141
|
+
raise
|
|
1142
|
+
return runtime
|