gentle-pi 4.0.0 → 4.2.0
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/README.md +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +29 -25
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase.ts +1 -1
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +7 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +79 -32
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +52 -1
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +6 -5
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +5 -4
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-risk-assessment.test.ts +109 -33
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +27 -1
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge to the owned native PulseAudio worker. Original MIT (repo-licensed).
|
|
3
|
+
*
|
|
4
|
+
* Design contract: this module performs no IO on import or construction. It has
|
|
5
|
+
* no module-level import of the stream, audio, context, config-owner or
|
|
6
|
+
* extension graph (an `audio -> bridge` import is fine; the reverse is not). The
|
|
7
|
+
* only execution surface is an explicit `probe()`/`play()` that spawns the owned
|
|
8
|
+
* worker as a Node child with fixed literal flags and a scrubbed environment.
|
|
9
|
+
* Linux only; macOS/Windows never spawn. No real audio, no real Pulse stream
|
|
10
|
+
* beyond the worker's own fake-test sockets; there is no physical-playback claim.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import type { EventEmitter } from "node:events";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
export const NATIVE_PULSE_SCHEMA = "gentle.audio.pulse/v1";
|
|
17
|
+
export const NATIVE_PULSE_WORKER_URL = new URL("./notification-pulse-worker.ts", import.meta.url);
|
|
18
|
+
const FIXED_NODE_FLAGS = ["--experimental-strip-types", "--max-old-space-size=32"] as const;
|
|
19
|
+
const MAX_STDOUT = 1024;
|
|
20
|
+
const MAX_STDERR = 4096;
|
|
21
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 1200;
|
|
22
|
+
/** 10 s maximum validated duration plus a 1 s termination margin; must outlive every accepted sound. */
|
|
23
|
+
const DEFAULT_PLAY_TIMEOUT_MS = 11000;
|
|
24
|
+
|
|
25
|
+
export interface NativePulseProbeResult { readonly available: boolean; readonly formats: readonly ("wav")[]; }
|
|
26
|
+
export interface NativePulseSpawnOptions {
|
|
27
|
+
readonly shell: false;
|
|
28
|
+
readonly windowsHide: true;
|
|
29
|
+
readonly detached: false;
|
|
30
|
+
readonly stdio: readonly ["ignore", "pipe", "pipe"];
|
|
31
|
+
readonly env: NodeJS.ProcessEnv;
|
|
32
|
+
}
|
|
33
|
+
export interface NativePulseChild {
|
|
34
|
+
stdout: EventEmitter | null;
|
|
35
|
+
stderr: EventEmitter | null;
|
|
36
|
+
kill(signal: string): boolean;
|
|
37
|
+
once(event: string, listener: (...args: unknown[]) => void): EventEmitter;
|
|
38
|
+
}
|
|
39
|
+
export interface NativePulseOptions {
|
|
40
|
+
platform?: string;
|
|
41
|
+
spawn?: (executable: string, args: string[], options: NativePulseSpawnOptions) => NativePulseChild;
|
|
42
|
+
workerPath?: string;
|
|
43
|
+
env?: NodeJS.ProcessEnv;
|
|
44
|
+
probeTimeoutMs?: number;
|
|
45
|
+
playTimeoutMs?: number;
|
|
46
|
+
setTimeout?: (callback: () => void, ms: number) => ReturnType<typeof setTimeout> | number;
|
|
47
|
+
clearTimeout?: (handle: ReturnType<typeof setTimeout> | number) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface Envelope { schema?: unknown; ok?: unknown; available?: unknown; formats?: unknown; played?: unknown; }
|
|
51
|
+
interface RunResult { readonly stdout: string; readonly stderr: string; readonly aborted: boolean; readonly timedOut: boolean; }
|
|
52
|
+
|
|
53
|
+
/** Distinct from other native failures so the owner can treat a false permit as "not started". */
|
|
54
|
+
export class NativePulseNotPermittedError extends Error {
|
|
55
|
+
constructor() { super("Native pulse: playback not permitted"); this.name = "NativePulseNotPermittedError"; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function unrefTimer(handle: ReturnType<typeof setTimeout> | number): void { (handle as { unref?: () => void }).unref?.(); }
|
|
59
|
+
function abortError(): Error { const error = new Error("Native pulse: aborted"); error.name = "AbortError"; return error; }
|
|
60
|
+
function defaultSpawn(executable: string, args: string[], options: NativePulseSpawnOptions): NativePulseChild {
|
|
61
|
+
const stdio: ("ignore" | "pipe")[] = ["ignore", "pipe", "pipe"];
|
|
62
|
+
// SAFETY: Node's ChildProcess exposes stdout/stderr/once/kill compatible with the narrower NativePulseChild interface; the cast only bridges that structural narrowing.
|
|
63
|
+
return spawn(executable, args, { ...options, stdio }) as unknown as NativePulseChild;
|
|
64
|
+
}
|
|
65
|
+
function parseEnvelope(stdout: string): Envelope | undefined {
|
|
66
|
+
const text = stdout.trim();
|
|
67
|
+
if (text.length === 0 || text.includes("\n")) return undefined;
|
|
68
|
+
try {
|
|
69
|
+
const value: unknown = JSON.parse(text);
|
|
70
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
71
|
+
const record = value as Envelope;
|
|
72
|
+
return record.schema === NATIVE_PULSE_SCHEMA ? record : undefined;
|
|
73
|
+
} catch { return undefined; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class NativePulsePlayer {
|
|
77
|
+
private readonly options: NativePulseOptions;
|
|
78
|
+
private readonly setTimer: NonNullable<NativePulseOptions["setTimeout"]>;
|
|
79
|
+
private readonly clearTimer: NonNullable<NativePulseOptions["clearTimeout"]>;
|
|
80
|
+
private formats: readonly ("wav")[] = [];
|
|
81
|
+
|
|
82
|
+
constructor(options: NativePulseOptions = {}) {
|
|
83
|
+
this.options = { ...options };
|
|
84
|
+
this.setTimer = options.setTimeout ?? ((callback, ms) => setTimeout(callback, ms));
|
|
85
|
+
this.clearTimer = options.clearTimeout ?? (handle => clearTimeout(handle as ReturnType<typeof setTimeout>));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
supportsTarget(): boolean { return (this.options.platform ?? process.platform) === "linux"; }
|
|
89
|
+
getNativeFormats(): readonly ("wav")[] { return this.formats; }
|
|
90
|
+
|
|
91
|
+
private workerPath(): string { return this.options.workerPath ?? fileURLToPath(NATIVE_PULSE_WORKER_URL); }
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Child env is an allowlist: only Pulse Unix/runtime/localization vars survive;
|
|
95
|
+
* API keys, cloud credentials, debug overlays, NODE_OPTIONS/NODE_PATH and PATH
|
|
96
|
+
* are dropped. process.execPath and the snapshot are absolute and TCP is denied,
|
|
97
|
+
* so no PATH resolution is needed.
|
|
98
|
+
*/
|
|
99
|
+
private childEnv(): NodeJS.ProcessEnv {
|
|
100
|
+
const source = this.options.env ?? process.env;
|
|
101
|
+
const env: NodeJS.ProcessEnv = {};
|
|
102
|
+
for (const key of ["HOME", "XDG_CONFIG_HOME", "XDG_RUNTIME_DIR", "PULSE_SERVER", "PULSE_COOKIE", "LANG", "LC_ALL", "LC_CTYPE"])
|
|
103
|
+
if (source[key] !== undefined) env[key] = source[key];
|
|
104
|
+
env.NODE_NO_WARNINGS = "1";
|
|
105
|
+
env.GENTLE_PI_AGENTS_CHILD = "1";
|
|
106
|
+
return env;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Spawns synchronously; resolves ONLY on the child `close` event (never exit/error/kill). */
|
|
110
|
+
private run(mode: "probe" | "play", snapshot: string | undefined, signal: AbortSignal | undefined, timeoutMs: number): Promise<RunResult> {
|
|
111
|
+
return new Promise<RunResult>(resolve => {
|
|
112
|
+
const args = [...FIXED_NODE_FLAGS, this.workerPath(), mode];
|
|
113
|
+
if (snapshot !== undefined) args.push(snapshot);
|
|
114
|
+
const child = (this.options.spawn ?? defaultSpawn)(process.execPath, args, {
|
|
115
|
+
shell: false, windowsHide: true, detached: false, stdio: ["ignore", "pipe", "pipe"], env: this.childEnv(),
|
|
116
|
+
});
|
|
117
|
+
let stdout = ""; let stderr = ""; let killed = false; let aborted = false; let timedOut = false;
|
|
118
|
+
const kill = () => { if (!killed) { killed = true; try { child.kill("SIGKILL"); } catch { /* keep waiting for close */ } } };
|
|
119
|
+
const cleanup = () => { this.clearTimer(timer); signal?.removeEventListener("abort", onAbort); child.stdout?.removeListener("data", onStdout); child.stderr?.removeListener("data", onStderr); };
|
|
120
|
+
const onStdout = (chunk: unknown) => { stdout += String(chunk); if (stdout.length > MAX_STDOUT) kill(); };
|
|
121
|
+
const onStderr = (chunk: unknown) => { if (stderr.length < MAX_STDERR) stderr += String(chunk).slice(0, MAX_STDERR - stderr.length); };
|
|
122
|
+
const onAbort = () => { aborted = true; kill(); };
|
|
123
|
+
const timer = this.setTimer(() => { timedOut = true; kill(); }, timeoutMs);
|
|
124
|
+
unrefTimer(timer);
|
|
125
|
+
child.stdout?.on("data", onStdout);
|
|
126
|
+
child.stderr?.on("data", onStderr);
|
|
127
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
128
|
+
child.once("close", () => { cleanup(); resolve({ stdout, stderr, aborted, timedOut }); });
|
|
129
|
+
child.once("error", () => { /* Node emits close after spawn errors; keep waiting */ });
|
|
130
|
+
if (signal?.aborted) onAbort();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private parseProbe(stdout: string): NativePulseProbeResult {
|
|
135
|
+
const envelope = parseEnvelope(stdout);
|
|
136
|
+
if (!envelope || envelope.ok !== true || typeof envelope.available !== "boolean" || !Array.isArray(envelope.formats))
|
|
137
|
+
return { available: false, formats: [] };
|
|
138
|
+
const hasWav = envelope.formats.some(value => value === "wav");
|
|
139
|
+
return envelope.available && hasWav ? { available: true, formats: ["wav"] } : { available: false, formats: [] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Offline/malformed/unsupported probe resolves unavailable; it never throws private IPC. */
|
|
143
|
+
async probe(signal?: AbortSignal): Promise<NativePulseProbeResult> {
|
|
144
|
+
if (!this.supportsTarget() || signal?.aborted) { this.formats = []; return { available: false, formats: [] }; }
|
|
145
|
+
const result = await this.run("probe", undefined, signal, this.options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
|
|
146
|
+
const parsed = result.aborted || result.timedOut ? { available: false, formats: [] as const } : this.parseProbe(result.stdout);
|
|
147
|
+
this.formats = parsed.available ? ["wav"] : [];
|
|
148
|
+
return parsed;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** `gate` (permit or predicate) runs synchronously before spawn; false means no child at all. */
|
|
152
|
+
async play(snapshot: string, signal?: AbortSignal, gate?: (() => boolean) | { permit(): boolean }): Promise<void> {
|
|
153
|
+
if (!this.supportsTarget()) throw new Error("Native pulse: unsupported platform");
|
|
154
|
+
if (signal?.aborted) throw abortError();
|
|
155
|
+
const permitted = typeof gate === "function" ? gate() : gate?.permit();
|
|
156
|
+
if (permitted === false) throw new NativePulseNotPermittedError();
|
|
157
|
+
const result = await this.run("play", snapshot, signal, this.options.playTimeoutMs ?? DEFAULT_PLAY_TIMEOUT_MS);
|
|
158
|
+
if (result.aborted) throw abortError();
|
|
159
|
+
const envelope = parseEnvelope(result.stdout);
|
|
160
|
+
if (result.timedOut || !envelope || envelope.ok !== true || envelope.played !== true) throw new Error("Native pulse playback failed");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Windows WAV playback adapter. Original MIT (repo-licensed).
|
|
3
|
+
*
|
|
4
|
+
* Design contract: construction and import perform no IO. The only execution
|
|
5
|
+
* surface is an explicit `probe()`/`play()` that spawns the fixed system
|
|
6
|
+
* PowerShell host with a fixed, trusted `-EncodedCommand` script and a scrubbed
|
|
7
|
+
* environment. The snapshot path is never interpolated into the command: it is
|
|
8
|
+
* base64-encoded and decoded inside the script, so no snapshot byte can become
|
|
9
|
+
* command text. Native Windows and the derived WSL Windows-interop target only;
|
|
10
|
+
* plain macOS/Linux never spawn. It is now the default native route: the pure,
|
|
11
|
+
* IO-free `createDefaultNotificationBackend` selects it for `win32` and for a
|
|
12
|
+
* `linux` process with a valid WSL interop env (P3), and a validated WSL
|
|
13
|
+
* snapshot is mapped to the derived `\\wsl.localhost\<distro>\...` UNC. No
|
|
14
|
+
* physical playback is claimed here.
|
|
15
|
+
*
|
|
16
|
+
* P2 (implemented): `supportsTarget()` also accepts the WSL Windows-interop
|
|
17
|
+
* target (Linux platform + a valid `WSL_DISTRO_NAME` + `/run/WSL/*_interop`).
|
|
18
|
+
* The fixed host is `/mnt/c/.../powershell.exe`, never resolved through `PATH`,
|
|
19
|
+
* `SystemRoot` or a runtime WSL path translation; a non-`/mnt/c` mount fails closed.
|
|
20
|
+
* `play()` maps the validated POSIX snapshot to the single derived
|
|
21
|
+
* `\\wsl.localhost\<distro>\...` UNC form; an externally supplied UNC path is
|
|
22
|
+
* never admitted and `isWindowsAbsolutePath()` still rejects every UNC path.
|
|
23
|
+
*/
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import type { EventEmitter } from "node:events";
|
|
26
|
+
import { constants } from "node:fs";
|
|
27
|
+
import { access } from "node:fs/promises";
|
|
28
|
+
import { NativePulseNotPermittedError } from "./notification-audio-native.ts";
|
|
29
|
+
|
|
30
|
+
export const NATIVE_WINDOWS_SCHEMA = "gentle.audio.windows/v1";
|
|
31
|
+
/**
|
|
32
|
+
* Trusted, frozen PowerShell executable. This literal intentionally duplicates
|
|
33
|
+
* `FIXED_WINDOWS_POWERSHELL` in `lib/windows-session-transport.ts`: importing
|
|
34
|
+
* that module would drag the agents-session-transport/SDK graph into the audio
|
|
35
|
+
* path. It is a trust anchor, never derived from `SystemRoot`, `windir`, `PATH`
|
|
36
|
+
* or config, so a hostile environment cannot redirect the executable.
|
|
37
|
+
*/
|
|
38
|
+
export const FIXED_WINDOWS_POWERSHELL_EXE = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
39
|
+
/** P1 supports only the default system root; a non-default root fails closed. */
|
|
40
|
+
const FIXED_WINDOWS_ROOT = "C:\\Windows";
|
|
41
|
+
/**
|
|
42
|
+
* Trusted WSL interop host: the fixed Windows PowerShell reached through the
|
|
43
|
+
* default `/mnt/c` automount. Like the native literal it is a trust anchor, never
|
|
44
|
+
* derived from `PATH`, an injected executable or a runtime WSL path translation. A
|
|
45
|
+
* non-`/mnt/c` mount or a custom Windows root is not reachable here and fails
|
|
46
|
+
* closed at the `probe()` access check (documented P2 limitation).
|
|
47
|
+
*/
|
|
48
|
+
export const WSL_WINDOWS_POWERSHELL_EXE = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
|
|
49
|
+
/** Fixed UNC host that maps a WSL distro filesystem into the Windows namespace. */
|
|
50
|
+
export const WSL_UNC_HOST = "wsl.localhost";
|
|
51
|
+
const WSL_INTEROP_PREFIX = "/run/WSL/";
|
|
52
|
+
const WSL_INTEROP_SUFFIX = "_interop";
|
|
53
|
+
/** Characters Windows forbids in a UNC share/distro component. */
|
|
54
|
+
const RESERVED_COMPONENT = /[<>:"/\\|?*]/;
|
|
55
|
+
const CONTROL_BYTES = /[\u0000-\u001f\u007f]/;
|
|
56
|
+
const FIXED_FLAGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"] as const;
|
|
57
|
+
/** Windows runtime vars only; PATH, NODE_OPTIONS/NODE_PATH, debug and credentials are dropped. */
|
|
58
|
+
const WINDOWS_ENV_ALLOWLIST = ["TEMP", "TMP", "USERPROFILE", "USERNAME", "USERDOMAIN", "HOMEDRIVE", "HOMEPATH"] as const;
|
|
59
|
+
const MAX_STDOUT = 1024;
|
|
60
|
+
const MAX_STDERR = 4096;
|
|
61
|
+
const MAX_SNAPSHOT_PATH = 4096;
|
|
62
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 1200;
|
|
63
|
+
/** 10 s maximum validated duration plus a 1 s termination margin; must outlive every accepted sound. */
|
|
64
|
+
const DEFAULT_PLAY_TIMEOUT_MS = 11000;
|
|
65
|
+
|
|
66
|
+
/** A fixed probe script: it only proves the .NET SoundPlayer type is loadable and prints one line. */
|
|
67
|
+
const PROBE_SCRIPT = [
|
|
68
|
+
"$ErrorActionPreference = 'Stop'",
|
|
69
|
+
"try {",
|
|
70
|
+
" $type = [System.Media.SoundPlayer]",
|
|
71
|
+
" if ($null -eq $type) { throw 'unavailable' }",
|
|
72
|
+
` [Console]::Out.WriteLine('{"schema":"${NATIVE_WINDOWS_SCHEMA}","ok":true,"available":true,"formats":["wav"]}')`,
|
|
73
|
+
"} catch {",
|
|
74
|
+
` [Console]::Out.WriteLine('{"schema":"${NATIVE_WINDOWS_SCHEMA}","ok":false,"available":false,"formats":[]}')`,
|
|
75
|
+
"}",
|
|
76
|
+
].join("\n");
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A fixed play script whose only dynamic value is a base64 string literal,
|
|
80
|
+
* decoded at runtime. The decoded path is never command text. The snapshot is
|
|
81
|
+
* read into a bounded byte buffer, the file handle is released before playback,
|
|
82
|
+
* and the bytes are played through a `MemoryStream`-backed `SoundPlayer`.
|
|
83
|
+
* Forcing the memory constructor is deliberate: the file-URI
|
|
84
|
+
* `SoundPlayer(string)` path reaches a WinMM `PlaySound(fileName)` branch that
|
|
85
|
+
* can report success without audible output, while the memory constructor drives
|
|
86
|
+
* the documented in-memory branch. Every failure prints the same schema-valid
|
|
87
|
+
* `played:false` line; no raw path byte enters the command.
|
|
88
|
+
*/
|
|
89
|
+
function playScript(encodedPath: string): string {
|
|
90
|
+
return [
|
|
91
|
+
"$ErrorActionPreference = 'Stop'",
|
|
92
|
+
"$file = $null",
|
|
93
|
+
"$memory = $null",
|
|
94
|
+
"$player = $null",
|
|
95
|
+
"try {",
|
|
96
|
+
` $bytes = [Convert]::FromBase64String('${encodedPath}')`,
|
|
97
|
+
" $path = [Text.Encoding]::UTF8.GetString($bytes)",
|
|
98
|
+
" $file = [IO.File]::Open($path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)",
|
|
99
|
+
" $count = $file.Length",
|
|
100
|
+
" if ($count -lt 44 -or $count -gt 2097152) { throw 'snapshot size' }",
|
|
101
|
+
" $raw = [byte[]]::new([int]$count)",
|
|
102
|
+
" $read = 0",
|
|
103
|
+
" while ($read -lt $count) {",
|
|
104
|
+
" $step = $file.Read($raw, $read, $count - $read)",
|
|
105
|
+
" if ($step -le 0) { throw 'snapshot short read' }",
|
|
106
|
+
" $read += $step",
|
|
107
|
+
" }",
|
|
108
|
+
" if ($file.ReadByte() -ne -1) { throw 'snapshot grew' }",
|
|
109
|
+
" $file.Close()",
|
|
110
|
+
" $file = $null",
|
|
111
|
+
" $memory = [IO.MemoryStream]::new($raw)",
|
|
112
|
+
" $player = [System.Media.SoundPlayer]::new($memory)",
|
|
113
|
+
" $player.Load()",
|
|
114
|
+
" $player.PlaySync()",
|
|
115
|
+
` [Console]::Out.WriteLine('{"schema":"${NATIVE_WINDOWS_SCHEMA}","ok":true,"played":true}')`,
|
|
116
|
+
"} catch {",
|
|
117
|
+
` [Console]::Out.WriteLine('{"schema":"${NATIVE_WINDOWS_SCHEMA}","ok":false,"played":false}')`,
|
|
118
|
+
"} finally {",
|
|
119
|
+
" if ($null -ne $player) { $player.Dispose() }",
|
|
120
|
+
" if ($null -ne $memory) { $memory.Dispose() }",
|
|
121
|
+
" if ($null -ne $file) { $file.Dispose() }",
|
|
122
|
+
"}",
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface NativeWindowsProbeResult { readonly available: boolean; readonly formats: readonly ("wav")[]; }
|
|
127
|
+
export interface NativeWindowsSpawnOptions {
|
|
128
|
+
readonly shell: false;
|
|
129
|
+
readonly windowsHide: true;
|
|
130
|
+
readonly detached: false;
|
|
131
|
+
readonly stdio: readonly ["ignore", "pipe", "pipe"];
|
|
132
|
+
readonly env: NodeJS.ProcessEnv;
|
|
133
|
+
}
|
|
134
|
+
export interface NativeWindowsChild {
|
|
135
|
+
stdout: EventEmitter | null;
|
|
136
|
+
stderr: EventEmitter | null;
|
|
137
|
+
kill(signal: string): boolean;
|
|
138
|
+
once(event: string, listener: (...args: unknown[]) => void): EventEmitter;
|
|
139
|
+
}
|
|
140
|
+
export interface NativeWindowsOptions {
|
|
141
|
+
platform?: string;
|
|
142
|
+
spawn?: (executable: string, args: string[], options: NativeWindowsSpawnOptions) => NativeWindowsChild;
|
|
143
|
+
env?: NodeJS.ProcessEnv;
|
|
144
|
+
executableAvailable?(path: string): Promise<boolean>;
|
|
145
|
+
probeTimeoutMs?: number;
|
|
146
|
+
playTimeoutMs?: number;
|
|
147
|
+
setTimeout?: (callback: () => void, ms: number) => ReturnType<typeof setTimeout> | number;
|
|
148
|
+
clearTimeout?: (handle: ReturnType<typeof setTimeout> | number) => void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface Envelope { schema?: unknown; ok?: unknown; available?: unknown; formats?: unknown; played?: unknown; }
|
|
152
|
+
interface RunResult { readonly stdout: string; readonly stderr: string; readonly aborted: boolean; readonly timedOut: boolean; readonly code: number | null; readonly overflowed: boolean; }
|
|
153
|
+
/** The native Windows host or the derived WSL Windows-interop host; nothing else is supported. */
|
|
154
|
+
type ResolvedTarget = { readonly kind: "win32" } | { readonly kind: "wsl"; readonly distro: string };
|
|
155
|
+
|
|
156
|
+
/** Normalize a stream chunk to bytes so the byte cap and the unicode-safe decode stay exact. */
|
|
157
|
+
function toBuffer(chunk: unknown): Buffer {
|
|
158
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
159
|
+
if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
160
|
+
return Buffer.from(String(chunk), "utf8");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function unrefTimer(handle: ReturnType<typeof setTimeout> | number): void { (handle as { unref?: () => void }).unref?.(); }
|
|
164
|
+
function abortError(): Error { const error = new Error("Native Windows: aborted"); error.name = "AbortError"; return error; }
|
|
165
|
+
function privateFailure(): Error { return new Error("Native Windows: playback failed"); }
|
|
166
|
+
function defaultSpawn(executable: string, args: string[], options: NativeWindowsSpawnOptions): NativeWindowsChild {
|
|
167
|
+
const stdio: ("ignore" | "pipe")[] = ["ignore", "pipe", "pipe"];
|
|
168
|
+
// SAFETY: Node's ChildProcess exposes stdout/stderr/once/kill compatible with the narrower NativeWindowsChild interface; the cast only bridges that structural narrowing.
|
|
169
|
+
return spawn(executable, args, { ...options, stdio }) as unknown as NativeWindowsChild;
|
|
170
|
+
}
|
|
171
|
+
function parseEnvelope(stdout: string): Envelope | undefined {
|
|
172
|
+
const text = stdout.trim();
|
|
173
|
+
if (text.length === 0 || text.includes("\n")) return undefined;
|
|
174
|
+
try {
|
|
175
|
+
const value: unknown = JSON.parse(text);
|
|
176
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
177
|
+
const record = value as Envelope;
|
|
178
|
+
return record.schema === NATIVE_WINDOWS_SCHEMA ? record : undefined;
|
|
179
|
+
} catch { return undefined; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* A distro/UNC component may be Unicode and contain spaces, but never the
|
|
184
|
+
* reserved set, control bytes or the `.`/`..` traversal names.
|
|
185
|
+
*/
|
|
186
|
+
function isValidUncComponent(value: unknown): value is string {
|
|
187
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 256) return false;
|
|
188
|
+
if (value === "." || value === "..") return false;
|
|
189
|
+
return !CONTROL_BYTES.test(value) && !RESERVED_COMPONENT.test(value);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `/run/WSL/<id>_interop`, absolute and traversal-free; the real path is never logged. */
|
|
193
|
+
function isSafeWslInteropPath(value: unknown): value is string {
|
|
194
|
+
if (typeof value !== "string" || !value.startsWith(WSL_INTEROP_PREFIX) || !value.endsWith(WSL_INTEROP_SUFFIX)) return false;
|
|
195
|
+
if (value.length > MAX_SNAPSHOT_PATH) return false;
|
|
196
|
+
if (CONTROL_BYTES.test(value) || /[\s\\:]/.test(value)) return false;
|
|
197
|
+
if (value.includes("//")) return false;
|
|
198
|
+
for (const segment of value.split("/")) if (segment === "." || segment === "..") return false;
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Pure WSL Windows-interop detection: a valid distro name plus the interop socket path. No IO. */
|
|
203
|
+
export function isWslTarget(env: NodeJS.ProcessEnv): boolean {
|
|
204
|
+
return isValidUncComponent(env.WSL_DISTRO_NAME) && isSafeWslInteropPath(env.WSL_INTEROP);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A WSL snapshot must be a POSIX-absolute, privately owned path. Control bytes,
|
|
209
|
+
* backslashes, colons (drive/alternate-stream/URI), ambiguous `//` and `.`/`..`
|
|
210
|
+
* traversal segments are denied so the later UNC mapping cannot cross the distro
|
|
211
|
+
* share or smuggle in a Windows drive path.
|
|
212
|
+
*/
|
|
213
|
+
export function isWslPosixAbsolutePath(value: unknown): value is string {
|
|
214
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_SNAPSHOT_PATH) return false;
|
|
215
|
+
if (!value.startsWith("/")) return false;
|
|
216
|
+
if (CONTROL_BYTES.test(value) || /[\\:]/.test(value)) return false;
|
|
217
|
+
if (value.includes("//")) return false;
|
|
218
|
+
for (const segment of value.split("/")) if (segment === "." || segment === "..") return false;
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Pure mapping to the single derived UNC form `\\wsl.localhost\<distro>\...`.
|
|
224
|
+
* The caller supplies a validated POSIX snapshot, never an externally provided
|
|
225
|
+
* UNC path, and the distro name is re-validated as a UNC component.
|
|
226
|
+
*/
|
|
227
|
+
export function toWslUncPath(distro: unknown, posixPath: unknown): string | undefined {
|
|
228
|
+
if (!isValidUncComponent(distro) || !isWslPosixAbsolutePath(posixPath)) return undefined;
|
|
229
|
+
const unc = `\\\\${WSL_UNC_HOST}\\${distro}\\${posixPath.slice(1).split("/").join("\\")}`;
|
|
230
|
+
return unc.length <= MAX_SNAPSHOT_PATH ? unc : undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Only a drive-absolute Windows path reaches the native script. URI schemes,
|
|
235
|
+
* drive-relative paths, control bytes, alternate data streams and every UNC
|
|
236
|
+
* share are denied; WSL snapshots are POSIX paths mapped by `toWslUncPath`.
|
|
237
|
+
*/
|
|
238
|
+
export function isWindowsAbsolutePath(value: unknown): value is string {
|
|
239
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_SNAPSHOT_PATH) return false;
|
|
240
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) return false;
|
|
241
|
+
if (value.startsWith("\\\\") || value.startsWith("//")) return false;
|
|
242
|
+
if (!/^[A-Za-z]:\\/.test(value)) return false;
|
|
243
|
+
if (value.indexOf(":", 2) !== -1) return false; // no URI scheme and no alternate data stream
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Structural equivalent of the routing module's `NativeAudioBackend` (kept local
|
|
249
|
+
* to avoid an `audio -> windows` import cycle). It reuses the shared
|
|
250
|
+
* `NativePulseNotPermittedError` so the outer owner can treat a false permit as
|
|
251
|
+
* "not started" exactly like the Linux bridge.
|
|
252
|
+
*/
|
|
253
|
+
export class NativeWindowsPlayer {
|
|
254
|
+
private readonly options: NativeWindowsOptions;
|
|
255
|
+
private readonly setTimer: NonNullable<NativeWindowsOptions["setTimeout"]>;
|
|
256
|
+
private readonly clearTimer: NonNullable<NativeWindowsOptions["clearTimeout"]>;
|
|
257
|
+
private formats: readonly ("wav")[] = [];
|
|
258
|
+
|
|
259
|
+
constructor(options: NativeWindowsOptions = {}) {
|
|
260
|
+
this.options = { ...options };
|
|
261
|
+
this.setTimer = options.setTimeout ?? ((callback, ms) => setTimeout(callback, ms));
|
|
262
|
+
this.clearTimer = options.clearTimeout ?? (handle => clearTimeout(handle as ReturnType<typeof setTimeout>));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
supportsTarget(): boolean { return this.resolveTarget() !== undefined; }
|
|
266
|
+
getNativeFormats(): readonly ("wav")[] { return this.formats; }
|
|
267
|
+
|
|
268
|
+
/** Pure, IO-free target resolution from the injected platform/env. */
|
|
269
|
+
private resolveTarget(): ResolvedTarget | undefined {
|
|
270
|
+
const platform = this.options.platform ?? process.platform;
|
|
271
|
+
if (platform === "win32") return { kind: "win32" };
|
|
272
|
+
const source = this.options.env ?? process.env;
|
|
273
|
+
if (platform === "linux" && isWslTarget(source)) return { kind: "wsl", distro: source.WSL_DISTRO_NAME! };
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Fixed trust anchor per target: the native literal or the fixed `/mnt/c` interop literal. */
|
|
278
|
+
private executablePath(target: ResolvedTarget): string {
|
|
279
|
+
return target.kind === "wsl" ? WSL_WINDOWS_POWERSHELL_EXE : FIXED_WINDOWS_POWERSHELL_EXE;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Child env is a fixed allowlist: only Windows runtime/localization vars
|
|
284
|
+
* survive, `SystemRoot`/`windir` are forced to the trusted default root, and
|
|
285
|
+
* PATH, NODE_OPTIONS/NODE_PATH, debug overlays and every credential are
|
|
286
|
+
* dropped. The WSL interop target adds only `WSL_INTEROP`/`WSL_DISTRO_NAME`,
|
|
287
|
+
* which a scrubbed child needs so the Windows process can reach `/init`. No
|
|
288
|
+
* snapshot travels through the environment, so no `WSLENV` entry is needed or
|
|
289
|
+
* preserved.
|
|
290
|
+
*/
|
|
291
|
+
private childEnv(target: ResolvedTarget): NodeJS.ProcessEnv {
|
|
292
|
+
const source = this.options.env ?? process.env;
|
|
293
|
+
const env: NodeJS.ProcessEnv = {};
|
|
294
|
+
for (const key of WINDOWS_ENV_ALLOWLIST)
|
|
295
|
+
if (source[key] !== undefined) env[key] = source[key];
|
|
296
|
+
env.SystemRoot = FIXED_WINDOWS_ROOT;
|
|
297
|
+
env.windir = FIXED_WINDOWS_ROOT;
|
|
298
|
+
if (target.kind === "wsl") {
|
|
299
|
+
env.WSL_INTEROP = source.WSL_INTEROP;
|
|
300
|
+
env.WSL_DISTRO_NAME = source.WSL_DISTRO_NAME;
|
|
301
|
+
}
|
|
302
|
+
return env;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Spawns synchronously; resolves ONLY on the child `close` event (never exit/error/kill). */
|
|
306
|
+
private run(mode: "probe" | "play", target: ResolvedTarget, snapshot: string | undefined, signal: AbortSignal | undefined, timeoutMs: number): Promise<RunResult> {
|
|
307
|
+
return new Promise<RunResult>((resolve, reject) => {
|
|
308
|
+
const script = mode === "probe" ? PROBE_SCRIPT : playScript(Buffer.from(snapshot!, "utf8").toString("base64"));
|
|
309
|
+
const args = [...FIXED_FLAGS, Buffer.from(script, "utf16le").toString("base64")];
|
|
310
|
+
let child: NativeWindowsChild;
|
|
311
|
+
try {
|
|
312
|
+
child = (this.options.spawn ?? defaultSpawn)(this.executablePath(target), args, {
|
|
313
|
+
shell: false, windowsHide: true, detached: false, stdio: ["ignore", "pipe", "pipe"], env: this.childEnv(target),
|
|
314
|
+
});
|
|
315
|
+
} catch { reject(privateFailure()); return; }
|
|
316
|
+
let stdoutChunks: Buffer[] = []; let stderrChunks: Buffer[] = [];
|
|
317
|
+
let stdoutBytes = 0; let stderrBytes = 0; let overflowed = false;
|
|
318
|
+
let killed = false; let aborted = false; let timedOut = false;
|
|
319
|
+
const kill = () => { if (!killed) { killed = true; try { child.kill("SIGKILL"); } catch { /* keep waiting for close */ } } };
|
|
320
|
+
const cleanup = () => { this.clearTimer(timer); signal?.removeEventListener("abort", onAbort); child.stdout?.removeListener("data", onStdout); child.stderr?.removeListener("data", onStderr); };
|
|
321
|
+
// Bounded byte buffers: cut each chunk before storing, mark overflow stickily and kill once.
|
|
322
|
+
const onStdout = (chunk: unknown) => {
|
|
323
|
+
const buffer = toBuffer(chunk);
|
|
324
|
+
const remaining = MAX_STDOUT - stdoutBytes;
|
|
325
|
+
if (remaining > 0) { const slice = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer; stdoutChunks.push(slice); stdoutBytes += slice.length; }
|
|
326
|
+
if (buffer.length > remaining) overflowed = true;
|
|
327
|
+
if (overflowed) kill();
|
|
328
|
+
};
|
|
329
|
+
const onStderr = (chunk: unknown) => {
|
|
330
|
+
const buffer = toBuffer(chunk);
|
|
331
|
+
const remaining = MAX_STDERR - stderrBytes;
|
|
332
|
+
if (remaining > 0) { const slice = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer; stderrChunks.push(slice); stderrBytes += slice.length; }
|
|
333
|
+
};
|
|
334
|
+
const onAbort = () => { aborted = true; kill(); };
|
|
335
|
+
const timer = this.setTimer(() => { timedOut = true; kill(); }, timeoutMs);
|
|
336
|
+
unrefTimer(timer);
|
|
337
|
+
child.stdout?.on("data", onStdout);
|
|
338
|
+
child.stderr?.on("data", onStderr);
|
|
339
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
340
|
+
child.once("close", (...closeArgs: unknown[]) => {
|
|
341
|
+
cleanup();
|
|
342
|
+
const code = typeof closeArgs[0] === "number" ? closeArgs[0] : null;
|
|
343
|
+
resolve({ stdout: Buffer.concat(stdoutChunks).toString("utf8"), stderr: Buffer.concat(stderrChunks).toString("utf8"), aborted, timedOut, code, overflowed });
|
|
344
|
+
});
|
|
345
|
+
child.once("error", () => { /* Node emits close after spawn errors; keep waiting */ });
|
|
346
|
+
if (signal?.aborted) onAbort();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private parseProbe(stdout: string): NativeWindowsProbeResult {
|
|
351
|
+
const envelope = parseEnvelope(stdout);
|
|
352
|
+
if (!envelope || envelope.ok !== true || typeof envelope.available !== "boolean" || !Array.isArray(envelope.formats))
|
|
353
|
+
return { available: false, formats: [] };
|
|
354
|
+
const hasWav = envelope.formats.some(value => value === "wav");
|
|
355
|
+
return envelope.available && hasWav ? { available: true, formats: ["wav"] } : { available: false, formats: [] };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Explicit, read-only availability check: fixed host access, then one fixed probe script. Never throws. */
|
|
359
|
+
async probe(signal?: AbortSignal): Promise<NativeWindowsProbeResult> {
|
|
360
|
+
const target = this.resolveTarget();
|
|
361
|
+
if (target === undefined || signal?.aborted) { this.formats = []; return { available: false, formats: [] }; }
|
|
362
|
+
const executable = this.executablePath(target);
|
|
363
|
+
let available = false;
|
|
364
|
+
try {
|
|
365
|
+
available = this.options.executableAvailable ? await this.options.executableAvailable(executable)
|
|
366
|
+
: await access(executable, constants.X_OK).then(() => true);
|
|
367
|
+
} catch { available = false; }
|
|
368
|
+
if (!available || signal?.aborted) { this.formats = []; return { available: false, formats: [] }; }
|
|
369
|
+
let result: RunResult;
|
|
370
|
+
try { result = await this.run("probe", target, undefined, signal, this.options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS); }
|
|
371
|
+
catch { this.formats = []; return { available: false, formats: [] }; }
|
|
372
|
+
const parsed = result.aborted || result.timedOut || result.overflowed || result.code !== 0
|
|
373
|
+
? { available: false, formats: [] as const }
|
|
374
|
+
: this.parseProbe(result.stdout);
|
|
375
|
+
this.formats = parsed.available ? ["wav"] : [];
|
|
376
|
+
return parsed;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** `gate` (permit or predicate) runs synchronously before spawn; false means no child at all. */
|
|
380
|
+
async play(snapshot: string, signal?: AbortSignal, gate?: (() => boolean) | { permit(): boolean }): Promise<void> {
|
|
381
|
+
const target = this.resolveTarget();
|
|
382
|
+
if (target === undefined) throw new Error("Native Windows: unsupported platform");
|
|
383
|
+
if (signal?.aborted) throw abortError();
|
|
384
|
+
// Pure conversion/validation before the gate: posix -> derived UNC for WSL, drive-absolute for native.
|
|
385
|
+
const prepared = target.kind === "wsl"
|
|
386
|
+
? toWslUncPath(target.distro, snapshot)
|
|
387
|
+
: (isWindowsAbsolutePath(snapshot) ? snapshot : undefined);
|
|
388
|
+
if (prepared === undefined) throw new Error("Native Windows: invalid snapshot");
|
|
389
|
+
const permitted = typeof gate === "function" ? gate() : gate?.permit();
|
|
390
|
+
if (permitted === false) throw new NativePulseNotPermittedError();
|
|
391
|
+
let result: RunResult;
|
|
392
|
+
try { result = await this.run("play", target, prepared, signal, this.options.playTimeoutMs ?? DEFAULT_PLAY_TIMEOUT_MS); }
|
|
393
|
+
catch { throw privateFailure(); }
|
|
394
|
+
if (result.aborted) throw abortError();
|
|
395
|
+
if (result.overflowed || result.timedOut || result.code !== 0) throw privateFailure();
|
|
396
|
+
const envelope = parseEnvelope(result.stdout);
|
|
397
|
+
if (!envelope || envelope.ok !== true || envelope.played !== true) throw privateFailure();
|
|
398
|
+
}
|
|
399
|
+
}
|