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,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Emit Claude Code HTTP MCP authentication headers without exposing the token in config."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import stat
|
|
9
|
+
import sys
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Iterator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_TOKEN_FILE = Path.home() / ".config" / "blun-language-guard" / "mcp-http.token"
|
|
16
|
+
MAX_TOKEN_BYTES = 64 * 1024
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _file_identity(details: os.stat_result) -> tuple[int, int, int, int, int, int]:
|
|
20
|
+
return (
|
|
21
|
+
details.st_dev,
|
|
22
|
+
details.st_ino,
|
|
23
|
+
details.st_nlink,
|
|
24
|
+
details.st_size,
|
|
25
|
+
details.st_ctime_ns,
|
|
26
|
+
details.st_mtime_ns,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _directory_identity(details: os.stat_result) -> tuple[int, int, int, int, int]:
|
|
31
|
+
return (
|
|
32
|
+
details.st_dev,
|
|
33
|
+
details.st_ino,
|
|
34
|
+
details.st_mode,
|
|
35
|
+
details.st_uid,
|
|
36
|
+
details.st_gid,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _validate_token_directory(path: Path, details: os.stat_result) -> None:
|
|
41
|
+
if not stat.S_ISDIR(details.st_mode) or stat.S_ISLNK(details.st_mode):
|
|
42
|
+
raise RuntimeError(f"MCP access-token directory is invalid: {path}")
|
|
43
|
+
if os.name != "nt" and stat.S_IMODE(details.st_mode) & 0o022:
|
|
44
|
+
raise RuntimeError(f"MCP access-token directory is writable outside its owner: {path}")
|
|
45
|
+
if hasattr(os, "getuid") and details.st_uid != os.getuid():
|
|
46
|
+
raise RuntimeError(f"MCP access-token directory owner is invalid: {path}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _existing_token_directory_anchor(path: Path) -> Path:
|
|
50
|
+
candidate = path
|
|
51
|
+
while True:
|
|
52
|
+
try:
|
|
53
|
+
candidate.lstat()
|
|
54
|
+
return candidate
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
parent = candidate.parent
|
|
57
|
+
if parent == candidate:
|
|
58
|
+
raise RuntimeError(f"MCP access-token directory has no existing anchor: {path}")
|
|
59
|
+
candidate = parent
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _assert_token_directory_unchanged(
|
|
63
|
+
path: Path, expected: tuple[int, int, int, int, int],
|
|
64
|
+
) -> None:
|
|
65
|
+
try:
|
|
66
|
+
current = path.lstat()
|
|
67
|
+
except OSError as error:
|
|
68
|
+
raise RuntimeError(f"MCP access-token directory cannot be rechecked: {path}") from error
|
|
69
|
+
_validate_token_directory(path, current)
|
|
70
|
+
if _directory_identity(current) != expected:
|
|
71
|
+
raise RuntimeError(f"MCP access-token directory changed during read: {path}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@contextmanager
|
|
75
|
+
def _open_token_directory(path: Path) -> Iterator[int | None]:
|
|
76
|
+
if os.name == "nt":
|
|
77
|
+
yield None
|
|
78
|
+
return
|
|
79
|
+
anchor = Path.home()
|
|
80
|
+
try:
|
|
81
|
+
relative = path.parent.relative_to(anchor)
|
|
82
|
+
except ValueError:
|
|
83
|
+
anchor = _existing_token_directory_anchor(path.parent)
|
|
84
|
+
relative = path.parent.relative_to(anchor)
|
|
85
|
+
flags = (
|
|
86
|
+
os.O_RDONLY
|
|
87
|
+
| getattr(os, "O_BINARY", 0)
|
|
88
|
+
| getattr(os, "O_DIRECTORY", 0)
|
|
89
|
+
| getattr(os, "O_NOFOLLOW", 0)
|
|
90
|
+
)
|
|
91
|
+
descriptor = None
|
|
92
|
+
current_path = anchor
|
|
93
|
+
try:
|
|
94
|
+
try:
|
|
95
|
+
descriptor = os.open(anchor, flags)
|
|
96
|
+
_validate_token_directory(anchor, os.fstat(descriptor))
|
|
97
|
+
for component in relative.parts:
|
|
98
|
+
current_path = current_path / component
|
|
99
|
+
child = os.open(component, flags, dir_fd=descriptor)
|
|
100
|
+
try:
|
|
101
|
+
_validate_token_directory(current_path, os.fstat(child))
|
|
102
|
+
except Exception:
|
|
103
|
+
os.close(child)
|
|
104
|
+
raise
|
|
105
|
+
os.close(descriptor)
|
|
106
|
+
descriptor = child
|
|
107
|
+
expected = _directory_identity(os.fstat(descriptor))
|
|
108
|
+
except (FileNotFoundError, RuntimeError):
|
|
109
|
+
raise
|
|
110
|
+
except OSError as error:
|
|
111
|
+
raise RuntimeError(
|
|
112
|
+
f"MCP access-token directory cannot be opened safely: {current_path}"
|
|
113
|
+
) from error
|
|
114
|
+
try:
|
|
115
|
+
yield descriptor
|
|
116
|
+
finally:
|
|
117
|
+
_assert_token_directory_unchanged(path.parent, expected)
|
|
118
|
+
finally:
|
|
119
|
+
if descriptor is not None:
|
|
120
|
+
os.close(descriptor)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _validate_token_file(path: Path, details: os.stat_result) -> None:
|
|
124
|
+
if not stat.S_ISREG(details.st_mode) or stat.S_ISLNK(details.st_mode):
|
|
125
|
+
raise RuntimeError(f"MCP access-token path is not a regular file: {path}")
|
|
126
|
+
if details.st_nlink != 1:
|
|
127
|
+
raise RuntimeError(f"MCP access-token path has additional hard links: {path}")
|
|
128
|
+
if details.st_size < 32 or details.st_size > MAX_TOKEN_BYTES:
|
|
129
|
+
raise RuntimeError(f"MCP access token has an invalid size: {path}")
|
|
130
|
+
if os.name != "nt" and stat.S_IMODE(details.st_mode) & 0o077:
|
|
131
|
+
raise RuntimeError(f"MCP access-token permissions must be owner-only: {path}")
|
|
132
|
+
if hasattr(os, "getuid") and details.st_uid != os.getuid():
|
|
133
|
+
raise RuntimeError(f"MCP access-token owner is invalid: {path}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _token_lstat(path: Path, directory: int | None) -> os.stat_result:
|
|
137
|
+
if directory is None:
|
|
138
|
+
return path.lstat()
|
|
139
|
+
return os.stat(path.name, dir_fd=directory, follow_symlinks=False)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _open_token_file(path: Path, directory: int | None) -> int:
|
|
143
|
+
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
|
|
144
|
+
if directory is None:
|
|
145
|
+
return os.open(path, flags)
|
|
146
|
+
return os.open(path.name, flags, dir_fd=directory)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _token_fstat(descriptor: int) -> os.stat_result:
|
|
150
|
+
return os.fstat(descriptor)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def load_token(path: Path) -> str:
|
|
154
|
+
with _open_token_directory(path) as directory:
|
|
155
|
+
before = _token_lstat(path, directory)
|
|
156
|
+
_validate_token_file(path, before)
|
|
157
|
+
descriptor = _open_token_file(path, directory)
|
|
158
|
+
try:
|
|
159
|
+
opened = _token_fstat(descriptor)
|
|
160
|
+
_validate_token_file(path, opened)
|
|
161
|
+
if _file_identity(opened) != _file_identity(before):
|
|
162
|
+
raise RuntimeError(f"MCP access token changed while opening: {path}")
|
|
163
|
+
with os.fdopen(descriptor, "rb", closefd=False) as handle:
|
|
164
|
+
raw = handle.read(MAX_TOKEN_BYTES + 1)
|
|
165
|
+
after_read = _token_fstat(descriptor)
|
|
166
|
+
after_path = _token_lstat(path, directory)
|
|
167
|
+
finally:
|
|
168
|
+
os.close(descriptor)
|
|
169
|
+
if len(raw) > MAX_TOKEN_BYTES:
|
|
170
|
+
raise RuntimeError(f"MCP access token has an invalid size: {path}")
|
|
171
|
+
if (
|
|
172
|
+
_file_identity(after_read) != _file_identity(opened)
|
|
173
|
+
or _file_identity(after_path) != _file_identity(opened)
|
|
174
|
+
):
|
|
175
|
+
raise RuntimeError(f"MCP access token changed while reading: {path}")
|
|
176
|
+
try:
|
|
177
|
+
token = raw.decode("utf-8-sig").strip()
|
|
178
|
+
except UnicodeDecodeError as error:
|
|
179
|
+
raise RuntimeError(f"MCP access token is not valid UTF-8: {path}") from error
|
|
180
|
+
if len(token) < 32:
|
|
181
|
+
raise RuntimeError(f"MCP access token is invalid: {path}")
|
|
182
|
+
return token
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def main() -> int:
|
|
186
|
+
configured = os.environ.get("BLUN_LANGUAGE_GUARD_MCP_TOKEN_FILE", "").strip()
|
|
187
|
+
path = Path(configured).expanduser() if configured else DEFAULT_TOKEN_FILE
|
|
188
|
+
try:
|
|
189
|
+
token = load_token(path)
|
|
190
|
+
except (OSError, RuntimeError) as error:
|
|
191
|
+
print(f"BLOCK: {error}", file=sys.stderr)
|
|
192
|
+
return 1
|
|
193
|
+
print(json.dumps({"Authorization": f"Bearer {token}"}, separators=(",", ":")))
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Authenticated, stateless Streamable HTTP transport for BLUN Language Guard MCP."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hmac
|
|
8
|
+
import importlib.util
|
|
9
|
+
import ipaddress
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import signal
|
|
13
|
+
import stat
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Iterator
|
|
20
|
+
from urllib.parse import urlsplit
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
24
|
+
GUARD_PATH = ROOT / "translate-native" / "scripts" / "blun_language_guard.py"
|
|
25
|
+
MAX_REQUEST_BYTES = 8 * 1024 * 1024
|
|
26
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
27
|
+
DEFAULT_PORT = 47632
|
|
28
|
+
DEFAULT_MCP_PATH = "/mcp"
|
|
29
|
+
SUPPORTED_PROTOCOL_VERSIONS = {"2025-03-26", "2025-06-18"}
|
|
30
|
+
MAX_ACCESS_TOKEN_BYTES = 64 * 1024
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load_guard():
|
|
34
|
+
spec = importlib.util.spec_from_file_location("blun_http_language_guard", GUARD_PATH)
|
|
35
|
+
if not spec or not spec.loader:
|
|
36
|
+
raise RuntimeError("Cannot load the BLUN Language Guard MCP implementation")
|
|
37
|
+
module = importlib.util.module_from_spec(spec)
|
|
38
|
+
sys.modules[spec.name] = module
|
|
39
|
+
spec.loader.exec_module(module)
|
|
40
|
+
return module
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
GUARD = _load_guard()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_loopback(host: str) -> bool:
|
|
47
|
+
if host.casefold() == "localhost":
|
|
48
|
+
return True
|
|
49
|
+
try:
|
|
50
|
+
return ipaddress.ip_address(host).is_loopback
|
|
51
|
+
except ValueError:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _token_file_identity(details: os.stat_result) -> tuple[int, int, int, int, int, int]:
|
|
56
|
+
return (
|
|
57
|
+
details.st_dev,
|
|
58
|
+
details.st_ino,
|
|
59
|
+
details.st_nlink,
|
|
60
|
+
details.st_size,
|
|
61
|
+
details.st_ctime_ns,
|
|
62
|
+
details.st_mtime_ns,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _token_directory_identity(details: os.stat_result) -> tuple[int, int, int, int, int]:
|
|
67
|
+
return (
|
|
68
|
+
details.st_dev,
|
|
69
|
+
details.st_ino,
|
|
70
|
+
details.st_mode,
|
|
71
|
+
details.st_uid,
|
|
72
|
+
details.st_gid,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _validate_access_token_directory(path: Path, details: os.stat_result) -> None:
|
|
77
|
+
if not stat.S_ISDIR(details.st_mode) or stat.S_ISLNK(details.st_mode):
|
|
78
|
+
raise RuntimeError(f"MCP access-token directory is invalid: {path}")
|
|
79
|
+
if os.name != "nt" and stat.S_IMODE(details.st_mode) & 0o022:
|
|
80
|
+
raise RuntimeError(f"MCP access-token directory is writable outside its owner: {path}")
|
|
81
|
+
if hasattr(os, "getuid") and details.st_uid != os.getuid():
|
|
82
|
+
raise RuntimeError(f"MCP access-token directory owner is invalid: {path}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _existing_access_token_directory_anchor(path: Path) -> Path:
|
|
86
|
+
candidate = path
|
|
87
|
+
while True:
|
|
88
|
+
try:
|
|
89
|
+
candidate.lstat()
|
|
90
|
+
return candidate
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
parent = candidate.parent
|
|
93
|
+
if parent == candidate:
|
|
94
|
+
raise RuntimeError(f"MCP access-token directory has no existing anchor: {path}")
|
|
95
|
+
candidate = parent
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _assert_access_token_directory_unchanged(
|
|
99
|
+
path: Path, expected: tuple[int, int, int, int, int],
|
|
100
|
+
) -> None:
|
|
101
|
+
try:
|
|
102
|
+
current = path.lstat()
|
|
103
|
+
except OSError as error:
|
|
104
|
+
raise RuntimeError(f"MCP access-token directory cannot be rechecked: {path}") from error
|
|
105
|
+
_validate_access_token_directory(path, current)
|
|
106
|
+
if _token_directory_identity(current) != expected:
|
|
107
|
+
raise RuntimeError(f"MCP access-token directory changed during read: {path}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@contextmanager
|
|
111
|
+
def _open_access_token_directory(path: Path) -> Iterator[int | None]:
|
|
112
|
+
if os.name == "nt":
|
|
113
|
+
yield None
|
|
114
|
+
return
|
|
115
|
+
anchor = Path.home()
|
|
116
|
+
try:
|
|
117
|
+
relative = path.parent.relative_to(anchor)
|
|
118
|
+
except ValueError:
|
|
119
|
+
anchor = _existing_access_token_directory_anchor(path.parent)
|
|
120
|
+
relative = path.parent.relative_to(anchor)
|
|
121
|
+
flags = (
|
|
122
|
+
os.O_RDONLY
|
|
123
|
+
| getattr(os, "O_BINARY", 0)
|
|
124
|
+
| getattr(os, "O_DIRECTORY", 0)
|
|
125
|
+
| getattr(os, "O_NOFOLLOW", 0)
|
|
126
|
+
)
|
|
127
|
+
descriptor = None
|
|
128
|
+
current_path = anchor
|
|
129
|
+
try:
|
|
130
|
+
try:
|
|
131
|
+
descriptor = os.open(anchor, flags)
|
|
132
|
+
_validate_access_token_directory(anchor, os.fstat(descriptor))
|
|
133
|
+
for component in relative.parts:
|
|
134
|
+
current_path = current_path / component
|
|
135
|
+
child = os.open(component, flags, dir_fd=descriptor)
|
|
136
|
+
try:
|
|
137
|
+
_validate_access_token_directory(current_path, os.fstat(child))
|
|
138
|
+
except Exception:
|
|
139
|
+
os.close(child)
|
|
140
|
+
raise
|
|
141
|
+
os.close(descriptor)
|
|
142
|
+
descriptor = child
|
|
143
|
+
expected = _token_directory_identity(os.fstat(descriptor))
|
|
144
|
+
except (FileNotFoundError, RuntimeError):
|
|
145
|
+
raise
|
|
146
|
+
except OSError as error:
|
|
147
|
+
raise RuntimeError(
|
|
148
|
+
f"MCP access-token directory cannot be opened safely: {current_path}"
|
|
149
|
+
) from error
|
|
150
|
+
try:
|
|
151
|
+
yield descriptor
|
|
152
|
+
finally:
|
|
153
|
+
_assert_access_token_directory_unchanged(path.parent, expected)
|
|
154
|
+
finally:
|
|
155
|
+
if descriptor is not None:
|
|
156
|
+
os.close(descriptor)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _validate_access_token_file(path: Path, details: os.stat_result) -> None:
|
|
160
|
+
if not stat.S_ISREG(details.st_mode) or stat.S_ISLNK(details.st_mode):
|
|
161
|
+
raise RuntimeError(f"MCP access-token path is not a regular file: {path}")
|
|
162
|
+
if details.st_nlink != 1:
|
|
163
|
+
raise RuntimeError(f"MCP access-token path has additional hard links: {path}")
|
|
164
|
+
if details.st_size < 32 or details.st_size > MAX_ACCESS_TOKEN_BYTES:
|
|
165
|
+
raise RuntimeError(f"MCP access token has an invalid size: {path}")
|
|
166
|
+
if os.name != "nt" and stat.S_IMODE(details.st_mode) & 0o077:
|
|
167
|
+
raise RuntimeError(f"MCP access-token permissions must be owner-only: {path}")
|
|
168
|
+
if hasattr(os, "getuid") and details.st_uid != os.getuid():
|
|
169
|
+
raise RuntimeError(f"MCP access-token owner is invalid: {path}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _access_token_lstat(path: Path, directory: int | None) -> os.stat_result:
|
|
173
|
+
if directory is None:
|
|
174
|
+
return path.lstat()
|
|
175
|
+
return os.stat(path.name, dir_fd=directory, follow_symlinks=False)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _open_access_token_file(path: Path, directory: int | None) -> int:
|
|
179
|
+
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
|
|
180
|
+
if directory is None:
|
|
181
|
+
return os.open(path, flags)
|
|
182
|
+
return os.open(path.name, flags, dir_fd=directory)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _access_token_fstat(descriptor: int) -> os.stat_result:
|
|
186
|
+
return os.fstat(descriptor)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def load_access_token(path: Path) -> str:
|
|
190
|
+
with _open_access_token_directory(path) as directory:
|
|
191
|
+
before = _access_token_lstat(path, directory)
|
|
192
|
+
_validate_access_token_file(path, before)
|
|
193
|
+
descriptor = _open_access_token_file(path, directory)
|
|
194
|
+
try:
|
|
195
|
+
opened = _access_token_fstat(descriptor)
|
|
196
|
+
_validate_access_token_file(path, opened)
|
|
197
|
+
if _token_file_identity(opened) != _token_file_identity(before):
|
|
198
|
+
raise RuntimeError(f"MCP access token changed while opening: {path}")
|
|
199
|
+
with os.fdopen(descriptor, "rb", closefd=False) as handle:
|
|
200
|
+
raw = handle.read(MAX_ACCESS_TOKEN_BYTES + 1)
|
|
201
|
+
after_read = _access_token_fstat(descriptor)
|
|
202
|
+
after_path = _access_token_lstat(path, directory)
|
|
203
|
+
finally:
|
|
204
|
+
os.close(descriptor)
|
|
205
|
+
if len(raw) > MAX_ACCESS_TOKEN_BYTES:
|
|
206
|
+
raise RuntimeError(f"MCP access token has an invalid size: {path}")
|
|
207
|
+
if (
|
|
208
|
+
_token_file_identity(after_read) != _token_file_identity(opened)
|
|
209
|
+
or _token_file_identity(after_path) != _token_file_identity(opened)
|
|
210
|
+
):
|
|
211
|
+
raise RuntimeError(f"MCP access token changed while reading: {path}")
|
|
212
|
+
try:
|
|
213
|
+
token = raw.decode("utf-8-sig").strip()
|
|
214
|
+
except UnicodeDecodeError as error:
|
|
215
|
+
raise RuntimeError(f"MCP access token is not valid UTF-8: {path}") from error
|
|
216
|
+
if len(token) < 32:
|
|
217
|
+
raise RuntimeError(f"MCP access token is invalid: {path}")
|
|
218
|
+
return token
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class MCPHTTPServer(ThreadingHTTPServer):
|
|
222
|
+
allow_reuse_address = True
|
|
223
|
+
daemon_threads = True
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class MCPRequestHandler(BaseHTTPRequestHandler):
|
|
227
|
+
server_version = "BLUNLanguageGuardMCP"
|
|
228
|
+
sys_version = ""
|
|
229
|
+
|
|
230
|
+
def log_message(self, _format: str, *_args: Any) -> None:
|
|
231
|
+
# Request bodies and authorization headers must never reach logs.
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def mcp_server(self) -> MCPHTTPServer:
|
|
236
|
+
return self.server # type: ignore[return-value]
|
|
237
|
+
|
|
238
|
+
def _send_empty(self, status: int, *, allow: str | None = None) -> None:
|
|
239
|
+
self.send_response(status)
|
|
240
|
+
if allow:
|
|
241
|
+
self.send_header("Allow", allow)
|
|
242
|
+
self.send_header("Cache-Control", "no-store")
|
|
243
|
+
self.send_header("Content-Length", "0")
|
|
244
|
+
self.end_headers()
|
|
245
|
+
|
|
246
|
+
def _send_json(
|
|
247
|
+
self,
|
|
248
|
+
status: int,
|
|
249
|
+
payload: dict[str, Any],
|
|
250
|
+
*,
|
|
251
|
+
authenticate: bool = False,
|
|
252
|
+
) -> None:
|
|
253
|
+
encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
254
|
+
self.send_response(status)
|
|
255
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
256
|
+
self.send_header("Cache-Control", "no-store")
|
|
257
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
258
|
+
if authenticate:
|
|
259
|
+
self.send_header("WWW-Authenticate", 'Bearer realm="blun-language-guard"')
|
|
260
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
261
|
+
self.end_headers()
|
|
262
|
+
self.wfile.write(encoded)
|
|
263
|
+
|
|
264
|
+
def _jsonrpc_error(self, status: int, code: int, message: str, *, request_id: Any = None) -> None:
|
|
265
|
+
self._send_json(status, {
|
|
266
|
+
"jsonrpc": "2.0",
|
|
267
|
+
"id": request_id,
|
|
268
|
+
"error": {"code": code, "message": message},
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
def _origin_allowed(self) -> bool:
|
|
272
|
+
origin = self.headers.get("Origin")
|
|
273
|
+
if not origin:
|
|
274
|
+
return True
|
|
275
|
+
try:
|
|
276
|
+
parsed = urlsplit(origin)
|
|
277
|
+
except ValueError:
|
|
278
|
+
return False
|
|
279
|
+
return parsed.scheme in {"http", "https"} and bool(parsed.hostname) and _is_loopback(parsed.hostname or "")
|
|
280
|
+
|
|
281
|
+
def _authorized(self) -> bool:
|
|
282
|
+
supplied = self.headers.get("Authorization", "")
|
|
283
|
+
expected = f"Bearer {self.mcp_server.access_token}" # type: ignore[attr-defined]
|
|
284
|
+
return hmac.compare_digest(supplied, expected)
|
|
285
|
+
|
|
286
|
+
def _authorize_request(self) -> bool:
|
|
287
|
+
if not self._origin_allowed():
|
|
288
|
+
self._jsonrpc_error(403, -32001, "Forbidden origin")
|
|
289
|
+
return False
|
|
290
|
+
if not self._authorized():
|
|
291
|
+
payload = {
|
|
292
|
+
"jsonrpc": "2.0",
|
|
293
|
+
"id": None,
|
|
294
|
+
"error": {"code": -32001, "message": "Unauthorized"},
|
|
295
|
+
}
|
|
296
|
+
self._send_json(401, payload, authenticate=True)
|
|
297
|
+
return False
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
def do_GET(self) -> None:
|
|
301
|
+
if self.path == "/healthz":
|
|
302
|
+
if not self._authorize_request():
|
|
303
|
+
return
|
|
304
|
+
try:
|
|
305
|
+
health = GUARD.SERVICE_CLIENT.call_guard_service(
|
|
306
|
+
GUARD.SERVICE_ENDPOINT,
|
|
307
|
+
{"operation": "health"},
|
|
308
|
+
auth_token=GUARD._service_token(),
|
|
309
|
+
timeout=3.0,
|
|
310
|
+
)
|
|
311
|
+
healthy = health.get("status") == "ok" and health.get("isolated_key") is True
|
|
312
|
+
except (OSError, RuntimeError, ValueError, GUARD.SERVICE_CLIENT.GuardServiceError):
|
|
313
|
+
healthy = False
|
|
314
|
+
health = {"status": "BLOCK", "service": "blun-language-guard"}
|
|
315
|
+
self._send_json(200 if healthy else 503, health)
|
|
316
|
+
return
|
|
317
|
+
if self.path == self.mcp_server.mcp_path: # type: ignore[attr-defined]
|
|
318
|
+
self._send_empty(405, allow="POST")
|
|
319
|
+
return
|
|
320
|
+
self._send_empty(404)
|
|
321
|
+
|
|
322
|
+
def do_DELETE(self) -> None:
|
|
323
|
+
if self.path == self.mcp_server.mcp_path: # type: ignore[attr-defined]
|
|
324
|
+
self._send_empty(405, allow="POST")
|
|
325
|
+
return
|
|
326
|
+
self._send_empty(404)
|
|
327
|
+
|
|
328
|
+
def do_POST(self) -> None:
|
|
329
|
+
if self.path != self.mcp_server.mcp_path: # type: ignore[attr-defined]
|
|
330
|
+
self._send_empty(404)
|
|
331
|
+
return
|
|
332
|
+
if not self._authorize_request():
|
|
333
|
+
return
|
|
334
|
+
media_type = self.headers.get("Content-Type", "").partition(";")[0].strip().casefold()
|
|
335
|
+
if media_type != "application/json":
|
|
336
|
+
self._jsonrpc_error(415, -32600, "Content-Type must be application/json")
|
|
337
|
+
return
|
|
338
|
+
accepted = {item.partition(";")[0].strip().casefold() for item in self.headers.get("Accept", "").split(",")}
|
|
339
|
+
if not ({"application/json", "text/event-stream"} <= accepted or "*/*" in accepted):
|
|
340
|
+
self._jsonrpc_error(406, -32600, "Accept must include application/json and text/event-stream")
|
|
341
|
+
return
|
|
342
|
+
try:
|
|
343
|
+
length = int(self.headers.get("Content-Length", ""))
|
|
344
|
+
except ValueError:
|
|
345
|
+
self._jsonrpc_error(411, -32600, "Content-Length is required")
|
|
346
|
+
return
|
|
347
|
+
if length < 0 or length > MAX_REQUEST_BYTES:
|
|
348
|
+
self._jsonrpc_error(413, -32600, "MCP request is too large")
|
|
349
|
+
return
|
|
350
|
+
try:
|
|
351
|
+
raw = self.rfile.read(length)
|
|
352
|
+
message = json.loads(raw.decode("utf-8-sig"))
|
|
353
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
354
|
+
self._jsonrpc_error(400, -32700, "Invalid UTF-8 JSON")
|
|
355
|
+
return
|
|
356
|
+
if not isinstance(message, dict) or message.get("jsonrpc") != "2.0":
|
|
357
|
+
self._jsonrpc_error(400, -32600, "MCP message must be one JSON-RPC 2.0 object")
|
|
358
|
+
return
|
|
359
|
+
method = message.get("method")
|
|
360
|
+
if method != "initialize":
|
|
361
|
+
protocol = self.headers.get("MCP-Protocol-Version", "2025-03-26")
|
|
362
|
+
if protocol not in SUPPORTED_PROTOCOL_VERSIONS:
|
|
363
|
+
self._jsonrpc_error(400, -32600, "Unsupported MCP protocol version", request_id=message.get("id"))
|
|
364
|
+
return
|
|
365
|
+
try:
|
|
366
|
+
if isinstance(method, str):
|
|
367
|
+
response = GUARD.handle_message(message)
|
|
368
|
+
if message.get("id") is None:
|
|
369
|
+
self._send_empty(202)
|
|
370
|
+
elif response is None:
|
|
371
|
+
self._jsonrpc_error(500, -32603, "MCP request produced no response", request_id=message.get("id"))
|
|
372
|
+
else:
|
|
373
|
+
self._send_json(200, response)
|
|
374
|
+
return
|
|
375
|
+
if "result" in message or "error" in message:
|
|
376
|
+
self._send_empty(202)
|
|
377
|
+
return
|
|
378
|
+
self._jsonrpc_error(400, -32600, "Invalid JSON-RPC message", request_id=message.get("id"))
|
|
379
|
+
except Exception:
|
|
380
|
+
# One malformed or failing request must never terminate the persistent service.
|
|
381
|
+
self._jsonrpc_error(500, -32603, "Internal MCP failure", request_id=message.get("id"))
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def build_server(host: str, port: int, access_token: str, mcp_path: str = DEFAULT_MCP_PATH) -> MCPHTTPServer:
|
|
385
|
+
if not _is_loopback(host):
|
|
386
|
+
raise ValueError("the local MCP gateway may bind only to a loopback address")
|
|
387
|
+
if not access_token or len(access_token) < 32:
|
|
388
|
+
raise ValueError("MCP access token must contain at least 32 characters")
|
|
389
|
+
path = "/" + mcp_path.strip("/")
|
|
390
|
+
server = MCPHTTPServer((host, port), MCPRequestHandler)
|
|
391
|
+
server.access_token = access_token # type: ignore[attr-defined]
|
|
392
|
+
server.mcp_path = path # type: ignore[attr-defined]
|
|
393
|
+
return server
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def main() -> int:
|
|
397
|
+
runtime = Path.home() / ".config" / "blun-language-guard"
|
|
398
|
+
parser = argparse.ArgumentParser(description="Run the persistent BLUN Streamable HTTP MCP gateway")
|
|
399
|
+
parser.add_argument("--host", default=DEFAULT_HOST)
|
|
400
|
+
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
|
|
401
|
+
parser.add_argument("--path", default=DEFAULT_MCP_PATH)
|
|
402
|
+
parser.add_argument("--access-token-file", type=Path, default=runtime / "mcp-http.token")
|
|
403
|
+
parser.add_argument("--service-endpoint", required=True)
|
|
404
|
+
parser.add_argument("--service-token-file", type=Path, required=True)
|
|
405
|
+
args = parser.parse_args()
|
|
406
|
+
try:
|
|
407
|
+
access_token = load_access_token(args.access_token_file)
|
|
408
|
+
GUARD.SERVICE_ENDPOINT = args.service_endpoint
|
|
409
|
+
GUARD.SERVICE_TOKEN_FILE = str(args.service_token_file)
|
|
410
|
+
GUARD._service_token()
|
|
411
|
+
server = build_server(args.host, args.port, access_token, args.path)
|
|
412
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
413
|
+
print(f"BLOCK: {error}", file=sys.stderr)
|
|
414
|
+
return 1
|
|
415
|
+
|
|
416
|
+
def stop(_signum=None, _frame=None) -> None:
|
|
417
|
+
threading.Thread(target=server.shutdown, daemon=True).start()
|
|
418
|
+
|
|
419
|
+
for signum in (signal.SIGINT, signal.SIGTERM):
|
|
420
|
+
signal.signal(signum, stop)
|
|
421
|
+
try:
|
|
422
|
+
server.serve_forever(poll_interval=0.25)
|
|
423
|
+
finally:
|
|
424
|
+
server.server_close()
|
|
425
|
+
return 0
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
if __name__ == "__main__":
|
|
429
|
+
raise SystemExit(main())
|