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,1536 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Claude lifecycle boundary: exchange independently verified MCP releases for
|
|
5
|
+
// service-owned one-time delivery grants, then consume one for the exact output.
|
|
6
|
+
|
|
7
|
+
const crypto = require("crypto");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const net = require("net");
|
|
10
|
+
const os = require("os");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
|
|
13
|
+
const MAX_INPUT_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
const MAX_RECORD_BYTES = 64 * 1024;
|
|
15
|
+
const MAX_EPOCH_BYTES = 128;
|
|
16
|
+
const MAX_SERVICE_TOKEN_BYTES = 64 * 1024;
|
|
17
|
+
const MAX_POLICY_BYTES = 64 * 1024;
|
|
18
|
+
const MAX_RECORD_AGE_MS = 10 * 60 * 1000;
|
|
19
|
+
const DEFAULT_RUNTIME = path.join(os.homedir(), ".config", "blun-language-guard");
|
|
20
|
+
const EXACT_LANGUAGE = /^(?:[A-Za-z]{2,8}|x)(?:-[A-Za-z0-9]{1,8})*$/;
|
|
21
|
+
const HTML_C1_NUMERIC_REFERENCE_REPLACEMENTS = new Map([
|
|
22
|
+
[0x80, 0x20AC], [0x82, 0x201A], [0x83, 0x0192], [0x84, 0x201E],
|
|
23
|
+
[0x85, 0x2026], [0x86, 0x2020], [0x87, 0x2021], [0x88, 0x02C6],
|
|
24
|
+
[0x89, 0x2030], [0x8A, 0x0160], [0x8B, 0x2039], [0x8C, 0x0152],
|
|
25
|
+
[0x8E, 0x017D], [0x91, 0x2018], [0x92, 0x2019], [0x93, 0x201C],
|
|
26
|
+
[0x94, 0x201D], [0x95, 0x2022], [0x96, 0x2013], [0x97, 0x2014],
|
|
27
|
+
[0x98, 0x02DC], [0x99, 0x2122], [0x9A, 0x0161], [0x9B, 0x203A],
|
|
28
|
+
[0x9C, 0x0153], [0x9E, 0x017E], [0x9F, 0x0178]
|
|
29
|
+
]);
|
|
30
|
+
const NON_LANGUAGE_NAMED_REFERENCES = new Set(require("./non_language_html_entities"));
|
|
31
|
+
const LEGACY_NON_LANGUAGE_NAMED_REFERENCES = [
|
|
32
|
+
"brvbar", "divide", "frac12", "frac14", "frac34", "iquest", "middot", "plusmn",
|
|
33
|
+
"pound", "acute", "curren", "iexcl", "laquo", "nbsp", "para", "raquo", "sect",
|
|
34
|
+
"times", "cedil", "cent", "copy", "macr", "quot", "shy", "sup1", "sup2",
|
|
35
|
+
"sup3", "AMP", "COPY", "QUOT", "REG", "amp", "deg", "GT", "gt", "LT", "lt",
|
|
36
|
+
"not", "reg", "uml", "yen"
|
|
37
|
+
].sort((left, right) => right.length - left.length);
|
|
38
|
+
const ENCLOSED_LATIN_LETTER_RANGES = [
|
|
39
|
+
[0x249C, 0x24E9],
|
|
40
|
+
[0x1F130, 0x1F149],
|
|
41
|
+
[0x1F150, 0x1F169],
|
|
42
|
+
[0x1F170, 0x1F189]
|
|
43
|
+
];
|
|
44
|
+
const MORSE_LETTER_CODES = new Set([
|
|
45
|
+
".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---",
|
|
46
|
+
"-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-",
|
|
47
|
+
"...-", ".--", "-..-", "-.--", "--.."
|
|
48
|
+
]);
|
|
49
|
+
const ROMAN_NUMERAL_VALUES = new Map([
|
|
50
|
+
["I", 1], ["V", 5], ["X", 10], ["L", 50], ["C", 100], ["D", 500], ["M", 1000]
|
|
51
|
+
]);
|
|
52
|
+
const ROMAN_SUBTRACTIVE_PAIRS = new Set(["IV", "IX", "XL", "XC", "CD", "CM"]);
|
|
53
|
+
let currentHookInput = null;
|
|
54
|
+
|
|
55
|
+
function canonicalText(value) {
|
|
56
|
+
return String(value || "")
|
|
57
|
+
.replace(/^\uFEFF/, "")
|
|
58
|
+
.replace(/\r\n?/g, "\n")
|
|
59
|
+
.normalize("NFC");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function textHash(value) {
|
|
63
|
+
return crypto.createHash("sha256").update(canonicalText(value), "utf8").digest("hex");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readBoundedUtf8Descriptor(descriptor, maximumBytes, label) {
|
|
67
|
+
const buffer = Buffer.alloc(maximumBytes + 1);
|
|
68
|
+
let size = 0;
|
|
69
|
+
while (size < buffer.length) {
|
|
70
|
+
const count = fs.readSync(descriptor, buffer, size, buffer.length - size, null);
|
|
71
|
+
if (count === 0) break;
|
|
72
|
+
size += count;
|
|
73
|
+
}
|
|
74
|
+
if (size > maximumBytes) throw new Error(`${label} has an invalid size`);
|
|
75
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, size));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isRomanNumber(value) {
|
|
79
|
+
let previousToken = Number.POSITIVE_INFINITY;
|
|
80
|
+
for (let index = 0; index < value.length;) {
|
|
81
|
+
const current = ROMAN_NUMERAL_VALUES.get(value[index]);
|
|
82
|
+
const next = ROMAN_NUMERAL_VALUES.get(value[index + 1]);
|
|
83
|
+
let token = current;
|
|
84
|
+
if (next > current) {
|
|
85
|
+
if (!ROMAN_SUBTRACTIVE_PAIRS.has(value.slice(index, index + 2))) return false;
|
|
86
|
+
token = next - current;
|
|
87
|
+
index += 2;
|
|
88
|
+
} else {
|
|
89
|
+
index += 1;
|
|
90
|
+
}
|
|
91
|
+
if (token > previousToken) return false;
|
|
92
|
+
previousToken = token;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function containsLanguageCharacters(value) {
|
|
98
|
+
const text = String(value || "");
|
|
99
|
+
if (/\p{L}/u.test(text)) return true;
|
|
100
|
+
const normalizedMorse = text
|
|
101
|
+
.replace(/[\u00B7\u2022]/gu, ".")
|
|
102
|
+
.replace(/[\u2010-\u2015\u2212]/gu, "-");
|
|
103
|
+
if (/(?:^|[^.\-])\.\.\.---\.\.\.(?=$|[^.\-])/.test(normalizedMorse)) return true;
|
|
104
|
+
const morseRun = /(?:^|[^.\-])((?:[.\-]{1,4}[\t\n\r /]+){2,}[.\-]{1,4})(?=$|[^.\-])/g;
|
|
105
|
+
for (const match of normalizedMorse.matchAll(morseRun)) {
|
|
106
|
+
const tokens = match[1].trim().split(/[\s/]+/u).filter(Boolean);
|
|
107
|
+
if (tokens.length < 3 || !tokens.every((token) => MORSE_LETTER_CODES.has(token))) continue;
|
|
108
|
+
if (tokens.some((token) => token.includes(".")) && tokens.some((token) => token.includes("-"))) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const match of text.matchAll(/[\u2160-\u217F]+/gu)) {
|
|
113
|
+
const romanText = match[0].normalize("NFKC").toUpperCase();
|
|
114
|
+
if ([...match[0]].length >= 2 && romanText.length >= 3 && !isRomanNumber(romanText)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
let enclosedEmojiLetters = 0;
|
|
119
|
+
let brailleCells = 0;
|
|
120
|
+
let signWritingSymbols = 0;
|
|
121
|
+
let regionalIndicatorRun = 0;
|
|
122
|
+
let unpairedRegionalIndicators = 0;
|
|
123
|
+
const finishRegionalIndicatorRun = () => {
|
|
124
|
+
unpairedRegionalIndicators += regionalIndicatorRun % 2;
|
|
125
|
+
regionalIndicatorRun = 0;
|
|
126
|
+
};
|
|
127
|
+
for (const character of text) {
|
|
128
|
+
const codePoint = character.codePointAt(0);
|
|
129
|
+
if (codePoint >= 0x2801 && codePoint <= 0x28FF) {
|
|
130
|
+
brailleCells += 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (codePoint >= 0x1D800 && codePoint <= 0x1DA86 && codePoint !== 0x1DA84) {
|
|
134
|
+
signWritingSymbols += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF) {
|
|
138
|
+
regionalIndicatorRun += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
finishRegionalIndicatorRun();
|
|
142
|
+
const enclosedLatinLetter = ENCLOSED_LATIN_LETTER_RANGES.some(
|
|
143
|
+
([start, end]) => codePoint >= start && codePoint <= end
|
|
144
|
+
);
|
|
145
|
+
if (!enclosedLatinLetter) continue;
|
|
146
|
+
if (!/\p{Emoji}/u.test(character)) return true;
|
|
147
|
+
enclosedEmojiLetters += 1;
|
|
148
|
+
}
|
|
149
|
+
finishRegionalIndicatorRun();
|
|
150
|
+
if (signWritingSymbols >= 2) return true;
|
|
151
|
+
if (brailleCells >= 2) return true;
|
|
152
|
+
if (unpairedRegionalIndicators >= 2) return true;
|
|
153
|
+
if (enclosedEmojiLetters >= 2) return true;
|
|
154
|
+
const withoutEmojiFormatting = text.replace(/[\u20E3\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/gu, "");
|
|
155
|
+
return /\p{M}/u.test(withoutEmojiFormatting);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hasNaturalLanguage(value) {
|
|
159
|
+
const text = String(value || "").replace(
|
|
160
|
+
/&#(?:0*(\d{1,7})(?!\d)|x0*([0-9a-f]{1,6})(?![0-9a-f]));?|&([A-Za-z][A-Za-z0-9]{1,31})(;?)/gi,
|
|
161
|
+
(entity, decimal, hexadecimal, name, semicolon) => {
|
|
162
|
+
if (decimal || hexadecimal) {
|
|
163
|
+
const parsedCodePoint = Number.parseInt(decimal || hexadecimal, decimal ? 10 : 16);
|
|
164
|
+
if (Number.isSafeInteger(parsedCodePoint) && parsedCodePoint >= 0 && parsedCodePoint <= 0x10FFFF) {
|
|
165
|
+
const renderedCodePoint = HTML_C1_NUMERIC_REFERENCE_REPLACEMENTS.get(parsedCodePoint)
|
|
166
|
+
?? parsedCodePoint;
|
|
167
|
+
return String.fromCodePoint(renderedCodePoint);
|
|
168
|
+
}
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
if (semicolon && NON_LANGUAGE_NAMED_REFERENCES.has(name)) return "";
|
|
172
|
+
const legacyReference = LEGACY_NON_LANGUAGE_NAMED_REFERENCES.find(
|
|
173
|
+
(reference) => name.startsWith(reference)
|
|
174
|
+
);
|
|
175
|
+
if (!legacyReference) return entity;
|
|
176
|
+
return `${name.slice(legacyReference.length)}${semicolon}`;
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
return containsLanguageCharacters(text);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function validatePolicyStats(stats) {
|
|
183
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("delivery policy must be a regular file");
|
|
184
|
+
if (stats.nlink !== 1) throw new Error("delivery policy must not have additional hard links");
|
|
185
|
+
if (stats.size < 2 || stats.size > MAX_POLICY_BYTES) throw new Error("delivery policy has an invalid size");
|
|
186
|
+
if (process.platform !== "win32" && (stats.mode & 0o077) !== 0) {
|
|
187
|
+
throw new Error("delivery policy permissions are too broad");
|
|
188
|
+
}
|
|
189
|
+
if (typeof process.getuid === "function" && stats.uid !== process.getuid()) {
|
|
190
|
+
throw new Error("delivery policy has the wrong owner");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function protectedDirectoryIdentity(stats) {
|
|
195
|
+
return {
|
|
196
|
+
dev: stats.dev,
|
|
197
|
+
ino: stats.ino,
|
|
198
|
+
mode: stats.mode,
|
|
199
|
+
uid: stats.uid,
|
|
200
|
+
gid: stats.gid
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function sameProtectedDirectoryIdentity(stats, expected) {
|
|
205
|
+
return stats.dev === expected.dev
|
|
206
|
+
&& stats.ino === expected.ino
|
|
207
|
+
&& stats.mode === expected.mode
|
|
208
|
+
&& stats.uid === expected.uid
|
|
209
|
+
&& stats.gid === expected.gid;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function validateProtectedDirectoryStats(stats, directory, label) {
|
|
213
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
214
|
+
throw new Error(`${label} directory must be a directory: ${directory}`);
|
|
215
|
+
}
|
|
216
|
+
if (process.platform !== "win32" && (stats.mode & 0o022) !== 0) {
|
|
217
|
+
throw new Error(`${label} directory is writable outside its owner: ${directory}`);
|
|
218
|
+
}
|
|
219
|
+
if (typeof process.getuid === "function" && stats.uid !== process.getuid()) {
|
|
220
|
+
throw new Error(`${label} directory has the wrong owner: ${directory}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function existingProtectedDirectoryAnchor(directory, label) {
|
|
225
|
+
let candidate = directory;
|
|
226
|
+
while (true) {
|
|
227
|
+
try {
|
|
228
|
+
fs.lstatSync(candidate);
|
|
229
|
+
return candidate;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
232
|
+
const parent = path.dirname(candidate);
|
|
233
|
+
if (parent === candidate) {
|
|
234
|
+
throw new Error(`${label} directory has no existing anchor: ${directory}`);
|
|
235
|
+
}
|
|
236
|
+
candidate = parent;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function ensureProtectedDirectory(directory, label) {
|
|
242
|
+
const absoluteDirectory = path.resolve(directory);
|
|
243
|
+
const home = path.resolve(os.homedir());
|
|
244
|
+
const underHome = absoluteDirectory === home || absoluteDirectory.startsWith(`${home}${path.sep}`);
|
|
245
|
+
const anchor = underHome ? home : existingProtectedDirectoryAnchor(absoluteDirectory, label);
|
|
246
|
+
const relative = path.relative(anchor, absoluteDirectory);
|
|
247
|
+
const components = relative ? relative.split(path.sep) : [];
|
|
248
|
+
if (process.platform === "win32") {
|
|
249
|
+
let current = anchor;
|
|
250
|
+
validateProtectedDirectoryStats(fs.lstatSync(current), current, label);
|
|
251
|
+
for (const component of components) {
|
|
252
|
+
current = path.join(current, component);
|
|
253
|
+
try {
|
|
254
|
+
validateProtectedDirectoryStats(fs.lstatSync(current), current, label);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
257
|
+
try {
|
|
258
|
+
fs.mkdirSync(current, { mode: 0o700 });
|
|
259
|
+
} catch (mkdirError) {
|
|
260
|
+
if (!mkdirError || mkdirError.code !== "EEXIST") throw mkdirError;
|
|
261
|
+
}
|
|
262
|
+
validateProtectedDirectoryStats(fs.lstatSync(current), current, label);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
268
|
+
const directoryOnly = typeof fs.constants.O_DIRECTORY === "number" ? fs.constants.O_DIRECTORY : 0;
|
|
269
|
+
const flags = fs.constants.O_RDONLY | noFollow | directoryOnly;
|
|
270
|
+
const descriptorRoot = process.platform === "linux" ? "/proc/self/fd" : "/dev/fd";
|
|
271
|
+
let descriptor = null;
|
|
272
|
+
let current = anchor;
|
|
273
|
+
try {
|
|
274
|
+
descriptor = fs.openSync(anchor, flags);
|
|
275
|
+
validateProtectedDirectoryStats(fs.fstatSync(descriptor), anchor, label);
|
|
276
|
+
for (const component of components) {
|
|
277
|
+
current = path.join(current, component);
|
|
278
|
+
const accessPath = path.join(descriptorRoot, String(descriptor), component);
|
|
279
|
+
let child;
|
|
280
|
+
try {
|
|
281
|
+
child = fs.openSync(accessPath, flags);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
284
|
+
try {
|
|
285
|
+
fs.mkdirSync(accessPath, { mode: 0o700 });
|
|
286
|
+
} catch (mkdirError) {
|
|
287
|
+
if (!mkdirError || mkdirError.code !== "EEXIST") throw mkdirError;
|
|
288
|
+
}
|
|
289
|
+
child = fs.openSync(accessPath, flags);
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
validateProtectedDirectoryStats(fs.fstatSync(child), current, label);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
fs.closeSync(child);
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
fs.closeSync(descriptor);
|
|
298
|
+
descriptor = child;
|
|
299
|
+
}
|
|
300
|
+
const held = fs.fstatSync(descriptor);
|
|
301
|
+
const currentPath = fs.lstatSync(absoluteDirectory);
|
|
302
|
+
validateProtectedDirectoryStats(currentPath, absoluteDirectory, label);
|
|
303
|
+
if (!sameProtectedDirectoryIdentity(held, protectedDirectoryIdentity(currentPath))) {
|
|
304
|
+
throw new Error(`${label} directory changed while creating: ${absoluteDirectory}`);
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error && String(error.message || "").startsWith(`${label} directory`)) throw error;
|
|
308
|
+
throw new Error(`${label} directory cannot be created safely: ${current}`);
|
|
309
|
+
} finally {
|
|
310
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function openProtectedDirectory(file, label) {
|
|
315
|
+
const absoluteFile = path.resolve(file);
|
|
316
|
+
const directory = path.dirname(absoluteFile);
|
|
317
|
+
if (process.platform === "win32") {
|
|
318
|
+
const details = fs.lstatSync(directory);
|
|
319
|
+
validateProtectedDirectoryStats(details, directory, label);
|
|
320
|
+
return {
|
|
321
|
+
accessPath: absoluteFile,
|
|
322
|
+
absoluteFile,
|
|
323
|
+
descriptor: null,
|
|
324
|
+
directory,
|
|
325
|
+
identity: protectedDirectoryIdentity(details)
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const home = path.resolve(os.homedir());
|
|
329
|
+
const underHome = directory === home || directory.startsWith(`${home}${path.sep}`);
|
|
330
|
+
const anchor = underHome ? home : existingProtectedDirectoryAnchor(directory, label);
|
|
331
|
+
const relative = path.relative(anchor, directory);
|
|
332
|
+
const components = relative ? relative.split(path.sep) : [];
|
|
333
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
334
|
+
const directoryOnly = typeof fs.constants.O_DIRECTORY === "number" ? fs.constants.O_DIRECTORY : 0;
|
|
335
|
+
const flags = fs.constants.O_RDONLY | noFollow | directoryOnly;
|
|
336
|
+
const descriptorRoot = process.platform === "linux" ? "/proc/self/fd" : "/dev/fd";
|
|
337
|
+
let descriptor = null;
|
|
338
|
+
let current = anchor;
|
|
339
|
+
try {
|
|
340
|
+
descriptor = fs.openSync(anchor, flags);
|
|
341
|
+
validateProtectedDirectoryStats(fs.fstatSync(descriptor), anchor, label);
|
|
342
|
+
for (const component of components) {
|
|
343
|
+
current = path.join(current, component);
|
|
344
|
+
const child = fs.openSync(path.join(descriptorRoot, String(descriptor), component), flags);
|
|
345
|
+
try {
|
|
346
|
+
validateProtectedDirectoryStats(fs.fstatSync(child), current, label);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
fs.closeSync(child);
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
fs.closeSync(descriptor);
|
|
352
|
+
descriptor = child;
|
|
353
|
+
}
|
|
354
|
+
const identity = protectedDirectoryIdentity(fs.fstatSync(descriptor));
|
|
355
|
+
const accessPath = path.join(descriptorRoot, String(descriptor), path.basename(absoluteFile));
|
|
356
|
+
return { accessPath, absoluteFile, descriptor, directory, identity };
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
359
|
+
if (error && String(error.message || "").startsWith(`${label} directory`)) throw error;
|
|
360
|
+
throw new Error(`${label} directory cannot be opened safely: ${current}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function closeProtectedDirectory(protectedDirectory, label) {
|
|
365
|
+
if (protectedDirectory.descriptor === null) {
|
|
366
|
+
const current = fs.lstatSync(protectedDirectory.directory);
|
|
367
|
+
validateProtectedDirectoryStats(current, protectedDirectory.directory, label);
|
|
368
|
+
if (!sameProtectedDirectoryIdentity(current, protectedDirectory.identity)) {
|
|
369
|
+
throw new Error(`${label} directory changed while reading: ${protectedDirectory.directory}`);
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const held = fs.fstatSync(protectedDirectory.descriptor);
|
|
375
|
+
const current = fs.lstatSync(protectedDirectory.directory);
|
|
376
|
+
validateProtectedDirectoryStats(current, protectedDirectory.directory, label);
|
|
377
|
+
if (!sameProtectedDirectoryIdentity(held, protectedDirectory.identity)
|
|
378
|
+
|| !sameProtectedDirectoryIdentity(current, protectedDirectory.identity)) {
|
|
379
|
+
throw new Error(`${label} directory changed while reading: ${protectedDirectory.directory}`);
|
|
380
|
+
}
|
|
381
|
+
} finally {
|
|
382
|
+
fs.closeSync(protectedDirectory.descriptor);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function readProtectedDeliveryPolicy(file) {
|
|
387
|
+
const protectedDirectory = openProtectedDirectory(file, "delivery policy");
|
|
388
|
+
const policyFile = protectedDirectory.accessPath;
|
|
389
|
+
try {
|
|
390
|
+
const before = fs.lstatSync(policyFile);
|
|
391
|
+
validatePolicyStats(before);
|
|
392
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
393
|
+
const descriptor = fs.openSync(policyFile, fs.constants.O_RDONLY | noFollow);
|
|
394
|
+
let raw;
|
|
395
|
+
let opened;
|
|
396
|
+
try {
|
|
397
|
+
opened = fs.fstatSync(descriptor);
|
|
398
|
+
validatePolicyStats(opened);
|
|
399
|
+
if (!sameRecordIdentity(opened, recordIdentity(before)) || opened.nlink !== before.nlink) {
|
|
400
|
+
throw new Error("delivery policy changed while opening");
|
|
401
|
+
}
|
|
402
|
+
const buffer = Buffer.alloc(MAX_POLICY_BYTES + 1);
|
|
403
|
+
let size = 0;
|
|
404
|
+
while (size < buffer.length) {
|
|
405
|
+
const count = fs.readSync(descriptor, buffer, size, buffer.length - size, null);
|
|
406
|
+
if (count === 0) break;
|
|
407
|
+
size += count;
|
|
408
|
+
}
|
|
409
|
+
const afterRead = fs.fstatSync(descriptor);
|
|
410
|
+
if (!sameRecordIdentity(afterRead, recordIdentity(opened)) || afterRead.nlink !== opened.nlink) {
|
|
411
|
+
throw new Error("delivery policy changed while reading");
|
|
412
|
+
}
|
|
413
|
+
if (size > MAX_POLICY_BYTES) throw new Error("delivery policy has an invalid size");
|
|
414
|
+
raw = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, size)).replace(/^\uFEFF/, "");
|
|
415
|
+
} finally {
|
|
416
|
+
fs.closeSync(descriptor);
|
|
417
|
+
}
|
|
418
|
+
const afterPath = fs.lstatSync(policyFile);
|
|
419
|
+
if (!sameRecordIdentity(afterPath, recordIdentity(opened)) || afterPath.nlink !== opened.nlink) {
|
|
420
|
+
throw new Error("delivery policy changed while reading");
|
|
421
|
+
}
|
|
422
|
+
const parsed = JSON.parse(raw);
|
|
423
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
424
|
+
throw new Error("delivery policy root must be an object");
|
|
425
|
+
}
|
|
426
|
+
const isolated = parsed.isolated_service;
|
|
427
|
+
if (parsed.mandatory !== true || !isolated || typeof isolated !== "object" || Array.isArray(isolated)
|
|
428
|
+
|| isolated.required !== true || typeof isolated.endpoint !== "string" || !isolated.endpoint.trim()
|
|
429
|
+
|| isolated.endpoint.length > 4096 || typeof isolated.token_file !== "string" || !isolated.token_file.trim()
|
|
430
|
+
|| isolated.token_file.length > 4096
|
|
431
|
+
|| (Object.hasOwn(parsed, "fail_closed") && parsed.fail_closed !== true)
|
|
432
|
+
|| (Object.hasOwn(parsed, "direct_delivery_allowed") && parsed.direct_delivery_allowed !== false)
|
|
433
|
+
|| (Object.hasOwn(parsed, "raw_streaming_allowed") && parsed.raw_streaming_allowed !== false)
|
|
434
|
+
|| (Object.hasOwn(parsed, "on_guard_error") && parsed.on_guard_error !== "block")) {
|
|
435
|
+
throw new Error("mandatory isolated-service policy is invalid");
|
|
436
|
+
}
|
|
437
|
+
return parsed;
|
|
438
|
+
} finally {
|
|
439
|
+
closeProtectedDirectory(protectedDirectory, "delivery policy");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function validateServiceTokenStats(stats) {
|
|
444
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("service token must be a regular file");
|
|
445
|
+
if (stats.nlink !== 1) throw new Error("service token must not have additional hard links");
|
|
446
|
+
if (stats.size < 32 || stats.size > MAX_SERVICE_TOKEN_BYTES) throw new Error("service token has an invalid size");
|
|
447
|
+
if (process.platform !== "win32" && (stats.mode & 0o077) !== 0) {
|
|
448
|
+
throw new Error("service token permissions are too broad");
|
|
449
|
+
}
|
|
450
|
+
if (typeof process.getuid === "function" && stats.uid !== process.getuid()) {
|
|
451
|
+
throw new Error("service token has the wrong owner");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function readProtectedServiceToken(destination) {
|
|
456
|
+
const protectedDirectory = openProtectedDirectory(destination, "service token");
|
|
457
|
+
const tokenFile = protectedDirectory.accessPath;
|
|
458
|
+
try {
|
|
459
|
+
const before = fs.lstatSync(tokenFile);
|
|
460
|
+
validateServiceTokenStats(before);
|
|
461
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
462
|
+
const descriptor = fs.openSync(tokenFile, fs.constants.O_RDONLY | noFollow);
|
|
463
|
+
let opened;
|
|
464
|
+
try {
|
|
465
|
+
opened = fs.fstatSync(descriptor);
|
|
466
|
+
validateServiceTokenStats(opened);
|
|
467
|
+
if (!sameRecordIdentity(opened, recordIdentity(before)) || opened.nlink !== before.nlink) {
|
|
468
|
+
throw new Error("service token changed while opening");
|
|
469
|
+
}
|
|
470
|
+
const buffer = Buffer.alloc(MAX_SERVICE_TOKEN_BYTES + 1);
|
|
471
|
+
let size = 0;
|
|
472
|
+
while (size < buffer.length) {
|
|
473
|
+
const count = fs.readSync(descriptor, buffer, size, buffer.length - size, null);
|
|
474
|
+
if (count === 0) break;
|
|
475
|
+
size += count;
|
|
476
|
+
}
|
|
477
|
+
const after = fs.fstatSync(descriptor);
|
|
478
|
+
const afterPath = fs.lstatSync(tokenFile);
|
|
479
|
+
if (!sameRecordIdentity(after, recordIdentity(opened)) || after.nlink !== opened.nlink
|
|
480
|
+
|| !sameRecordIdentity(afterPath, recordIdentity(opened)) || afterPath.nlink !== opened.nlink) {
|
|
481
|
+
throw new Error("service token changed while reading");
|
|
482
|
+
}
|
|
483
|
+
if (size > MAX_SERVICE_TOKEN_BYTES) throw new Error("service token has an invalid size");
|
|
484
|
+
const token = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, size)).replace(/^\uFEFF/, "").trim();
|
|
485
|
+
if (token.length < 32) throw new Error("service token is invalid");
|
|
486
|
+
return token;
|
|
487
|
+
} finally {
|
|
488
|
+
fs.closeSync(descriptor);
|
|
489
|
+
}
|
|
490
|
+
} finally {
|
|
491
|
+
closeProtectedDirectory(protectedDirectory, "service token");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function runtimeConfig() {
|
|
496
|
+
const runtime = process.env.BLUN_LANGUAGE_GUARD_RUNTIME || DEFAULT_RUNTIME;
|
|
497
|
+
const policyPath = process.env.BLUN_LANGUAGE_GUARD_POLICY || path.join(runtime, "delivery-policy.json");
|
|
498
|
+
const policy = readProtectedDeliveryPolicy(policyPath);
|
|
499
|
+
const isolated = policy.isolated_service;
|
|
500
|
+
if (policy.mandatory !== true || !isolated || isolated.required !== true) {
|
|
501
|
+
throw new Error("mandatory isolated-service policy is missing");
|
|
502
|
+
}
|
|
503
|
+
const endpoint = process.env.BLUN_LANGUAGE_GUARD_SERVICE_ENDPOINT || isolated.endpoint;
|
|
504
|
+
const tokenFile = process.env.BLUN_LANGUAGE_GUARD_SERVICE_TOKEN_FILE || isolated.token_file;
|
|
505
|
+
const token = process.env.BLUN_LANGUAGE_GUARD_SERVICE_TOKEN || readProtectedServiceToken(tokenFile);
|
|
506
|
+
if (!endpoint || token.length < 32) {
|
|
507
|
+
throw new Error("isolated service endpoint or token is invalid");
|
|
508
|
+
}
|
|
509
|
+
return { endpoint, token, runtime };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function parseEndpoint(endpoint) {
|
|
513
|
+
if (endpoint.startsWith("unix:") && endpoint.length > 5) {
|
|
514
|
+
return { path: endpoint.slice(5) };
|
|
515
|
+
}
|
|
516
|
+
if (endpoint.startsWith("tcp:")) {
|
|
517
|
+
const value = endpoint.slice(4);
|
|
518
|
+
const split = value.lastIndexOf(":");
|
|
519
|
+
const host = value.slice(0, split);
|
|
520
|
+
const port = Number(value.slice(split + 1));
|
|
521
|
+
if (!["127.0.0.1", "localhost", "::1"].includes(host) || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
522
|
+
throw new Error("guard service TCP endpoint must use loopback");
|
|
523
|
+
}
|
|
524
|
+
return { host, port };
|
|
525
|
+
}
|
|
526
|
+
throw new Error("guard service endpoint must be a Unix socket or loopback TCP endpoint");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function callGuard(request, timeoutMs = 5000) {
|
|
530
|
+
const config = runtimeConfig();
|
|
531
|
+
const address = parseEndpoint(config.endpoint);
|
|
532
|
+
const payload = Buffer.from(JSON.stringify({ ...request, service_token: config.token }) + "\n", "utf8");
|
|
533
|
+
if (payload.length > MAX_INPUT_BYTES) {
|
|
534
|
+
return Promise.reject(new Error("guard request is too large"));
|
|
535
|
+
}
|
|
536
|
+
return new Promise((resolve, reject) => {
|
|
537
|
+
let settled = false;
|
|
538
|
+
let buffered = Buffer.alloc(0);
|
|
539
|
+
const socket = net.createConnection(address);
|
|
540
|
+
const finish = (error, result) => {
|
|
541
|
+
if (settled) return;
|
|
542
|
+
settled = true;
|
|
543
|
+
socket.destroy();
|
|
544
|
+
if (error) reject(error); else resolve(result);
|
|
545
|
+
};
|
|
546
|
+
socket.setTimeout(timeoutMs, () => finish(new Error("guard service timed out")));
|
|
547
|
+
socket.on("error", (error) => finish(error));
|
|
548
|
+
socket.on("connect", () => socket.write(payload));
|
|
549
|
+
socket.on("data", (chunk) => {
|
|
550
|
+
buffered = Buffer.concat([buffered, chunk]);
|
|
551
|
+
if (buffered.length > MAX_INPUT_BYTES) return finish(new Error("guard response is too large"));
|
|
552
|
+
const newline = buffered.indexOf(10);
|
|
553
|
+
if (newline < 0) return;
|
|
554
|
+
try {
|
|
555
|
+
finish(null, JSON.parse(buffered.subarray(0, newline).toString("utf8").replace(/^\uFEFF/, "")));
|
|
556
|
+
} catch (error) {
|
|
557
|
+
finish(new Error("guard service returned invalid JSON"));
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
socket.on("end", () => {
|
|
561
|
+
if (!settled) finish(new Error("guard service closed without a response"));
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function findRelease(value, seen = new Set()) {
|
|
567
|
+
if (typeof value === "string") {
|
|
568
|
+
const trimmed = value.trim().replace(/^\uFEFF/, "");
|
|
569
|
+
if ((trimmed.startsWith("{") || trimmed.startsWith("[")) && trimmed.length <= MAX_INPUT_BYTES) {
|
|
570
|
+
try { return findRelease(JSON.parse(trimmed), seen); } catch (_) { return null; }
|
|
571
|
+
}
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
if (!value || typeof value !== "object" || seen.has(value)) return null;
|
|
575
|
+
seen.add(value);
|
|
576
|
+
if (value.release_allowed === true && typeof value.release_token === "string") return value;
|
|
577
|
+
const children = Array.isArray(value) ? value : Object.values(value);
|
|
578
|
+
for (const child of children) {
|
|
579
|
+
const found = findRelease(child, seen);
|
|
580
|
+
if (found) return found;
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function stateDirectory() {
|
|
586
|
+
if (process.env.BLUN_LANGUAGE_GUARD_HOOK_STATE_DIR) return process.env.BLUN_LANGUAGE_GUARD_HOOK_STATE_DIR;
|
|
587
|
+
return path.join(runtimeConfig().runtime, "claude-hooks");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function hookIdentity(input, agentPolicy = "optional") {
|
|
591
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
592
|
+
throw new Error("Claude hook input has no valid identity");
|
|
593
|
+
}
|
|
594
|
+
if (!["optional", "required", "forbidden"].includes(agentPolicy)) {
|
|
595
|
+
throw new Error("Claude hook identity has no valid agent policy");
|
|
596
|
+
}
|
|
597
|
+
const session = input.session_id;
|
|
598
|
+
const hasAgent = Object.prototype.hasOwnProperty.call(input, "agent_id");
|
|
599
|
+
if (agentPolicy === "required" && !hasAgent) {
|
|
600
|
+
throw new Error("Claude hook input has no explicit agent_id");
|
|
601
|
+
}
|
|
602
|
+
if (agentPolicy === "forbidden" && hasAgent) {
|
|
603
|
+
throw new Error("Claude hook input has an unexpected agent_id");
|
|
604
|
+
}
|
|
605
|
+
const agent = hasAgent ? input.agent_id : "main";
|
|
606
|
+
if (typeof session !== "string" || session.length === 0) {
|
|
607
|
+
throw new Error("Claude hook input has no valid session_id");
|
|
608
|
+
}
|
|
609
|
+
if (typeof agent !== "string" || agent.length === 0) {
|
|
610
|
+
throw new Error("Claude hook input has no valid agent_id");
|
|
611
|
+
}
|
|
612
|
+
if (session.includes("\0") || agent.includes("\0")) {
|
|
613
|
+
throw new Error("Claude hook identity fields must not contain NUL characters");
|
|
614
|
+
}
|
|
615
|
+
return { session, agent };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function identity(input) {
|
|
619
|
+
const { session, agent } = hookIdentity(input);
|
|
620
|
+
return crypto.createHash("sha256").update(`${session}\0${agent}`, "utf8").digest("hex");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function sessionHash(input) {
|
|
624
|
+
const { session } = hookIdentity(input);
|
|
625
|
+
return crypto.createHash("sha256").update(session, "utf8").digest("hex");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function sessionEpochPath(input) {
|
|
629
|
+
return path.join(stateDirectory(), `session-${sessionHash(input)}.epoch`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function validateSessionEpochStats(stats) {
|
|
633
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("session epoch must be a regular file");
|
|
634
|
+
if (stats.nlink !== 1) throw new Error("session epoch must not have additional hard links");
|
|
635
|
+
if (stats.size < 1 || stats.size > MAX_EPOCH_BYTES) throw new Error("session epoch has an invalid size");
|
|
636
|
+
if (process.platform !== "win32" && (stats.mode & 0o077) !== 0) {
|
|
637
|
+
throw new Error("session epoch permissions are too broad");
|
|
638
|
+
}
|
|
639
|
+
if (typeof process.getuid === "function" && stats.uid !== process.getuid()) {
|
|
640
|
+
throw new Error("session epoch has the wrong owner");
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function readSessionEpochFile(epochFile) {
|
|
645
|
+
const before = fs.lstatSync(epochFile);
|
|
646
|
+
validateSessionEpochStats(before);
|
|
647
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
648
|
+
const descriptor = fs.openSync(epochFile, fs.constants.O_RDONLY | noFollow);
|
|
649
|
+
try {
|
|
650
|
+
const opened = fs.fstatSync(descriptor);
|
|
651
|
+
validateSessionEpochStats(opened);
|
|
652
|
+
if (!sameRecordIdentity(opened, recordIdentity(before))) {
|
|
653
|
+
throw new Error("session epoch changed while opening");
|
|
654
|
+
}
|
|
655
|
+
const rawEpoch = readBoundedUtf8Descriptor(descriptor, MAX_EPOCH_BYTES, "session epoch");
|
|
656
|
+
const finished = fs.fstatSync(descriptor);
|
|
657
|
+
validateSessionEpochStats(finished);
|
|
658
|
+
if (!sameRecordIdentity(finished, recordIdentity(opened))) {
|
|
659
|
+
throw new Error("session epoch changed while reading");
|
|
660
|
+
}
|
|
661
|
+
const epoch = rawEpoch.replace(/^\uFEFF/, "").trim();
|
|
662
|
+
if (!/^[a-f0-9]{64}$/.test(epoch)) throw new Error("session epoch is invalid");
|
|
663
|
+
return { epoch, fileIdentity: recordIdentity(opened) };
|
|
664
|
+
} finally {
|
|
665
|
+
fs.closeSync(descriptor);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function readSessionEpoch(input) {
|
|
670
|
+
const destination = sessionEpochPath(input);
|
|
671
|
+
const protectedDirectory = openProtectedDirectory(destination, "session epoch");
|
|
672
|
+
try {
|
|
673
|
+
return { destination, ...readSessionEpochFile(protectedDirectory.accessPath) };
|
|
674
|
+
} finally {
|
|
675
|
+
closeProtectedDirectory(protectedDirectory, "session epoch");
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function removeExistingSessionEpochFile(epochFile) {
|
|
680
|
+
let inspected;
|
|
681
|
+
try {
|
|
682
|
+
inspected = readSessionEpochFile(epochFile);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (error && error.code === "ENOENT") return;
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
quarantineAndRemoveExactFile(
|
|
688
|
+
epochFile,
|
|
689
|
+
inspected.fileIdentity,
|
|
690
|
+
validateSessionEpochStats,
|
|
691
|
+
"session epoch changed before renewal",
|
|
692
|
+
"session epoch changed while quarantining"
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function assertSessionEpochPublicationTargetAbsent(epochFile) {
|
|
697
|
+
let current;
|
|
698
|
+
try {
|
|
699
|
+
current = fs.lstatSync(epochFile);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if (error && error.code === "ENOENT") return;
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
validateSessionEpochStats(current);
|
|
705
|
+
throw new Error("session epoch changed before publication");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function beginSessionEpoch(input) {
|
|
709
|
+
const { session } = hookIdentity(input);
|
|
710
|
+
const directory = stateDirectory();
|
|
711
|
+
ensureProtectedDirectory(directory, "session epoch");
|
|
712
|
+
const destination = sessionEpochPath(input);
|
|
713
|
+
const protectedDirectory = openProtectedDirectory(destination, "session epoch");
|
|
714
|
+
const epochFile = protectedDirectory.accessPath;
|
|
715
|
+
try {
|
|
716
|
+
removeExistingSessionEpochFile(epochFile);
|
|
717
|
+
invalidateSessionRecords(input);
|
|
718
|
+
const epoch = crypto.randomBytes(32).toString("hex");
|
|
719
|
+
const registration = await callGuard({
|
|
720
|
+
operation: "register_session_epoch",
|
|
721
|
+
session_id: session,
|
|
722
|
+
session_epoch: epoch
|
|
723
|
+
}, 3000);
|
|
724
|
+
if (registration.status !== "PASS" || registration.registered !== true) {
|
|
725
|
+
throw new Error("isolated guard rejected the session epoch");
|
|
726
|
+
}
|
|
727
|
+
const temporary = `${epochFile}.${process.pid}.${crypto.randomBytes(12).toString("hex")}.tmp`;
|
|
728
|
+
let descriptor;
|
|
729
|
+
let createdIdentity;
|
|
730
|
+
try {
|
|
731
|
+
descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
|
|
732
|
+
createdIdentity = temporaryFileIdentity(fs.fstatSync(descriptor));
|
|
733
|
+
fs.writeFileSync(descriptor, `${epoch}\n`, "utf8");
|
|
734
|
+
if (process.platform !== "win32") fs.fchmodSync(descriptor, 0o600);
|
|
735
|
+
fs.fsyncSync(descriptor);
|
|
736
|
+
const sealedStats = fs.fstatSync(descriptor);
|
|
737
|
+
validateSessionEpochStats(sealedStats);
|
|
738
|
+
const sealedIdentity = recordIdentity(sealedStats);
|
|
739
|
+
assertSessionEpochPublicationTargetAbsent(epochFile);
|
|
740
|
+
assertProtectedPublicationFile(
|
|
741
|
+
temporary,
|
|
742
|
+
sealedIdentity,
|
|
743
|
+
validateSessionEpochStats,
|
|
744
|
+
"session epoch temporary file changed before publication"
|
|
745
|
+
);
|
|
746
|
+
fs.renameSync(temporary, epochFile);
|
|
747
|
+
const publishedStats = fs.fstatSync(descriptor);
|
|
748
|
+
validateSessionEpochStats(publishedStats);
|
|
749
|
+
assertProtectedPublicationFile(
|
|
750
|
+
epochFile,
|
|
751
|
+
recordIdentity(publishedStats),
|
|
752
|
+
validateSessionEpochStats,
|
|
753
|
+
"session epoch changed during publication"
|
|
754
|
+
);
|
|
755
|
+
fs.closeSync(descriptor);
|
|
756
|
+
descriptor = undefined;
|
|
757
|
+
} finally {
|
|
758
|
+
try {
|
|
759
|
+
removeCreatedTemporaryFile(temporary, createdIdentity, "session epoch", descriptor);
|
|
760
|
+
} finally {
|
|
761
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return epoch;
|
|
765
|
+
} finally {
|
|
766
|
+
closeProtectedDirectory(protectedDirectory, "session epoch");
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function statePath(input) {
|
|
771
|
+
return path.join(stateDirectory(), `${identity(input)}.json`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function validateRecordStats(stats) {
|
|
775
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
776
|
+
throw new Error("delivery grant state must be a regular file");
|
|
777
|
+
}
|
|
778
|
+
if (stats.nlink !== 1) {
|
|
779
|
+
throw new Error("delivery grant state must not have additional hard links");
|
|
780
|
+
}
|
|
781
|
+
if (stats.size < 1 || stats.size > MAX_RECORD_BYTES) {
|
|
782
|
+
throw new Error("delivery grant state has an invalid size");
|
|
783
|
+
}
|
|
784
|
+
if (process.platform !== "win32" && (stats.mode & 0o077) !== 0) {
|
|
785
|
+
throw new Error("delivery grant state permissions are too broad");
|
|
786
|
+
}
|
|
787
|
+
if (typeof process.getuid === "function" && stats.uid !== process.getuid()) {
|
|
788
|
+
throw new Error("delivery grant state has the wrong owner");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function recordIdentity(stats) {
|
|
793
|
+
return {
|
|
794
|
+
dev: stats.dev,
|
|
795
|
+
ino: stats.ino,
|
|
796
|
+
nlink: stats.nlink,
|
|
797
|
+
size: stats.size,
|
|
798
|
+
ctimeMs: stats.ctimeMs,
|
|
799
|
+
mtimeMs: stats.mtimeMs
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function sameRecordIdentity(stats, expected) {
|
|
804
|
+
return expected && stats.dev === expected.dev && stats.ino === expected.ino
|
|
805
|
+
&& stats.nlink === expected.nlink && stats.size === expected.size && stats.ctimeMs === expected.ctimeMs
|
|
806
|
+
&& stats.mtimeMs === expected.mtimeMs;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function sameRenamedRecordIdentity(stats, expected) {
|
|
810
|
+
return expected && stats.dev === expected.dev && stats.ino === expected.ino
|
|
811
|
+
&& stats.nlink === expected.nlink && stats.size === expected.size && stats.mtimeMs === expected.mtimeMs;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function quarantineAndRemoveExactFile(file, expected, validate, changedBeforeMessage, changedDuringMessage) {
|
|
815
|
+
const current = fs.lstatSync(file);
|
|
816
|
+
validate(current);
|
|
817
|
+
if (!sameRecordIdentity(current, expected)) throw new Error(changedBeforeMessage);
|
|
818
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
819
|
+
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
820
|
+
const quarantine = `${file}.${process.pid}.${crypto.randomBytes(12).toString("hex")}.remove`;
|
|
821
|
+
try {
|
|
822
|
+
const opened = fs.fstatSync(descriptor);
|
|
823
|
+
validate(opened);
|
|
824
|
+
if (!sameRecordIdentity(opened, expected)) throw new Error(changedBeforeMessage);
|
|
825
|
+
try {
|
|
826
|
+
fs.lstatSync(quarantine);
|
|
827
|
+
throw new Error(`${changedDuringMessage}: quarantine target already exists`);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
830
|
+
}
|
|
831
|
+
fs.renameSync(file, quarantine);
|
|
832
|
+
const held = fs.fstatSync(descriptor);
|
|
833
|
+
const moved = fs.lstatSync(quarantine);
|
|
834
|
+
validate(held);
|
|
835
|
+
validate(moved);
|
|
836
|
+
if (!sameRenamedRecordIdentity(held, expected)
|
|
837
|
+
|| !sameRecordIdentity(moved, recordIdentity(held))) {
|
|
838
|
+
throw new Error(changedDuringMessage);
|
|
839
|
+
}
|
|
840
|
+
fs.unlinkSync(quarantine);
|
|
841
|
+
const removed = fs.fstatSync(descriptor);
|
|
842
|
+
if (removed.nlink !== 0) throw new Error(changedDuringMessage);
|
|
843
|
+
} finally {
|
|
844
|
+
fs.closeSync(descriptor);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function temporaryFileIdentity(stats) {
|
|
849
|
+
return {
|
|
850
|
+
dev: stats.dev,
|
|
851
|
+
ino: stats.ino,
|
|
852
|
+
nlink: stats.nlink,
|
|
853
|
+
birthtimeMs: stats.birthtimeMs
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function sameTemporaryFileIdentity(stats, expected) {
|
|
858
|
+
return expected && stats.dev === expected.dev && stats.ino === expected.ino
|
|
859
|
+
&& stats.nlink === expected.nlink && stats.birthtimeMs === expected.birthtimeMs;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function assertProtectedPublicationFile(file, expected, validate, message) {
|
|
863
|
+
const current = fs.lstatSync(file);
|
|
864
|
+
validate(current);
|
|
865
|
+
if (!sameRecordIdentity(current, expected)) throw new Error(message);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function validateCreatedTemporaryFile(stats, expected, label) {
|
|
869
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1
|
|
870
|
+
|| (typeof process.getuid === "function" && stats.uid !== process.getuid())
|
|
871
|
+
|| !sameTemporaryFileIdentity(stats, expected)) {
|
|
872
|
+
throw new Error(`${label} temporary file changed before cleanup`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function removeCreatedTemporaryFile(file, expected, label, descriptor) {
|
|
877
|
+
if (!expected) return;
|
|
878
|
+
let current;
|
|
879
|
+
try {
|
|
880
|
+
current = fs.lstatSync(file);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
if (error && error.code === "ENOENT") return;
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
validateCreatedTemporaryFile(current, expected, label);
|
|
886
|
+
if (descriptor === undefined) {
|
|
887
|
+
throw new Error(`${label} temporary file descriptor unavailable during cleanup`);
|
|
888
|
+
}
|
|
889
|
+
validateCreatedTemporaryFile(fs.fstatSync(descriptor), expected, label);
|
|
890
|
+
const quarantine = `${file}.${process.pid}.${crypto.randomBytes(12).toString("hex")}.cleanup`;
|
|
891
|
+
try {
|
|
892
|
+
fs.lstatSync(quarantine);
|
|
893
|
+
throw new Error(`${label} temporary cleanup quarantine already exists`);
|
|
894
|
+
} catch (error) {
|
|
895
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
896
|
+
}
|
|
897
|
+
fs.renameSync(file, quarantine);
|
|
898
|
+
const held = fs.fstatSync(descriptor);
|
|
899
|
+
const moved = fs.lstatSync(quarantine);
|
|
900
|
+
if (!sameTemporaryFileIdentity(held, expected)
|
|
901
|
+
|| !sameTemporaryFileIdentity(moved, expected)
|
|
902
|
+
|| !sameTemporaryFileIdentity(moved, temporaryFileIdentity(held))) {
|
|
903
|
+
throw new Error(`${label} temporary file changed during cleanup`);
|
|
904
|
+
}
|
|
905
|
+
fs.unlinkSync(quarantine);
|
|
906
|
+
if (fs.fstatSync(descriptor).nlink !== 0) {
|
|
907
|
+
throw new Error(`${label} temporary file changed during cleanup`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function readProtectedRecordFile(recordFile) {
|
|
912
|
+
const before = fs.lstatSync(recordFile);
|
|
913
|
+
validateRecordStats(before);
|
|
914
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
915
|
+
const descriptor = fs.openSync(recordFile, fs.constants.O_RDONLY | noFollow);
|
|
916
|
+
try {
|
|
917
|
+
const opened = fs.fstatSync(descriptor);
|
|
918
|
+
validateRecordStats(opened);
|
|
919
|
+
if (!sameRecordIdentity(opened, recordIdentity(before))) {
|
|
920
|
+
throw new Error("delivery grant state changed while opening");
|
|
921
|
+
}
|
|
922
|
+
const raw = readBoundedUtf8Descriptor(descriptor, MAX_RECORD_BYTES, "delivery grant state")
|
|
923
|
+
.replace(/^\uFEFF/, "");
|
|
924
|
+
const finished = fs.fstatSync(descriptor);
|
|
925
|
+
validateRecordStats(finished);
|
|
926
|
+
if (!sameRecordIdentity(finished, recordIdentity(opened))) {
|
|
927
|
+
throw new Error("delivery grant state changed while reading");
|
|
928
|
+
}
|
|
929
|
+
const parsed = JSON.parse(raw);
|
|
930
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
931
|
+
throw new Error("delivery grant state root must be an object");
|
|
932
|
+
}
|
|
933
|
+
return { record: parsed, fileIdentity: recordIdentity(opened) };
|
|
934
|
+
} finally {
|
|
935
|
+
fs.closeSync(descriptor);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function readProtectedRecord(destination) {
|
|
940
|
+
const protectedDirectory = openProtectedDirectory(destination, "delivery grant state");
|
|
941
|
+
try {
|
|
942
|
+
return readProtectedRecordFile(protectedDirectory.accessPath);
|
|
943
|
+
} finally {
|
|
944
|
+
closeProtectedDirectory(protectedDirectory, "delivery grant state");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function removeExactRecordFile(stateFile, expected) {
|
|
949
|
+
quarantineAndRemoveExactFile(
|
|
950
|
+
stateFile,
|
|
951
|
+
expected,
|
|
952
|
+
validateRecordStats,
|
|
953
|
+
"delivery grant state changed before consumption",
|
|
954
|
+
"delivery grant state changed while quarantining"
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function removeExactRecord(destination, expected) {
|
|
959
|
+
const protectedDirectory = openProtectedDirectory(destination, "Claude hook state");
|
|
960
|
+
try {
|
|
961
|
+
removeExactRecordFile(protectedDirectory.accessPath, expected);
|
|
962
|
+
} finally {
|
|
963
|
+
closeProtectedDirectory(protectedDirectory, "Claude hook state");
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function inspectRecordPublicationTarget(stateFile) {
|
|
968
|
+
try {
|
|
969
|
+
return readProtectedRecordFile(stateFile).fileIdentity;
|
|
970
|
+
} catch (error) {
|
|
971
|
+
if (error && error.code === "ENOENT") return null;
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function assertRecordPublicationTarget(stateFile, expected) {
|
|
977
|
+
let current;
|
|
978
|
+
try {
|
|
979
|
+
current = fs.lstatSync(stateFile);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
if (error && error.code === "ENOENT" && expected === null) return;
|
|
982
|
+
if (error && error.code === "ENOENT") {
|
|
983
|
+
throw new Error("delivery grant state changed before publication");
|
|
984
|
+
}
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
987
|
+
validateRecordStats(current);
|
|
988
|
+
if (expected === null || !sameRecordIdentity(current, expected)) {
|
|
989
|
+
throw new Error("delivery grant state changed before publication");
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function writeRecord(input, record) {
|
|
994
|
+
const destination = statePath(input);
|
|
995
|
+
const protectedDirectory = openProtectedDirectory(destination, "delivery grant state");
|
|
996
|
+
const stateFile = protectedDirectory.accessPath;
|
|
997
|
+
const temporary = `${stateFile}.${process.pid}.${crypto.randomBytes(12).toString("hex")}.tmp`;
|
|
998
|
+
try {
|
|
999
|
+
const existingIdentity = inspectRecordPublicationTarget(stateFile);
|
|
1000
|
+
let descriptor;
|
|
1001
|
+
let createdIdentity;
|
|
1002
|
+
try {
|
|
1003
|
+
descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
|
|
1004
|
+
createdIdentity = temporaryFileIdentity(fs.fstatSync(descriptor));
|
|
1005
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(record)}\n`, "utf8");
|
|
1006
|
+
if (process.platform !== "win32") fs.fchmodSync(descriptor, 0o600);
|
|
1007
|
+
fs.fsyncSync(descriptor);
|
|
1008
|
+
const sealedStats = fs.fstatSync(descriptor);
|
|
1009
|
+
validateRecordStats(sealedStats);
|
|
1010
|
+
const sealedIdentity = recordIdentity(sealedStats);
|
|
1011
|
+
assertRecordPublicationTarget(stateFile, existingIdentity);
|
|
1012
|
+
assertProtectedPublicationFile(
|
|
1013
|
+
temporary,
|
|
1014
|
+
sealedIdentity,
|
|
1015
|
+
validateRecordStats,
|
|
1016
|
+
"delivery grant temporary file changed before publication"
|
|
1017
|
+
);
|
|
1018
|
+
fs.renameSync(temporary, stateFile);
|
|
1019
|
+
const publishedStats = fs.fstatSync(descriptor);
|
|
1020
|
+
validateRecordStats(publishedStats);
|
|
1021
|
+
assertProtectedPublicationFile(
|
|
1022
|
+
stateFile,
|
|
1023
|
+
recordIdentity(publishedStats),
|
|
1024
|
+
validateRecordStats,
|
|
1025
|
+
"delivery grant state changed during publication"
|
|
1026
|
+
);
|
|
1027
|
+
fs.closeSync(descriptor);
|
|
1028
|
+
descriptor = undefined;
|
|
1029
|
+
} finally {
|
|
1030
|
+
try {
|
|
1031
|
+
removeCreatedTemporaryFile(temporary, createdIdentity, "delivery grant", descriptor);
|
|
1032
|
+
} finally {
|
|
1033
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
} finally {
|
|
1037
|
+
closeProtectedDirectory(protectedDirectory, "delivery grant state");
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function readRecord(input) {
|
|
1042
|
+
const destination = statePath(input);
|
|
1043
|
+
try {
|
|
1044
|
+
return { destination, ...readProtectedRecord(destination) };
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
if (error && error.code === "ENOENT") return { destination, record: null, fileIdentity: null };
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function invalidateAgentRecord(input) {
|
|
1052
|
+
const destination = statePath(input);
|
|
1053
|
+
const protectedDirectory = openProtectedDirectory(destination, "delivery grant state");
|
|
1054
|
+
const stateFile = protectedDirectory.accessPath;
|
|
1055
|
+
try {
|
|
1056
|
+
try {
|
|
1057
|
+
const { fileIdentity } = readProtectedRecordFile(stateFile);
|
|
1058
|
+
removeExactRecordFile(stateFile, fileIdentity);
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
1061
|
+
}
|
|
1062
|
+
} finally {
|
|
1063
|
+
closeProtectedDirectory(protectedDirectory, "delivery grant state");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function invalidateSessionRecords(input) {
|
|
1068
|
+
const directory = stateDirectory();
|
|
1069
|
+
const expectedSession = sessionHash(input);
|
|
1070
|
+
const legacyMainRecordName = path.basename(statePath(input));
|
|
1071
|
+
try {
|
|
1072
|
+
fs.lstatSync(directory);
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
if (error && error.code === "ENOENT") return;
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
const anchor = path.join(directory, ".session-invalidation-anchor");
|
|
1078
|
+
const protectedDirectory = openProtectedDirectory(anchor, "Claude hook state");
|
|
1079
|
+
const stateAccessDirectory = path.dirname(protectedDirectory.accessPath);
|
|
1080
|
+
try {
|
|
1081
|
+
const entries = fs.readdirSync(stateAccessDirectory, { withFileTypes: true });
|
|
1082
|
+
for (const entry of entries) {
|
|
1083
|
+
if ((!entry.isFile() && !entry.isSymbolicLink()) || !entry.name.endsWith(".json")) continue;
|
|
1084
|
+
const candidate = path.join(stateAccessDirectory, entry.name);
|
|
1085
|
+
const { record, fileIdentity } = readProtectedRecordFile(candidate);
|
|
1086
|
+
const legacyGrant = typeof record.session_sha256 !== "string"
|
|
1087
|
+
&& typeof record.delivery_grant === "string"
|
|
1088
|
+
&& Number.isFinite(record.authorized_at);
|
|
1089
|
+
const belongsToSession = entry.name === legacyMainRecordName
|
|
1090
|
+
|| record.session_sha256 === expectedSession || legacyGrant;
|
|
1091
|
+
if (!belongsToSession) continue;
|
|
1092
|
+
removeExactRecordFile(candidate, fileIdentity);
|
|
1093
|
+
}
|
|
1094
|
+
} finally {
|
|
1095
|
+
closeProtectedDirectory(protectedDirectory, "Claude hook state");
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function emit(payload) {
|
|
1100
|
+
if (payload) process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function blocked(reason) {
|
|
1104
|
+
return { decision: "block", reason };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function hostReleasePolicy(environment = process.env) {
|
|
1108
|
+
const hasLanguage = Object.prototype.hasOwnProperty.call(environment, "BLUN_LANGUAGE_GUARD_LANGUAGE");
|
|
1109
|
+
const hasTaskKind = Object.prototype.hasOwnProperty.call(environment, "BLUN_LANGUAGE_GUARD_TASK_KIND");
|
|
1110
|
+
if (!hasLanguage && !hasTaskKind) return null;
|
|
1111
|
+
const language = hasLanguage ? String(environment.BLUN_LANGUAGE_GUARD_LANGUAGE || "").trim() : "";
|
|
1112
|
+
const taskKind = hasTaskKind ? String(environment.BLUN_LANGUAGE_GUARD_TASK_KIND || "").trim().toLowerCase() : "";
|
|
1113
|
+
if (hasLanguage && (!EXACT_LANGUAGE.test(language) || ["auto", "all"].includes(language.toLowerCase()))) {
|
|
1114
|
+
throw new Error("host release language must be an exact language or locale tag");
|
|
1115
|
+
}
|
|
1116
|
+
if (hasTaskKind && !["response", "translation"].includes(taskKind)) {
|
|
1117
|
+
throw new Error("host release task kind must be response or translation");
|
|
1118
|
+
}
|
|
1119
|
+
return { language, taskKind };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function hostPolicyInstruction() {
|
|
1123
|
+
let policy;
|
|
1124
|
+
try {
|
|
1125
|
+
policy = hostReleasePolicy();
|
|
1126
|
+
} catch (_) {
|
|
1127
|
+
return "The trusted host release policy is invalid. Fail closed: do not call a release tool, finish, or deliver natural-language output until the host policy is repaired.";
|
|
1128
|
+
}
|
|
1129
|
+
if (!policy) return "";
|
|
1130
|
+
const parts = [];
|
|
1131
|
+
if (policy.taskKind) {
|
|
1132
|
+
const tool = policy.taskKind === "translation" ? "release_translation" : "release_response";
|
|
1133
|
+
parts.push(`The trusted host requires ${tool}; do not use the other release purpose.`);
|
|
1134
|
+
}
|
|
1135
|
+
if (policy.language) {
|
|
1136
|
+
parts.push(`Pass language exactly as ${JSON.stringify(policy.language)}. Do not substitute a base language or another locale.`);
|
|
1137
|
+
}
|
|
1138
|
+
return parts.join(" ");
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function preTool(input) {
|
|
1142
|
+
const toolName = String(input.tool_name || "");
|
|
1143
|
+
const purpose = toolName.endsWith("__release_translation") ? "translation"
|
|
1144
|
+
: toolName.endsWith("__release_response") ? "response" : "";
|
|
1145
|
+
if (!purpose) return;
|
|
1146
|
+
let policy;
|
|
1147
|
+
try {
|
|
1148
|
+
policy = hostReleasePolicy();
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
emit({
|
|
1151
|
+
hookSpecificOutput: {
|
|
1152
|
+
hookEventName: "PreToolUse",
|
|
1153
|
+
permissionDecision: "deny",
|
|
1154
|
+
permissionDecisionReason: `BLUN Language Guard blocked an invalid host release policy: ${error.message}.`,
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (!policy) return;
|
|
1160
|
+
if (policy.taskKind && policy.taskKind !== purpose) {
|
|
1161
|
+
const required = policy.taskKind === "translation" ? "release_translation" : "release_response";
|
|
1162
|
+
emit({
|
|
1163
|
+
hookSpecificOutput: {
|
|
1164
|
+
hookEventName: "PreToolUse",
|
|
1165
|
+
permissionDecision: "deny",
|
|
1166
|
+
permissionDecisionReason: `The trusted host requires ${required} for this turn.`,
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
if (!policy.language) return;
|
|
1172
|
+
const toolInput = input.tool_input && typeof input.tool_input === "object" && !Array.isArray(input.tool_input)
|
|
1173
|
+
? input.tool_input : {};
|
|
1174
|
+
emit({
|
|
1175
|
+
hookSpecificOutput: {
|
|
1176
|
+
hookEventName: "PreToolUse",
|
|
1177
|
+
updatedInput: { ...toolInput, language: policy.language },
|
|
1178
|
+
additionalContext: `The trusted host fixed this release to language ${JSON.stringify(policy.language)}.`,
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function isDirectTelegramDeliveryTool(toolName) {
|
|
1184
|
+
return /^mcp__.*telegram.*__(?:reply|send|send_message|sendMessage)$/i.test(String(toolName || ""));
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function preDelivery(input) {
|
|
1188
|
+
if (!isDirectTelegramDeliveryTool(input.tool_name)) return;
|
|
1189
|
+
emit({
|
|
1190
|
+
hookSpecificOutput: {
|
|
1191
|
+
hookEventName: "PreToolUse",
|
|
1192
|
+
permissionDecision: "deny",
|
|
1193
|
+
permissionDecisionReason: "Direct Telegram delivery is disabled by mandatory BLUN Translate Native. Return the verified final response normally; the host-owned bridge must deliver it after Stop verification."
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function blockedStop(input, reason) {
|
|
1199
|
+
const repeatedStop = input && input.stop_hook_active === true
|
|
1200
|
+
&& ["Stop", "SubagentStop"].includes(String(input.hook_event_name || ""));
|
|
1201
|
+
if (!repeatedStop) return blocked(reason);
|
|
1202
|
+
return {
|
|
1203
|
+
continue: false,
|
|
1204
|
+
stopReason: "BLUN Language Guard stopped an unverified response after the protected correction attempt failed. Reconnect or repair the guard, then retry the response."
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function malformedStopState() {
|
|
1209
|
+
return {
|
|
1210
|
+
continue: false,
|
|
1211
|
+
stopReason: "BLUN Language Guard stopped a response after receiving an invalid host stop state. Repair or reconnect the hook, then retry the response."
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function startupMessage(eventName, healthy) {
|
|
1216
|
+
const subject = eventName === "SubagentStart" ? "This subagent" : "This session";
|
|
1217
|
+
const policyInstruction = hostPolicyInstruction();
|
|
1218
|
+
if (!healthy) {
|
|
1219
|
+
return `${subject} is protected by mandatory BLUN Translate Native, but its isolated guard is unavailable. Fail closed: do not finish or deliver natural-language output until the service is healthy.${policyInstruction ? ` ${policyInstruction}` : ""}`;
|
|
1220
|
+
}
|
|
1221
|
+
return `${subject} is protected by mandatory BLUN Translate Native. Before every natural-language final answer call release_response with the exact final text. For translations load the translate-native skill and call release_translation with the complete source and target. Never call a Telegram reply or send tool directly; after Stop verifies the exact final response, the host-owned bridge delivers it. Do not rely on another agent's release: the final visible text must use a fresh grant bound to this session and agent identity and remain byte-for-byte equivalent after Unicode normalization to the released target.${policyInstruction ? ` ${policyInstruction}` : ""}`;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
async function startupContext(eventName) {
|
|
1225
|
+
try {
|
|
1226
|
+
const result = await callGuard({ operation: "health" }, 3000);
|
|
1227
|
+
const healthy = result.status === "ok" && result.isolated_key === true;
|
|
1228
|
+
emit({
|
|
1229
|
+
hookSpecificOutput: {
|
|
1230
|
+
hookEventName: eventName,
|
|
1231
|
+
additionalContext: startupMessage(eventName, healthy)
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
} catch (_) {
|
|
1235
|
+
emit({
|
|
1236
|
+
hookSpecificOutput: {
|
|
1237
|
+
hookEventName: eventName,
|
|
1238
|
+
additionalContext: startupMessage(eventName, false)
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
async function sessionStart(input) {
|
|
1245
|
+
try {
|
|
1246
|
+
await beginSessionEpoch(input);
|
|
1247
|
+
} catch (_) {
|
|
1248
|
+
emit({
|
|
1249
|
+
hookSpecificOutput: {
|
|
1250
|
+
hookEventName: "SessionStart",
|
|
1251
|
+
additionalContext: "This session is protected by mandatory BLUN Translate Native, but its delivery epoch could not be renewed. Fail closed: do not finish or deliver natural-language output until protected session state is repaired and SessionStart succeeds."
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
return startupContext("SessionStart");
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
async function subagentStart() {
|
|
1260
|
+
return startupContext("SubagentStart");
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function promptBoundary(input) {
|
|
1264
|
+
try {
|
|
1265
|
+
invalidateSessionRecords(input);
|
|
1266
|
+
} catch (_) {
|
|
1267
|
+
emit(blocked("BLUN Language Guard could not invalidate release state from the prior turn. The prompt is blocked to prevent cross-turn receipt reuse."));
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
const policyInstruction = hostPolicyInstruction();
|
|
1271
|
+
if (policyInstruction) {
|
|
1272
|
+
emit({
|
|
1273
|
+
hookSpecificOutput: {
|
|
1274
|
+
hookEventName: "UserPromptSubmit",
|
|
1275
|
+
additionalContext: policyInstruction,
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
async function stopFailure(input) {
|
|
1282
|
+
try {
|
|
1283
|
+
await beginSessionEpoch(input);
|
|
1284
|
+
} catch (_) {
|
|
1285
|
+
// beginSessionEpoch removes the old local marker before service registration.
|
|
1286
|
+
// Claude ignores StopFailure output and exit status, so any incomplete
|
|
1287
|
+
// rotation remains silent and fail-closed until SessionStart repairs it.
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
async function sessionEnd(input) {
|
|
1292
|
+
let session;
|
|
1293
|
+
try {
|
|
1294
|
+
session = hookIdentity(input).session;
|
|
1295
|
+
} catch (_) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
let previousEpoch = "";
|
|
1299
|
+
try {
|
|
1300
|
+
const current = readSessionEpoch(input);
|
|
1301
|
+
previousEpoch = current.epoch;
|
|
1302
|
+
removeExactRecord(current.destination, current.fileIdentity);
|
|
1303
|
+
} catch (_) {}
|
|
1304
|
+
try { invalidateSessionRecords(input); } catch (_) {}
|
|
1305
|
+
if (!previousEpoch) return;
|
|
1306
|
+
try {
|
|
1307
|
+
await callGuard({
|
|
1308
|
+
operation: "retire_session_epoch",
|
|
1309
|
+
session_id: session,
|
|
1310
|
+
session_epoch: previousEpoch
|
|
1311
|
+
}, 700);
|
|
1312
|
+
} catch (_) {
|
|
1313
|
+
// SessionEnd cannot block termination. Local authority is already gone;
|
|
1314
|
+
// a later SessionStart must establish a fresh service-authoritative epoch.
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function invalidateReleaseState(input, failureReason) {
|
|
1319
|
+
try {
|
|
1320
|
+
invalidateAgentRecord(input);
|
|
1321
|
+
return true;
|
|
1322
|
+
} catch (_) {
|
|
1323
|
+
emit(blocked(failureReason));
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function rejectRelease(input, reason) {
|
|
1329
|
+
if (!invalidateReleaseState(
|
|
1330
|
+
input,
|
|
1331
|
+
"BLUN Language Guard could not invalidate stale release state after rejecting a release attempt. Do not finish or deliver natural-language output until protected state is repaired."
|
|
1332
|
+
)) return;
|
|
1333
|
+
emit(blocked(reason));
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
async function postTool(input) {
|
|
1337
|
+
const toolName = String(input.tool_name || "");
|
|
1338
|
+
const purpose = toolName.endsWith("__release_translation") ? "translation"
|
|
1339
|
+
: toolName.endsWith("__release_response") ? "response" : "";
|
|
1340
|
+
if (!purpose) return;
|
|
1341
|
+
const { session, agent } = hookIdentity(input);
|
|
1342
|
+
if (!invalidateReleaseState(
|
|
1343
|
+
input,
|
|
1344
|
+
"BLUN Language Guard could not clear the prior release before processing a new attempt. The new receipt is not trusted; repair protected state and release the exact final text again."
|
|
1345
|
+
)) return;
|
|
1346
|
+
let sessionEpoch;
|
|
1347
|
+
try {
|
|
1348
|
+
sessionEpoch = readSessionEpoch(input).epoch;
|
|
1349
|
+
} catch (_) {
|
|
1350
|
+
rejectRelease(input, "BLUN Language Guard has no valid epoch for this session. Start or resume the session again, then release the exact final text with a fresh receipt.");
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
const args = input.tool_input;
|
|
1354
|
+
const release = findRelease(input.tool_response);
|
|
1355
|
+
if (!args || typeof args !== "object" || !release) {
|
|
1356
|
+
rejectRelease(input, "BLUN Language Guard returned no usable release receipt. Correct the finding and call the proper release tool again.");
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const target = typeof args.target_text === "string" ? args.target_text : "";
|
|
1360
|
+
const source = purpose === "translation" && typeof args.source_text === "string" ? args.source_text : "";
|
|
1361
|
+
const language = typeof args.language === "string" ? args.language : "";
|
|
1362
|
+
let policy;
|
|
1363
|
+
try {
|
|
1364
|
+
policy = hostReleasePolicy();
|
|
1365
|
+
} catch (_) {
|
|
1366
|
+
rejectRelease(input, "The trusted host release policy is invalid. Repair it before releasing or delivering natural-language output.");
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
if (policy?.taskKind && policy.taskKind !== purpose) {
|
|
1370
|
+
const required = policy.taskKind === "translation" ? "release_translation" : "release_response";
|
|
1371
|
+
rejectRelease(input, `The trusted host requires ${required} for this turn. Call that release tool with a fresh candidate.`);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
if (policy?.language && policy.language !== language) {
|
|
1375
|
+
rejectRelease(input, `The release used the wrong language tag. Retry with language exactly ${JSON.stringify(policy.language)}.`);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
const request = {
|
|
1379
|
+
operation: "authorize_delivery",
|
|
1380
|
+
task_kind: purpose,
|
|
1381
|
+
source_text: source,
|
|
1382
|
+
target_text: target,
|
|
1383
|
+
language,
|
|
1384
|
+
release_token: release.release_token,
|
|
1385
|
+
content_type: typeof args.content_type === "string" ? args.content_type : "prose",
|
|
1386
|
+
short_text_reviewed: args.short_text_reviewed === true,
|
|
1387
|
+
agent_id: agent,
|
|
1388
|
+
session_id: session,
|
|
1389
|
+
session_epoch: sessionEpoch,
|
|
1390
|
+
channel: "claude-hook"
|
|
1391
|
+
};
|
|
1392
|
+
try {
|
|
1393
|
+
const verification = await callGuard(request);
|
|
1394
|
+
if (verification.valid !== true || typeof verification.delivery_grant !== "string") {
|
|
1395
|
+
rejectRelease(input, "The isolated BLUN verifier rejected this receipt. Do not reuse it; correct and release the exact final text again.");
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
writeRecord(input, {
|
|
1399
|
+
delivery_grant: verification.delivery_grant,
|
|
1400
|
+
session_sha256: sessionHash(input),
|
|
1401
|
+
session_epoch_sha256: textHash(sessionEpoch),
|
|
1402
|
+
source_sha256: textHash(source),
|
|
1403
|
+
target_sha256: textHash(target),
|
|
1404
|
+
language,
|
|
1405
|
+
task_kind: purpose,
|
|
1406
|
+
content_type: typeof args.content_type === "string" ? args.content_type : "prose",
|
|
1407
|
+
short_text_reviewed: args.short_text_reviewed === true,
|
|
1408
|
+
channel: "claude-hook",
|
|
1409
|
+
authorized_at: Date.now()
|
|
1410
|
+
});
|
|
1411
|
+
} catch (_) {
|
|
1412
|
+
rejectRelease(input, "The isolated BLUN verifier is unavailable. Fail closed and reconnect the language guard before finishing.");
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function postToolFailure(input) {
|
|
1417
|
+
const toolName = String(input.tool_name || "");
|
|
1418
|
+
if (!toolName.endsWith("__release_response") && !toolName.endsWith("__release_translation")) return;
|
|
1419
|
+
try {
|
|
1420
|
+
invalidateAgentRecord(input);
|
|
1421
|
+
emit({
|
|
1422
|
+
decision: "block",
|
|
1423
|
+
reason: "The BLUN release tool failed, so no response is authorized. Reconnect the language guard and retry the correct release tool with the exact final text.",
|
|
1424
|
+
hookSpecificOutput: {
|
|
1425
|
+
hookEventName: "PostToolUseFailure",
|
|
1426
|
+
additionalContext: "Fail closed after this release-tool failure. Any earlier unconsumed grant for this session and agent has been invalidated. Do not finish, reuse an earlier release, or deliver unchecked natural-language text. Reconnect the BLUN Language Guard, then call release_translation with the complete source and exact target for a translation, or release_response with the exact final answer. Stop and SubagentStop remain the authoritative delivery boundary."
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
} catch (_) {
|
|
1430
|
+
emit(blocked("BLUN Language Guard could not invalidate stale release state after the release tool failed. Do not finish or deliver natural-language output until the guard and its protected state are repaired."));
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async function stop(input, expectedEvent) {
|
|
1435
|
+
if (!input || input.hook_event_name !== expectedEvent) {
|
|
1436
|
+
emit(blockedStop(input, `The BLUN hook route expected ${expectedEvent} input and cannot trust this mismatched event. Fail closed and retry through the configured host hook.`));
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (typeof input.stop_hook_active !== "boolean") {
|
|
1440
|
+
emit(malformedStopState());
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (!input || typeof input.last_assistant_message !== "string") {
|
|
1444
|
+
emit(blockedStop(input, "The BLUN hook received no valid last_assistant_message and cannot verify the actual final response. Fail closed and retry after the host supplies the documented Stop output field."));
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
let hook;
|
|
1448
|
+
try {
|
|
1449
|
+
hook = hookIdentity(input, expectedEvent === "SubagentStop" ? "required" : "forbidden");
|
|
1450
|
+
} catch (_) {
|
|
1451
|
+
emit(blockedStop(input, "The BLUN hook received no valid explicit host identity for this stop event. Fail closed and retry after the host supplies the documented identity fields."));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const target = input.last_assistant_message;
|
|
1455
|
+
const naturalLanguage = hasNaturalLanguage(target);
|
|
1456
|
+
try {
|
|
1457
|
+
const { session, agent } = hook;
|
|
1458
|
+
const { destination, record, fileIdentity } = readRecord(input);
|
|
1459
|
+
const { epoch: sessionEpoch } = readSessionEpoch(input);
|
|
1460
|
+
const fresh = record && Number.isFinite(record.authorized_at)
|
|
1461
|
+
&& Date.now() - record.authorized_at >= 0
|
|
1462
|
+
&& Date.now() - record.authorized_at <= MAX_RECORD_AGE_MS;
|
|
1463
|
+
const usable = fresh && typeof record.delivery_grant === "string"
|
|
1464
|
+
&& record.session_epoch_sha256 === textHash(sessionEpoch);
|
|
1465
|
+
if (record) removeExactRecord(destination, fileIdentity);
|
|
1466
|
+
if (usable) {
|
|
1467
|
+
try {
|
|
1468
|
+
const result = await callGuard({
|
|
1469
|
+
operation: "consume_delivery",
|
|
1470
|
+
delivery_grant: record.delivery_grant,
|
|
1471
|
+
source_sha256: record.source_sha256,
|
|
1472
|
+
target_text: target,
|
|
1473
|
+
language: record.language,
|
|
1474
|
+
task_kind: record.task_kind,
|
|
1475
|
+
content_type: record.content_type,
|
|
1476
|
+
short_text_reviewed: record.short_text_reviewed === true,
|
|
1477
|
+
session_id: session,
|
|
1478
|
+
agent_id: agent,
|
|
1479
|
+
session_epoch: sessionEpoch,
|
|
1480
|
+
channel: record.channel
|
|
1481
|
+
});
|
|
1482
|
+
if (naturalLanguage && record.target_sha256 === textHash(target) && result.valid === true) return;
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
if (naturalLanguage) throw error;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
} catch (_) {
|
|
1488
|
+
if (!naturalLanguage) return;
|
|
1489
|
+
emit(blockedStop(input, "The BLUN hook could not verify its protected release state. Fail closed and call the correct release tool again."));
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (!naturalLanguage) return;
|
|
1493
|
+
emit(blockedStop(input,
|
|
1494
|
+
"The exact final response has no fresh verified BLUN receipt. If this is a translation, load translate-native and call release_translation with the complete source and exact final target. Otherwise call release_response with the exact final answer. Do not edit the text after release."
|
|
1495
|
+
));
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
async function readInput() {
|
|
1499
|
+
const chunks = [];
|
|
1500
|
+
let size = 0;
|
|
1501
|
+
for await (const chunk of process.stdin) {
|
|
1502
|
+
size += chunk.length;
|
|
1503
|
+
if (size > MAX_INPUT_BYTES) throw new Error("Claude hook input is too large");
|
|
1504
|
+
chunks.push(chunk);
|
|
1505
|
+
}
|
|
1506
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8").replace(/^\uFEFF/, ""));
|
|
1507
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Claude hook input must be an object");
|
|
1508
|
+
return parsed;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
async function main() {
|
|
1512
|
+
const mode = process.argv[2];
|
|
1513
|
+
const input = await readInput();
|
|
1514
|
+
currentHookInput = input;
|
|
1515
|
+
if (mode === "session-start") return sessionStart(input);
|
|
1516
|
+
if (mode === "subagent-start") return subagentStart(input);
|
|
1517
|
+
if (mode === "prompt-boundary") return promptBoundary(input);
|
|
1518
|
+
if (mode === "stop-failure") return stopFailure(input);
|
|
1519
|
+
if (mode === "session-end") return sessionEnd(input);
|
|
1520
|
+
if (mode === "pre-delivery") return preDelivery(input);
|
|
1521
|
+
if (mode === "pre-tool") return preTool(input);
|
|
1522
|
+
if (mode === "post-tool") return postTool(input);
|
|
1523
|
+
if (mode === "post-tool-failure") return postToolFailure(input);
|
|
1524
|
+
if (mode === "stop") return stop(input, "Stop");
|
|
1525
|
+
if (mode === "subagent-stop") return stop(input, "SubagentStop");
|
|
1526
|
+
throw new Error("unknown Claude hook mode");
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (require.main === module) {
|
|
1530
|
+
main().catch(() => {
|
|
1531
|
+
emit(blockedStop(currentHookInput, "BLUN language hook failed closed. Repair or reconnect the guard and retry the exact release."));
|
|
1532
|
+
process.exitCode = 0;
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
module.exports = { beginSessionEpoch, blockedStop, canonicalText, findRelease, hasNaturalLanguage, hookIdentity, hostReleasePolicy, invalidateAgentRecord, invalidateSessionRecords, isDirectTelegramDeliveryTool, postToolFailure, preDelivery, preTool, readProtectedDeliveryPolicy, readProtectedRecord, readProtectedServiceToken, readSessionEpoch, removeExactRecord, sessionEnd, sessionHash, stopFailure, textHash, writeRecord };
|