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,581 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
FIXED_WINDOWS_POWERSHELL_EXE,
|
|
8
|
+
NATIVE_WINDOWS_SCHEMA,
|
|
9
|
+
NativeWindowsPlayer,
|
|
10
|
+
WSL_WINDOWS_POWERSHELL_EXE,
|
|
11
|
+
isWindowsAbsolutePath,
|
|
12
|
+
isWslPosixAbsolutePath,
|
|
13
|
+
isWslTarget,
|
|
14
|
+
toWslUncPath,
|
|
15
|
+
type NativeWindowsOptions,
|
|
16
|
+
type NativeWindowsSpawnOptions,
|
|
17
|
+
} from "../lib/notification-audio-windows.ts";
|
|
18
|
+
|
|
19
|
+
const probeOk = () => JSON.stringify({ schema: NATIVE_WINDOWS_SCHEMA, ok: true, available: true, formats: ["wav"] });
|
|
20
|
+
const playOk = () => JSON.stringify({ schema: NATIVE_WINDOWS_SCHEMA, ok: true, played: true });
|
|
21
|
+
/** The encoded command is the only dynamic argv; decode it to inspect what PowerShell would run. */
|
|
22
|
+
const decodeCommand = (encoded: string): string => Buffer.from(encoded, "base64").toString("utf16le");
|
|
23
|
+
/** The probe awaits the fixed-host access check before spawning; flush before observing the child. */
|
|
24
|
+
const flush = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
|
|
25
|
+
|
|
26
|
+
class FakeChild extends EventEmitter {
|
|
27
|
+
readonly stdout = new EventEmitter();
|
|
28
|
+
readonly stderr = new EventEmitter();
|
|
29
|
+
readonly kills: string[] = [];
|
|
30
|
+
kill(signal: string): boolean { this.kills.push(signal); return true; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const WSL_ENV: NodeJS.ProcessEnv = { WSL_DISTRO_NAME: "Ubuntu", WSL_INTEROP: "/run/WSL/4242_interop" };
|
|
34
|
+
function wslRecorder(env: NodeJS.ProcessEnv = WSL_ENV) {
|
|
35
|
+
return recorder("linux", { env });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function recorder(platform = "win32", overrides: Partial<NativeWindowsOptions> = {}) {
|
|
39
|
+
const spawned: Array<{ executable: string; args: string[]; options: NativeWindowsSpawnOptions }> = [];
|
|
40
|
+
const children: FakeChild[] = [];
|
|
41
|
+
const executableChecks: string[] = [];
|
|
42
|
+
const player = new NativeWindowsPlayer({
|
|
43
|
+
platform,
|
|
44
|
+
executableAvailable: async (path) => { executableChecks.push(path); return true; },
|
|
45
|
+
spawn: (executable, args, options) => { spawned.push({ executable, args, options }); const child = new FakeChild(); children.push(child); return child; },
|
|
46
|
+
env: {
|
|
47
|
+
SystemRoot: "D:\\Hostile", windir: "D:\\Hostile", TEMP: "C:\\Temp", TMP: "C:\\Tmp", USERPROFILE: "C:\\Users\\u",
|
|
48
|
+
OPENAI_API_KEY: "sk", ANTHROPIC_API_KEY: "x", AWS_SECRET_ACCESS_KEY: "y",
|
|
49
|
+
NODE_OPTIONS: "--inspect", NODE_PATH: "/n", DEBUG: "*", PATH: "C:\\evil", GENTLE_AUDIO_SNAPSHOT: "C:\\leak.wav",
|
|
50
|
+
},
|
|
51
|
+
...overrides,
|
|
52
|
+
});
|
|
53
|
+
return { player, spawned, children, executableChecks };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
test("windows adapter is lazy, supports only win32 and stays silent elsewhere", async () => {
|
|
57
|
+
const { player, spawned, children, executableChecks } = recorder();
|
|
58
|
+
assert.deepEqual(spawned, []);
|
|
59
|
+
assert.deepEqual(executableChecks, []);
|
|
60
|
+
assert.deepEqual(player.getNativeFormats(), []);
|
|
61
|
+
assert.equal(player.supportsTarget(), true);
|
|
62
|
+
const controller = new AbortController(); controller.abort();
|
|
63
|
+
assert.deepEqual(await player.probe(controller.signal), { available: false, formats: [] });
|
|
64
|
+
assert.deepEqual(executableChecks, []);
|
|
65
|
+
assert.equal(spawned.length, 0);
|
|
66
|
+
assert.equal(children.length, 0);
|
|
67
|
+
for (const platform of ["linux", "darwin", "freebsd"]) {
|
|
68
|
+
const other = recorder(platform);
|
|
69
|
+
assert.equal(other.player.supportsTarget(), false);
|
|
70
|
+
assert.deepEqual(await other.player.probe(), { available: false, formats: [] });
|
|
71
|
+
assert.deepEqual(other.executableChecks, []);
|
|
72
|
+
assert.equal(other.spawned.length, 0);
|
|
73
|
+
await assert.rejects(other.player.play("C:\\Temp\\sound.wav"), /Native Windows/);
|
|
74
|
+
assert.equal(other.spawned.length, 0);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a successful probe checks the fixed host and caches wav only after real output", async () => {
|
|
79
|
+
const { player, spawned, children, executableChecks } = recorder();
|
|
80
|
+
const probing = player.probe();
|
|
81
|
+
await flush();
|
|
82
|
+
assert.deepEqual(executableChecks, [FIXED_WINDOWS_POWERSHELL_EXE]);
|
|
83
|
+
assert.equal(spawned.length, 1);
|
|
84
|
+
const [call] = spawned;
|
|
85
|
+
assert.equal(call.executable, FIXED_WINDOWS_POWERSHELL_EXE);
|
|
86
|
+
assert.deepEqual(call.args.slice(0, 4), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"]);
|
|
87
|
+
assert.match(call.args[4]!, /^[A-Za-z0-9+/=]+$/);
|
|
88
|
+
assert.deepEqual(call.options.stdio, ["ignore", "pipe", "pipe"]);
|
|
89
|
+
assert.equal(call.options.shell, false);
|
|
90
|
+
assert.equal(call.options.windowsHide, true);
|
|
91
|
+
assert.equal(call.options.detached, false);
|
|
92
|
+
assert.deepEqual(player.getNativeFormats(), []); // never cached before a validated probe
|
|
93
|
+
children[0]!.stdout.emit("data", probeOk());
|
|
94
|
+
children[0]!.emit("close", 0);
|
|
95
|
+
assert.deepEqual(await probing, { available: true, formats: ["wav"] });
|
|
96
|
+
assert.deepEqual(player.getNativeFormats(), ["wav"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("probe is unavailable when the fixed host is missing or denied, without spawning", async () => {
|
|
100
|
+
for (const behavior of ["false", "throw"] as const) {
|
|
101
|
+
const spawned: number[] = [];
|
|
102
|
+
const player = new NativeWindowsPlayer({
|
|
103
|
+
platform: "win32",
|
|
104
|
+
executableAvailable: async () => { if (behavior === "throw") throw new Error("denied"); return false; },
|
|
105
|
+
spawn: () => { spawned.push(1); throw new Error("must not spawn"); },
|
|
106
|
+
});
|
|
107
|
+
assert.deepEqual(await player.probe(), { available: false, formats: [] });
|
|
108
|
+
assert.deepEqual(player.getNativeFormats(), []);
|
|
109
|
+
assert.deepEqual(spawned, []);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("probe treats malformed, multiline, oversize and false output as unavailable", async () => {
|
|
114
|
+
const envelope = probeOk();
|
|
115
|
+
const cases: Array<(child: FakeChild) => void> = [
|
|
116
|
+
(child) => child.stdout.emit("data", "not json"),
|
|
117
|
+
(child) => child.stdout.emit("data", JSON.stringify({ schema: "wrong", ok: true, available: true, formats: ["wav"] })),
|
|
118
|
+
(child) => child.stdout.emit("data", `${envelope}\n${envelope}`),
|
|
119
|
+
(child) => child.stdout.emit("data", JSON.stringify({ schema: NATIVE_WINDOWS_SCHEMA, ok: false, available: false, formats: [] })),
|
|
120
|
+
(child) => child.stdout.emit("data", JSON.stringify({ schema: NATIVE_WINDOWS_SCHEMA, ok: true, available: true, formats: ["ogg"] })),
|
|
121
|
+
];
|
|
122
|
+
for (const emit of cases) {
|
|
123
|
+
const { player, children } = recorder();
|
|
124
|
+
const probing = player.probe();
|
|
125
|
+
await flush();
|
|
126
|
+
emit(children[0]!);
|
|
127
|
+
children[0]!.emit("close", 0);
|
|
128
|
+
assert.deepEqual(await probing, { available: false, formats: [] });
|
|
129
|
+
assert.deepEqual(player.getNativeFormats(), []);
|
|
130
|
+
}
|
|
131
|
+
const oversize = recorder();
|
|
132
|
+
const probing = oversize.player.probe();
|
|
133
|
+
await flush();
|
|
134
|
+
oversize.children[0]!.stdout.emit("data", "x".repeat(2048));
|
|
135
|
+
assert.deepEqual(oversize.children[0]!.kills, ["SIGKILL"]);
|
|
136
|
+
oversize.children[0]!.emit("close", 1);
|
|
137
|
+
assert.deepEqual(await probing, { available: false, formats: [] });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("play gates synchronously once, encodes the snapshot and resolves on a clean close", async () => {
|
|
141
|
+
const snapshot = "C:\\Users\\u\\AppData\\Local\\Temp\\gentle-123\\sound.wav";
|
|
142
|
+
const { player, spawned, children } = recorder();
|
|
143
|
+
let calls = 0;
|
|
144
|
+
const playing = player.play(snapshot, undefined, () => { calls += 1; return true; });
|
|
145
|
+
assert.equal(calls, 1); // gate ran synchronously before the spawn
|
|
146
|
+
assert.equal(spawned.length, 1);
|
|
147
|
+
const encoded = spawned[0]!.args[4]!;
|
|
148
|
+
assert.match(encoded, /^[A-Za-z0-9+/=]+$/);
|
|
149
|
+
const script = decodeCommand(encoded);
|
|
150
|
+
const embedded = script.match(/FromBase64String\('([^']*)'\)/)?.[1];
|
|
151
|
+
assert.ok(embedded, "the play script must decode one embedded base64 literal");
|
|
152
|
+
assert.equal(Buffer.from(embedded!, "base64").toString("utf8"), snapshot, "the embedded bytes must round-trip to the exact snapshot");
|
|
153
|
+
assert.ok(!script.includes(snapshot), "the raw snapshot path must never appear in the command");
|
|
154
|
+
children[0]!.stdout.emit("data", playOk());
|
|
155
|
+
children[0]!.emit("close", 0);
|
|
156
|
+
await playing;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("a false permit performs no IO and throws the shared not-permitted error", async () => {
|
|
160
|
+
const { player, spawned, children } = recorder();
|
|
161
|
+
await assert.rejects(player.play("C:\\Temp\\sound.wav", undefined, () => false), (error: Error) => error.name === "NativePulseNotPermittedError");
|
|
162
|
+
assert.equal(spawned.length, 0);
|
|
163
|
+
assert.equal(children.length, 0);
|
|
164
|
+
const second = recorder();
|
|
165
|
+
await assert.rejects(second.player.play("C:\\Temp\\sound.wav", undefined, { permit: () => false }), /permit/);
|
|
166
|
+
assert.equal(second.spawned.length, 0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("hostile snapshot characters cannot reach PowerShell unencoded", async () => {
|
|
170
|
+
const hostile = "C:\\Temp\\a'; Start-Process calc; $x=`whoami`; #.wav";
|
|
171
|
+
const { player, spawned, children } = recorder();
|
|
172
|
+
const playing = player.play(hostile);
|
|
173
|
+
const encoded = spawned[0]!.args[4]!;
|
|
174
|
+
assert.match(encoded, /^[A-Za-z0-9+/=]+$/);
|
|
175
|
+
const script = decodeCommand(encoded);
|
|
176
|
+
assert.ok(script.includes(Buffer.from(hostile, "utf8").toString("base64")));
|
|
177
|
+
assert.ok(!script.includes("whoami") && !script.includes("Start-Process") && !script.includes(hostile));
|
|
178
|
+
children[0]!.stdout.emit("data", playOk());
|
|
179
|
+
children[0]!.emit("close", 0);
|
|
180
|
+
await playing;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("play rejects non-Windows-absolute snapshots before any spawn", async () => {
|
|
184
|
+
assert.equal(isWindowsAbsolutePath("C:\\Users\\u\\sound.wav"), true);
|
|
185
|
+
assert.equal(isWindowsAbsolutePath("C:\\"), true);
|
|
186
|
+
const invalid: Array<string> = [
|
|
187
|
+
"/tmp/sound.wav", "C:sound.wav", "\\\\server\\share\\sound.wav", "//server/share/sound.wav",
|
|
188
|
+
"C:\\a\\b:sound.wav", "C:\\a\u0000b.wav", "https://example.com/sound.wav", "",
|
|
189
|
+
];
|
|
190
|
+
for (const path of invalid) {
|
|
191
|
+
assert.equal(isWindowsAbsolutePath(path), false, path);
|
|
192
|
+
const { player, spawned, children } = recorder();
|
|
193
|
+
await assert.rejects(player.play(path), /Native Windows/);
|
|
194
|
+
assert.equal(spawned.length, 0);
|
|
195
|
+
assert.equal(children.length, 0);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("play rejects privately on wrong schema, multiline, oversize and nonzero close", async () => {
|
|
200
|
+
const snapshot = "C:\\Users\\u\\AppData\\Local\\Temp\\gentle\\sound.wav";
|
|
201
|
+
const variants: Array<{ emit: (child: FakeChild) => void; code: number }> = [
|
|
202
|
+
{ emit: (child) => child.stdout.emit("data", "not json"), code: 0 },
|
|
203
|
+
{ emit: (child) => child.stdout.emit("data", JSON.stringify({ schema: "wrong", ok: true, played: true })), code: 0 },
|
|
204
|
+
{ emit: (child) => child.stdout.emit("data", `${playOk()}\n${playOk()}`), code: 0 },
|
|
205
|
+
{ emit: (child) => child.stdout.emit("data", playOk()), code: 1 },
|
|
206
|
+
{ emit: (child) => child.stdout.emit("data", "x".repeat(2048)), code: 0 },
|
|
207
|
+
];
|
|
208
|
+
for (const variant of variants) {
|
|
209
|
+
const { player, children } = recorder();
|
|
210
|
+
const playing = player.play(snapshot);
|
|
211
|
+
variant.emit(children[0]!);
|
|
212
|
+
children[0]!.emit("close", variant.code);
|
|
213
|
+
await assert.rejects(playing, (error: Error) => /Native Windows/.test(error.message) && !error.message.includes(snapshot) && !/AppData|sound\.wav/.test(error.message));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("timeout and abort SIGKILL once and settle only on close", async () => {
|
|
218
|
+
const timed = recorder("win32", { probeTimeoutMs: 15 });
|
|
219
|
+
const probing = timed.player.probe();
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, 45));
|
|
221
|
+
assert.deepEqual(timed.children[0]!.kills, ["SIGKILL"]);
|
|
222
|
+
let settled = false; void probing.then(() => { settled = true; });
|
|
223
|
+
timed.children[0]!.emit("exit", 0);
|
|
224
|
+
timed.children[0]!.emit("error", new Error("spawn failed")); // Node emits close after error
|
|
225
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
226
|
+
assert.equal(settled, false);
|
|
227
|
+
timed.children[0]!.emit("close", 0);
|
|
228
|
+
assert.deepEqual(await probing, { available: false, formats: [] });
|
|
229
|
+
|
|
230
|
+
const aborted = recorder();
|
|
231
|
+
const controller = new AbortController();
|
|
232
|
+
const playing = aborted.player.play("C:\\Temp\\sound.wav", controller.signal);
|
|
233
|
+
controller.abort();
|
|
234
|
+
assert.deepEqual(aborted.children[0]!.kills, ["SIGKILL"]);
|
|
235
|
+
aborted.children[0]!.emit("close", 0);
|
|
236
|
+
await assert.rejects(playing, /aborted|Native Windows/);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("default play deadline is 11000 ms and outlives the accepted 10 s maximum", async () => {
|
|
240
|
+
let scheduledMs = 0; let fire: (() => void) | undefined;
|
|
241
|
+
const { player, children } = recorder("win32", {
|
|
242
|
+
setTimeout: (callback, ms) => { scheduledMs = ms; fire = callback; return 1; },
|
|
243
|
+
clearTimeout: () => {},
|
|
244
|
+
});
|
|
245
|
+
const playing = player.play("C:\\Temp\\sound.wav", undefined, () => true);
|
|
246
|
+
assert.equal(children.length, 1);
|
|
247
|
+
assert.equal(scheduledMs, 11000);
|
|
248
|
+
assert.ok(scheduledMs > 10000, "the Windows adapter must outlive the accepted 10 s file");
|
|
249
|
+
assert.deepEqual(children[0]!.kills, []);
|
|
250
|
+
fire!();
|
|
251
|
+
assert.deepEqual(children[0]!.kills, ["SIGKILL"]);
|
|
252
|
+
children[0]!.emit("close", 0);
|
|
253
|
+
await assert.rejects(playing, /Native Windows/);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("a synchronous spawn failure rejects privately without pending children", async () => {
|
|
257
|
+
const player = new NativeWindowsPlayer({
|
|
258
|
+
platform: "win32",
|
|
259
|
+
executableAvailable: async () => true,
|
|
260
|
+
spawn: () => { throw new Error("boom C:\\private leak"); },
|
|
261
|
+
});
|
|
262
|
+
assert.deepEqual(await player.probe(), { available: false, formats: [] });
|
|
263
|
+
await assert.rejects(player.play("C:\\Temp\\sound.wav"), (error: Error) => /Native Windows/.test(error.message) && !/private|leak/.test(error.message));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("child environment is a fixed Windows allowlist with forced SystemRoot", async () => {
|
|
267
|
+
const captured: NodeJS.ProcessEnv[] = [];
|
|
268
|
+
const player = new NativeWindowsPlayer({
|
|
269
|
+
platform: "win32",
|
|
270
|
+
executableAvailable: async () => true,
|
|
271
|
+
spawn: (_executable, _args, options) => { captured.push(options.env); return new FakeChild(); },
|
|
272
|
+
env: {
|
|
273
|
+
SystemRoot: "D:\\Hostile", windir: "D:\\Hostile", TEMP: "C:\\Temp", TMP: "C:\\Tmp", USERPROFILE: "C:\\Users\\u",
|
|
274
|
+
OPENAI_API_KEY: "sk", ANTHROPIC_API_KEY: "x", AWS_SECRET_ACCESS_KEY: "y",
|
|
275
|
+
NODE_OPTIONS: "--inspect", NODE_PATH: "/n", DEBUG: "*", PATH: "C:\\evil", GENTLE_AUDIO_SNAPSHOT: "C:\\leak.wav",
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
void player.probe();
|
|
279
|
+
await flush();
|
|
280
|
+
const env = captured[0]!;
|
|
281
|
+
assert.equal(env.SystemRoot, "C:\\Windows");
|
|
282
|
+
assert.equal(env.windir, "C:\\Windows");
|
|
283
|
+
assert.equal(env.TEMP, "C:\\Temp");
|
|
284
|
+
assert.equal(env.TMP, "C:\\Tmp");
|
|
285
|
+
assert.equal(env.USERPROFILE, "C:\\Users\\u");
|
|
286
|
+
for (const drop of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "AWS_SECRET_ACCESS_KEY", "NODE_OPTIONS", "NODE_PATH", "DEBUG", "PATH", "GENTLE_AUDIO_SNAPSHOT"])
|
|
287
|
+
assert.equal(env[drop], undefined, drop);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("an aborted play performs no IO and never calls the gate", async () => {
|
|
291
|
+
const { player, spawned, children } = recorder();
|
|
292
|
+
const controller = new AbortController(); controller.abort();
|
|
293
|
+
let calls = 0;
|
|
294
|
+
await assert.rejects(player.play("C:\\Temp\\sound.wav", controller.signal, () => { calls += 1; return true; }), /Native Windows/);
|
|
295
|
+
assert.equal(calls, 0);
|
|
296
|
+
assert.equal(spawned.length, 0);
|
|
297
|
+
assert.equal(children.length, 0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("an injected timer is installed once and cleared when the child closes", async () => {
|
|
301
|
+
const timers: Array<() => void> = [];
|
|
302
|
+
const cleared: unknown[] = [];
|
|
303
|
+
let handle = 0;
|
|
304
|
+
const { player, children } = recorder("win32", {
|
|
305
|
+
setTimeout: (callback) => { timers.push(callback); return ++handle; },
|
|
306
|
+
clearTimeout: (value) => { cleared.push(value); },
|
|
307
|
+
});
|
|
308
|
+
const probing = player.probe();
|
|
309
|
+
await flush();
|
|
310
|
+
assert.equal(timers.length, 1);
|
|
311
|
+
children[0]!.stdout.emit("data", probeOk());
|
|
312
|
+
children[0]!.emit("close", 0);
|
|
313
|
+
assert.deepEqual(await probing, { available: true, formats: ["wav"] });
|
|
314
|
+
assert.deepEqual(cleared, [1]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("probe rejects a valid ready envelope when the process closes nonzero", async () => {
|
|
318
|
+
const { player, children } = recorder();
|
|
319
|
+
const probing = player.probe();
|
|
320
|
+
await flush();
|
|
321
|
+
children[0]!.stdout.emit("data", probeOk());
|
|
322
|
+
children[0]!.emit("close", 1);
|
|
323
|
+
assert.deepEqual(await probing, { available: false, formats: [] });
|
|
324
|
+
assert.deepEqual(player.getNativeFormats(), []);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("probe rejects ready output that overflows the byte cap and kills exactly once", async () => {
|
|
328
|
+
const { player, children } = recorder();
|
|
329
|
+
const probing = player.probe();
|
|
330
|
+
await flush();
|
|
331
|
+
children[0]!.stdout.emit("data", probeOk());
|
|
332
|
+
children[0]!.stdout.emit("data", " ".repeat(3000));
|
|
333
|
+
children[0]!.stdout.emit("data", " ".repeat(3000));
|
|
334
|
+
assert.deepEqual(children[0]!.kills, ["SIGKILL"]);
|
|
335
|
+
children[0]!.emit("close", 0);
|
|
336
|
+
assert.deepEqual(await probing, { available: false, formats: [] });
|
|
337
|
+
assert.deepEqual(player.getNativeFormats(), []);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("play rejects played output that overflows the byte cap even on a clean close", async () => {
|
|
341
|
+
const { player, children } = recorder();
|
|
342
|
+
const playing = player.play("C:\\Temp\\sound.wav");
|
|
343
|
+
children[0]!.stdout.emit("data", playOk());
|
|
344
|
+
children[0]!.stdout.emit("data", " ".repeat(3000));
|
|
345
|
+
assert.deepEqual(children[0]!.kills, ["SIGKILL"]);
|
|
346
|
+
children[0]!.emit("close", 0);
|
|
347
|
+
await assert.rejects(playing, (error: Error) => /Native Windows/.test(error.message) && !error.message.includes("sound.wav"));
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("the fixed play script disposes the SoundPlayer in a finally block", async () => {
|
|
351
|
+
const { player, spawned, children } = recorder();
|
|
352
|
+
const playing = player.play("C:\\Temp\\sound.wav");
|
|
353
|
+
const script = decodeCommand(spawned[0]!.args[4]!);
|
|
354
|
+
assert.match(script, /\$player = \$null/, "the local player must be pre-initialized to null");
|
|
355
|
+
const finallyIndex = script.indexOf("finally {");
|
|
356
|
+
const disposeIndex = script.indexOf(".Dispose(");
|
|
357
|
+
assert.ok(finallyIndex !== -1, "a finally block must exist");
|
|
358
|
+
assert.ok(disposeIndex > finallyIndex, "the SoundPlayer must be disposed inside the finally block");
|
|
359
|
+
assert.ok(!/AppData|sound\.wav/.test(script), "the raw snapshot path must not appear in the script");
|
|
360
|
+
children[0]!.stdout.emit("data", playOk());
|
|
361
|
+
children[0]!.emit("close", 0);
|
|
362
|
+
await playing;
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("the fixed play script plays bounded snapshot bytes from memory, never the file path", async () => {
|
|
366
|
+
const hostile = "C:\\Temp\\a'; Start-Process calc; $x=`whoami`; #.wav";
|
|
367
|
+
const { player, spawned, children } = recorder();
|
|
368
|
+
const playing = player.play(hostile);
|
|
369
|
+
const script = decodeCommand(spawned[0]!.args[4]!);
|
|
370
|
+
// the snapshot is opened read-only/shared-read, never through a file-URI SoundPlayer ctor
|
|
371
|
+
assert.match(script, /\[IO\.File\]::Open\(\$path, \[IO\.FileMode\]::Open, \[IO\.FileAccess\]::Read, \[IO\.FileShare\]::Read\)/);
|
|
372
|
+
assert.match(script, /\$count = \$file\.Length/);
|
|
373
|
+
// the 2 MiB bound must be checked before any byte buffer is allocated or read
|
|
374
|
+
const sizeIndex = script.indexOf("2097152");
|
|
375
|
+
const allocIndex = script.indexOf("[byte[]]::new(");
|
|
376
|
+
const readIndex = script.indexOf("$file.Read(");
|
|
377
|
+
assert.ok(sizeIndex !== -1 && allocIndex !== -1 && readIndex !== -1, "size guard, allocation and read must exist");
|
|
378
|
+
assert.ok(sizeIndex < allocIndex && sizeIndex < readIndex, "the size bound must precede allocation and read");
|
|
379
|
+
// a full-length read loop rejects a short read and detects a file that grew
|
|
380
|
+
assert.match(script, /while \(\$read -lt \$count\)/);
|
|
381
|
+
assert.match(script, /\$file\.ReadByte\(\) -ne -1/);
|
|
382
|
+
// the SoundPlayer is built from the MemoryStream, never the path/string overload
|
|
383
|
+
assert.match(script, /\[IO\.MemoryStream\]::new\(/);
|
|
384
|
+
assert.match(script, /\[System\.Media\.SoundPlayer\]::new\(\$memory\)/);
|
|
385
|
+
assert.ok(!script.includes("New-Object"), "the SoundPlayer must not use the ambiguous file-path New-Object form");
|
|
386
|
+
assert.ok(!/SoundPlayer[ (]+\$path/.test(script), "the path must never reach the SoundPlayer constructor");
|
|
387
|
+
// the file lock is released before playback and every resource is disposed
|
|
388
|
+
const closeIndex = script.indexOf("$file.Close()");
|
|
389
|
+
const loadIndex = script.indexOf("$player.Load()");
|
|
390
|
+
const playIndex = script.indexOf("$player.PlaySync()");
|
|
391
|
+
assert.ok(closeIndex !== -1 && closeIndex < playIndex, "the file must close before PlaySync");
|
|
392
|
+
assert.ok(loadIndex !== -1 && loadIndex < playIndex, "the memory snapshot must be validated with Load before PlaySync");
|
|
393
|
+
assert.match(script, /\$memory\.Dispose\(\)/);
|
|
394
|
+
assert.match(script, /\$file\.Dispose\(\)/);
|
|
395
|
+
// hostile bytes still only reach PowerShell as one opaque base64 literal
|
|
396
|
+
const embedded = script.match(/FromBase64String\('([^']*)'\)/)?.[1];
|
|
397
|
+
assert.equal(Buffer.from(embedded!, "base64").toString("utf8"), hostile);
|
|
398
|
+
assert.ok(!script.includes("whoami") && !script.includes("Start-Process") && !script.includes(hostile));
|
|
399
|
+
children[0]!.stdout.emit("data", playOk());
|
|
400
|
+
children[0]!.emit("close", 0);
|
|
401
|
+
await playing;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("wsl interop target is lazy, supports via linux env, and selects the fixed mounted host", async () => {
|
|
405
|
+
const { player, spawned, children, executableChecks } = wslRecorder();
|
|
406
|
+
assert.deepEqual(player.getNativeFormats(), []);
|
|
407
|
+
assert.equal(player.supportsTarget(), true);
|
|
408
|
+
assert.deepEqual(spawned, []); // construction and support checks stay IO-free
|
|
409
|
+
assert.deepEqual(executableChecks, []);
|
|
410
|
+
const probing = player.probe();
|
|
411
|
+
await flush();
|
|
412
|
+
assert.deepEqual(executableChecks, [WSL_WINDOWS_POWERSHELL_EXE]);
|
|
413
|
+
assert.equal(spawned.length, 1);
|
|
414
|
+
assert.equal(spawned[0]!.executable, WSL_WINDOWS_POWERSHELL_EXE);
|
|
415
|
+
assert.deepEqual(spawned[0]!.options.stdio, ["ignore", "pipe", "pipe"]);
|
|
416
|
+
assert.deepEqual(player.getNativeFormats(), []); // never cached before a validated probe
|
|
417
|
+
children[0]!.stdout.emit("data", probeOk());
|
|
418
|
+
children[0]!.emit("close", 0);
|
|
419
|
+
assert.deepEqual(await probing, { available: true, formats: ["wav"] });
|
|
420
|
+
assert.deepEqual(player.getNativeFormats(), ["wav"]);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("wsl detection fails closed for a missing distro, bad interop path or invalid name", async () => {
|
|
424
|
+
const cases: NodeJS.ProcessEnv[] = [
|
|
425
|
+
{},
|
|
426
|
+
{ WSL_DISTRO_NAME: "Ubuntu" },
|
|
427
|
+
{ WSL_INTEROP: "/run/WSL/1_interop" },
|
|
428
|
+
{ WSL_DISTRO_NAME: "", WSL_INTEROP: "/run/WSL/1_interop" },
|
|
429
|
+
{ WSL_DISTRO_NAME: ".", WSL_INTEROP: "/run/WSL/1_interop" },
|
|
430
|
+
{ WSL_DISTRO_NAME: "..", WSL_INTEROP: "/run/WSL/1_interop" },
|
|
431
|
+
{ WSL_DISTRO_NAME: "a/b", WSL_INTEROP: "/run/WSL/1_interop" },
|
|
432
|
+
{ WSL_DISTRO_NAME: "a:b", WSL_INTEROP: "/run/WSL/1_interop" },
|
|
433
|
+
{ WSL_DISTRO_NAME: "Ubuntu", WSL_INTEROP: "relative_interop" },
|
|
434
|
+
{ WSL_DISTRO_NAME: "Ubuntu", WSL_INTEROP: "/tmp/1_interop" },
|
|
435
|
+
{ WSL_DISTRO_NAME: "Ubuntu", WSL_INTEROP: "/run/WSL/1" },
|
|
436
|
+
{ WSL_DISTRO_NAME: "Ubuntu", WSL_INTEROP: "/run/WSL/../1_interop" },
|
|
437
|
+
{ WSL_DISTRO_NAME: "Ubuntu", WSL_INTEROP: "/run/WSL//1_interop" },
|
|
438
|
+
];
|
|
439
|
+
for (const env of cases) {
|
|
440
|
+
assert.equal(isWslTarget(env), false, JSON.stringify(env));
|
|
441
|
+
const { player, spawned, children, executableChecks } = wslRecorder(env);
|
|
442
|
+
assert.equal(player.supportsTarget(), false);
|
|
443
|
+
assert.deepEqual(await player.probe(), { available: false, formats: [] });
|
|
444
|
+
assert.deepEqual(executableChecks, []);
|
|
445
|
+
assert.equal(spawned.length, 0);
|
|
446
|
+
await assert.rejects(player.play("/tmp/sound.wav"), /Native Windows/);
|
|
447
|
+
assert.equal(spawned.length, 0);
|
|
448
|
+
assert.equal(children.length, 0);
|
|
449
|
+
}
|
|
450
|
+
for (const platform of ["darwin", "freebsd"]) {
|
|
451
|
+
assert.equal(recorder(platform, { env: WSL_ENV }).player.supportsTarget(), false, platform);
|
|
452
|
+
}
|
|
453
|
+
assert.equal(isWslTarget(WSL_ENV), true);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("wsl play maps one POSIX snapshot to a single encoded UNC path without interpolating data", async () => {
|
|
457
|
+
const distro = "Ubuntu 22.04 \u00f1";
|
|
458
|
+
const snapshot = "/tmp/gentle-1/\u00f1 'quote' $x `y`; sound.wav";
|
|
459
|
+
const { player, spawned, children } = wslRecorder({ WSL_DISTRO_NAME: distro, WSL_INTEROP: "/run/WSL/7_interop" });
|
|
460
|
+
const playing = player.play(snapshot);
|
|
461
|
+
assert.equal(spawned.length, 1);
|
|
462
|
+
const encoded = spawned[0]!.args[4]!;
|
|
463
|
+
assert.match(encoded, /^[A-Za-z0-9+/=]+$/);
|
|
464
|
+
const script = decodeCommand(encoded);
|
|
465
|
+
const embedded = script.match(/FromBase64String\('([^']*)'\)/)?.[1];
|
|
466
|
+
assert.ok(embedded, "the play script must decode one embedded base64 literal");
|
|
467
|
+
const decoded = Buffer.from(embedded!, "base64").toString("utf8");
|
|
468
|
+
assert.equal(decoded, toWslUncPath(distro, snapshot));
|
|
469
|
+
assert.equal(decoded, `\\\\wsl.localhost\\${distro}\\tmp\\gentle-1\\\u00f1 'quote' $x \`y\`; sound.wav`);
|
|
470
|
+
assert.ok(!script.includes(snapshot), "the raw POSIX snapshot must never appear in the command");
|
|
471
|
+
assert.ok(!/whoami|Start-Process/.test(script), "no snapshot byte may become command text");
|
|
472
|
+
children[0]!.stdout.emit("data", playOk());
|
|
473
|
+
children[0]!.emit("close", 0);
|
|
474
|
+
await playing;
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("wsl snapshot validation rejects relative, traversal, drive, URI and externally supplied UNC paths", async () => {
|
|
478
|
+
const invalid: Array<string> = [
|
|
479
|
+
"tmp/sound.wav", "/tmp/../etc/x.wav", "/tmp/./x.wav", "/tmp//x.wav", "/tmp/x.wav/..",
|
|
480
|
+
"C:\\Temp\\x.wav", "\\\\wsl.localhost\\Ubuntu\\tmp\\x.wav", "//wsl.localhost/Ubuntu/tmp/x.wav",
|
|
481
|
+
"file:///tmp/x.wav", "/tmp/a:b.wav", "/tmp/a\\b.wav", "/tmp/a\u0000b.wav", "https://example.com/x.wav", "",
|
|
482
|
+
];
|
|
483
|
+
for (const path of invalid) {
|
|
484
|
+
assert.equal(isWslPosixAbsolutePath(path), false, path);
|
|
485
|
+
const { player, spawned, children } = wslRecorder();
|
|
486
|
+
await assert.rejects(player.play(path), /Native Windows/);
|
|
487
|
+
assert.equal(spawned.length, 0);
|
|
488
|
+
assert.equal(children.length, 0);
|
|
489
|
+
}
|
|
490
|
+
assert.equal(isWslPosixAbsolutePath("/tmp/sound.wav"), true);
|
|
491
|
+
assert.equal(toWslUncPath("Ubuntu", "/" + "a".repeat(4096)), undefined, "the mapped UNC path must respect the 4096 byte bound");
|
|
492
|
+
const oversized = wslRecorder();
|
|
493
|
+
await assert.rejects(oversized.player.play("/" + "a".repeat(4096)), /Native Windows/);
|
|
494
|
+
assert.equal(oversized.spawned.length, 0);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("native windows drive validation is unchanged and never admits a WSL UNC path", async () => {
|
|
498
|
+
assert.equal(isWindowsAbsolutePath("C:\\"), true);
|
|
499
|
+
assert.equal(isWindowsAbsolutePath("C:\\Users\\u\\x.wav"), true);
|
|
500
|
+
assert.equal(isWindowsAbsolutePath("\\\\wsl.localhost\\Ubuntu\\tmp\\x.wav"), false);
|
|
501
|
+
assert.equal(isWindowsAbsolutePath("\\\\wsl$\\Ubuntu\\tmp\\x.wav"), false);
|
|
502
|
+
assert.equal(isWindowsAbsolutePath("/tmp/x.wav"), false);
|
|
503
|
+
const { player, spawned } = recorder();
|
|
504
|
+
await assert.rejects(player.play("\\\\wsl.localhost\\Ubuntu\\tmp\\x.wav"), /Native Windows/);
|
|
505
|
+
assert.equal(spawned.length, 0);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("a WSL false permit performs no IO and the gate runs once before a close-settled spawn", async () => {
|
|
509
|
+
const denied = wslRecorder();
|
|
510
|
+
await assert.rejects(denied.player.play("/tmp/sound.wav", undefined, () => false), (error: Error) => error.name === "NativePulseNotPermittedError");
|
|
511
|
+
assert.equal(denied.spawned.length, 0);
|
|
512
|
+
const { player, spawned, children } = wslRecorder();
|
|
513
|
+
let calls = 0;
|
|
514
|
+
const playing = player.play("/tmp/sound.wav", undefined, () => { calls += 1; return true; });
|
|
515
|
+
assert.equal(calls, 1);
|
|
516
|
+
assert.equal(spawned.length, 1);
|
|
517
|
+
let settled = false; void playing.then(() => { settled = true; });
|
|
518
|
+
children[0]!.emit("exit", 0);
|
|
519
|
+
await flush();
|
|
520
|
+
assert.equal(settled, false, "play must settle only on close");
|
|
521
|
+
children[0]!.stdout.emit("data", playOk());
|
|
522
|
+
children[0]!.emit("close", 0);
|
|
523
|
+
await playing;
|
|
524
|
+
assert.equal(calls, 1);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("wsl child env keeps only the interop pair and cannot redirect the fixed host", async () => {
|
|
528
|
+
const captured: Array<{ executable: string; env: NodeJS.ProcessEnv }> = [];
|
|
529
|
+
const player = new NativeWindowsPlayer({
|
|
530
|
+
platform: "linux",
|
|
531
|
+
env: {
|
|
532
|
+
WSL_DISTRO_NAME: "Ubuntu", WSL_INTEROP: "/run/WSL/9_interop",
|
|
533
|
+
SystemRoot: "D:\\Hostile", windir: "D:\\Hostile", PATH: "/evil", WSLENV: "GENTLE_AUDIO_SNAPSHOT",
|
|
534
|
+
GENTLE_AUDIO_SNAPSHOT: "/tmp/leak.wav", OPENAI_API_KEY: "sk", NODE_OPTIONS: "--inspect",
|
|
535
|
+
TEMP: "/tmp", USERPROFILE: "C:\\Users\\u",
|
|
536
|
+
},
|
|
537
|
+
executableAvailable: async () => true,
|
|
538
|
+
spawn: (executable, _args, options) => { captured.push({ executable, env: options.env }); return new FakeChild(); },
|
|
539
|
+
});
|
|
540
|
+
void player.probe();
|
|
541
|
+
await flush();
|
|
542
|
+
assert.equal(captured[0]!.executable, WSL_WINDOWS_POWERSHELL_EXE);
|
|
543
|
+
const env = captured[0]!.env;
|
|
544
|
+
assert.equal(env.SystemRoot, "C:\\Windows");
|
|
545
|
+
assert.equal(env.windir, "C:\\Windows");
|
|
546
|
+
assert.equal(env.WSL_INTEROP, "/run/WSL/9_interop");
|
|
547
|
+
assert.equal(env.WSL_DISTRO_NAME, "Ubuntu");
|
|
548
|
+
assert.equal(env.TEMP, "/tmp");
|
|
549
|
+
assert.equal(env.USERPROFILE, "C:\\Users\\u");
|
|
550
|
+
for (const drop of ["PATH", "WSLENV", "GENTLE_AUDIO_SNAPSHOT", "OPENAI_API_KEY", "NODE_OPTIONS"])
|
|
551
|
+
assert.equal(env[drop], undefined, drop);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("wsl probe denies and an aborted wsl play is inert without spawning", async () => {
|
|
555
|
+
const deniedSpawned: number[] = [];
|
|
556
|
+
const denied = new NativeWindowsPlayer({
|
|
557
|
+
platform: "linux", env: WSL_ENV,
|
|
558
|
+
executableAvailable: async () => false,
|
|
559
|
+
spawn: () => { deniedSpawned.push(1); throw new Error("must not spawn"); },
|
|
560
|
+
});
|
|
561
|
+
assert.deepEqual(await denied.probe(), { available: false, formats: [] });
|
|
562
|
+
assert.deepEqual(denied.getNativeFormats(), []);
|
|
563
|
+
assert.deepEqual(deniedSpawned, []);
|
|
564
|
+
const { player, spawned, children } = wslRecorder();
|
|
565
|
+
const controller = new AbortController(); controller.abort();
|
|
566
|
+
let calls = 0;
|
|
567
|
+
await assert.rejects(player.play("/tmp/sound.wav", controller.signal, () => { calls += 1; return true; }), /Native Windows/);
|
|
568
|
+
assert.equal(calls, 0);
|
|
569
|
+
assert.equal(spawned.length, 0);
|
|
570
|
+
assert.equal(children.length, 0);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test("windows adapter source stays free of SDK, addon, script resource and audio imports", () => {
|
|
574
|
+
const source = readFileSync(join(process.cwd(), "lib", "notification-audio-windows.ts"), "utf8");
|
|
575
|
+
assert.doesNotMatch(source, /@earendil-works/);
|
|
576
|
+
assert.doesNotMatch(source, /\.ps1|ExecutionPolicy/);
|
|
577
|
+
assert.doesNotMatch(source, /\.node["']|native\//);
|
|
578
|
+
assert.doesNotMatch(source, /postinstall|installer|download/i);
|
|
579
|
+
assert.doesNotMatch(source, /wslpath/i, "the fixed host must never be resolved through a WSL path translator");
|
|
580
|
+
assert.doesNotMatch(source, /from\s+["']\.\/notification-audio\.ts["']/, "no production import cycle with the routing module");
|
|
581
|
+
});
|