@poolzin/pool-bot 2026.2.25 → 2026.2.26
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/dist/acp/event-mapper.js +87 -22
- package/dist/acp/meta.js +12 -6
- package/dist/agents/agent-paths.js +8 -9
- package/dist/agents/agent-scope.js +7 -5
- package/dist/agents/auth-profiles/oauth.js +148 -64
- package/dist/agents/auth-profiles/session-override.js +13 -7
- package/dist/agents/bash-tools.exec-host-gateway.js +14 -4
- package/dist/agents/bash-tools.exec-runtime.js +2 -25
- package/dist/agents/bedrock-discovery.js +3 -1
- package/dist/agents/byteplus-models.js +97 -0
- package/dist/agents/chutes-oauth.js +1 -0
- package/dist/agents/cli-runner/helpers.js +4 -0
- package/dist/agents/compaction.js +41 -14
- package/dist/agents/doubao-models.js +121 -0
- package/dist/agents/failover-error.js +2 -0
- package/dist/agents/huggingface-models.js +5 -3
- package/dist/agents/live-model-filter.js +5 -0
- package/dist/agents/minimax-vlm.js +10 -8
- package/dist/agents/model-auth.js +6 -0
- package/dist/agents/model-catalog.js +3 -1
- package/dist/agents/model-selection.js +7 -1
- package/dist/agents/models-config.providers.js +93 -11
- package/dist/agents/ollama-stream.js +117 -4
- package/dist/agents/opencode-zen-models.js +22 -11
- package/dist/agents/pi-embedded-helpers/errors.js +55 -33
- package/dist/agents/pi-embedded-helpers/messaging-dedupe.js +10 -5
- package/dist/agents/pi-embedded-helpers/thinking.js +10 -5
- package/dist/agents/pi-embedded-helpers.js +1 -1
- package/dist/agents/pi-embedded-runner/compact.js +29 -7
- package/dist/agents/pi-embedded-runner/extensions.js +28 -26
- package/dist/agents/pi-embedded-runner/google.js +20 -8
- package/dist/agents/pi-embedded-runner/run/attempt.js +95 -36
- package/dist/agents/pi-embedded-runner/run.js +71 -12
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +11 -2
- package/dist/agents/pi-embedded-runner/session-manager-cache.js +11 -7
- package/dist/agents/pi-embedded-runner/system-prompt.js +2 -0
- package/dist/agents/pi-embedded-runner/thinking.js +42 -0
- package/dist/agents/pi-embedded-runner/tool-name-allowlist.js +19 -0
- package/dist/agents/pi-embedded-runner/utils.js +7 -10
- package/dist/agents/pi-embedded-subscribe.handlers.lifecycle.js +45 -56
- package/dist/agents/pi-embedded-subscribe.handlers.tools.js +2 -2
- package/dist/agents/pi-embedded-subscribe.js +9 -4
- package/dist/agents/pi-embedded-subscribe.tools.js +68 -14
- package/dist/agents/pi-embedded-utils.js +3 -0
- package/dist/agents/pi-extensions/compaction-safeguard-runtime.js +4 -20
- package/dist/agents/pi-extensions/compaction-safeguard.js +75 -33
- package/dist/agents/pi-settings.js +40 -0
- package/dist/agents/pi-tools.policy.js +2 -1
- package/dist/agents/provider/config-loader.js +1 -1
- package/dist/agents/sandbox/browser.js +170 -33
- package/dist/agents/sandbox/config-hash.js +14 -27
- package/dist/agents/sandbox/config.js +21 -2
- package/dist/agents/sandbox/constants.js +2 -0
- package/dist/agents/sandbox/docker.js +16 -2
- package/dist/agents/sandbox/novnc-auth.js +62 -0
- package/dist/agents/sandbox/sanitize-env-vars.js +1 -1
- package/dist/agents/sandbox/shared.js +10 -6
- package/dist/agents/sandbox-paths.js +24 -11
- package/dist/agents/schema/clean-for-gemini.js +132 -85
- package/dist/agents/session-slug.js +10 -5
- package/dist/agents/session-tool-result-guard-wrapper.js +1 -0
- package/dist/agents/session-tool-result-guard.js +3 -1
- package/dist/agents/session-transcript-repair.js +40 -6
- package/dist/agents/skills/bundled-dir.js +19 -5
- package/dist/agents/skills/env-overrides.js +124 -43
- package/dist/agents/skills/frontmatter.js +6 -6
- package/dist/agents/skills/plugin-skills.js +14 -7
- package/dist/agents/skills/workspace.js +1 -0
- package/dist/agents/subagent-announce.js +251 -49
- package/dist/agents/subagent-lifecycle-events.js +19 -0
- package/dist/agents/subagent-registry-cleanup.js +31 -0
- package/dist/agents/subagent-registry-completion.js +68 -0
- package/dist/agents/subagent-registry-queries.js +117 -0
- package/dist/agents/subagent-registry-state.js +46 -0
- package/dist/agents/subagent-registry.js +252 -221
- package/dist/agents/subagent-registry.store.js +1 -0
- package/dist/agents/subagent-registry.types.js +1 -0
- package/dist/agents/subagent-spawn.js +195 -7
- package/dist/agents/system-prompt.js +22 -6
- package/dist/agents/test-helpers/fast-coding-tools.js +1 -18
- package/dist/agents/test-helpers/fast-core-tools.js +1 -17
- package/dist/agents/timeout.js +18 -6
- package/dist/agents/tool-call-id.js +1 -1
- package/dist/agents/tool-display-common.js +162 -29
- package/dist/agents/tool-images.js +82 -9
- package/dist/agents/tool-policy.js +51 -26
- package/dist/agents/tools/browser-tool.js +2 -2
- package/dist/agents/tools/canvas-tool.js +27 -1
- package/dist/agents/tools/common.js +45 -0
- package/dist/agents/tools/discord-actions-guild.js +4 -1
- package/dist/agents/tools/gateway-tool.js +3 -1
- package/dist/agents/tools/nodes-utils.js +1 -10
- package/dist/agents/tools/sessions-send-helpers.js +12 -6
- package/dist/agents/tools/sessions-spawn-tool.js +8 -2
- package/dist/agents/tools/subagents-tool.js +2 -1
- package/dist/agents/tools/whatsapp-actions.js +10 -2
- package/dist/agents/tools/whatsapp-target-auth.js +18 -0
- package/dist/agents/transcript-policy.js +22 -8
- package/dist/agents/venice-models.js +11 -3
- package/dist/auto-reply/commands-registry.data.js +51 -0
- package/dist/auto-reply/commands-registry.js +4 -3
- package/dist/auto-reply/group-activation.js +10 -5
- package/dist/auto-reply/inbound-debounce.js +10 -5
- package/dist/auto-reply/reply/abort.js +1 -1
- package/dist/auto-reply/reply/agent-runner-execution.js +4 -1
- package/dist/auto-reply/reply/bash-command.js +41 -39
- package/dist/auto-reply/reply/command-gates.js +25 -0
- package/dist/auto-reply/reply/commands-allowlist.js +111 -72
- package/dist/auto-reply/reply/commands-bash.js +6 -5
- package/dist/auto-reply/reply/commands-config.js +30 -28
- package/dist/auto-reply/reply/commands-core.js +2 -1
- package/dist/auto-reply/reply/commands-info.js +1 -0
- package/dist/auto-reply/reply/commands-models.js +65 -14
- package/dist/auto-reply/reply/commands-session.js +237 -82
- package/dist/auto-reply/reply/commands-setunset.js +45 -0
- package/dist/auto-reply/reply/commands-subagents/action-agents.js +44 -0
- package/dist/auto-reply/reply/commands-subagents/action-focus.js +64 -0
- package/dist/auto-reply/reply/commands-subagents/action-help.js +4 -0
- package/dist/auto-reply/reply/commands-subagents/action-info.js +45 -0
- package/dist/auto-reply/reply/commands-subagents/action-kill.js +60 -0
- package/dist/auto-reply/reply/commands-subagents/action-list.js +44 -0
- package/dist/auto-reply/reply/commands-subagents/action-log.js +29 -0
- package/dist/auto-reply/reply/commands-subagents/action-send.js +119 -0
- package/dist/auto-reply/reply/commands-subagents/action-spawn.js +52 -0
- package/dist/auto-reply/reply/commands-subagents/action-unfocus.js +30 -0
- package/dist/auto-reply/reply/commands-subagents/shared.js +303 -0
- package/dist/auto-reply/reply/commands-subagents.js +51 -587
- package/dist/auto-reply/reply/commands-tts.js +10 -5
- package/dist/auto-reply/reply/config-value.js +10 -5
- package/dist/auto-reply/reply/directive-handling.model-picker.js +12 -6
- package/dist/auto-reply/reply/directive-handling.persist.js +9 -21
- package/dist/auto-reply/reply/directive-handling.shared.js +24 -4
- package/dist/auto-reply/reply/followup-runner.js +1 -0
- package/dist/auto-reply/reply/get-reply-directives-utils.js +23 -14
- package/dist/auto-reply/reply/get-reply-directives.js +17 -28
- package/dist/auto-reply/reply/get-reply-inline-actions.js +1 -0
- package/dist/auto-reply/reply/get-reply.js +71 -12
- package/dist/auto-reply/reply/model-selection.js +80 -39
- package/dist/auto-reply/reply/queue/enqueue.js +10 -5
- package/dist/auto-reply/reply/queue/state.js +13 -12
- package/dist/auto-reply/reply/reply-payloads.js +67 -36
- package/dist/auto-reply/reply/reply-reference.js +9 -8
- package/dist/auto-reply/reply/route-reply.js +15 -8
- package/dist/auto-reply/reply/session-reset-prompt.js +1 -1
- package/dist/auto-reply/reply/session.js +22 -6
- package/dist/auto-reply/reply/strip-inbound-meta.js +147 -0
- package/dist/auto-reply/reply/subagents-utils.js +56 -30
- package/dist/auto-reply/reply/typing.js +46 -21
- package/dist/auto-reply/send-policy.js +14 -7
- package/dist/auto-reply/status.js +140 -16
- package/dist/auto-reply/templating.js +10 -5
- package/dist/auto-reply/thinking.js +7 -16
- package/dist/auto-reply/tokens.js +21 -5
- package/dist/browser/bridge-server.js +36 -20
- package/dist/browser/cdp.helpers.js +7 -14
- package/dist/browser/cdp.js +35 -15
- package/dist/browser/chrome.profile-decoration.js +7 -4
- package/dist/browser/config.js +4 -0
- package/dist/browser/extension-relay-auth.js +55 -0
- package/dist/browser/extension-relay.js +74 -29
- package/dist/browser/navigation-guard.js +9 -1
- package/dist/browser/paths.js +77 -0
- package/dist/browser/profiles.js +13 -8
- package/dist/browser/pw-ai-module.js +10 -5
- package/dist/browser/pw-session.js +76 -39
- package/dist/browser/pw-tools-core.interactions.js +14 -7
- package/dist/browser/pw-tools-core.state.js +12 -6
- package/dist/browser/routes/agent.act.js +2 -2
- package/dist/browser/server-context.js +7 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/allow-from.js +2 -1
- package/dist/channels/allowlists/resolve-utils.js +43 -19
- package/dist/channels/channel-config.js +14 -7
- package/dist/channels/draft-stream-loop.js +7 -0
- package/dist/channels/model-overrides.js +82 -0
- package/dist/channels/plugins/normalize/imessage.js +14 -7
- package/dist/channels/plugins/normalize/slack.js +10 -5
- package/dist/channels/plugins/normalize/telegram.js +14 -7
- package/dist/channels/plugins/outbound/discord.js +80 -8
- package/dist/channels/plugins/outbound/signal.js +11 -11
- package/dist/channels/plugins/setup-helpers.js +10 -5
- package/dist/channels/sender-label.js +14 -7
- package/dist/channels/session.js +4 -2
- package/dist/channels/status-reactions.js +297 -0
- package/dist/cli/banner.js +1 -1
- package/dist/cli/browser-cli-actions-input/register.files-downloads.js +65 -56
- package/dist/cli/cli-name.js +11 -11
- package/dist/cli/cli-utils.js +13 -3
- package/dist/cli/command-format.js +1 -1
- package/dist/cli/config-cli.js +1 -1
- package/dist/cli/daemon-cli/lifecycle-core.js +31 -19
- package/dist/cli/daemon-cli/lifecycle.js +64 -2
- package/dist/cli/daemon-cli/restart-health.js +126 -0
- package/dist/cli/daemon-cli/status.gather.js +9 -13
- package/dist/cli/daemon-cli/status.print.js +2 -10
- package/dist/cli/deps.js +27 -22
- package/dist/cli/gateway-cli/run-loop.js +23 -5
- package/dist/cli/node-cli/register.js +14 -5
- package/dist/cli/nodes-media-utils.js +7 -2
- package/dist/cli/outbound-send-deps.js +2 -9
- package/dist/cli/outbound-send-mapping.js +11 -0
- package/dist/cli/pairing-cli.js +40 -14
- package/dist/cli/plugins-cli.js +34 -41
- package/dist/cli/ports.js +11 -10
- package/dist/cli/program/command-registry.js +2 -11
- package/dist/cli/program/command-tree.js +16 -0
- package/dist/cli/program/preaction.js +13 -9
- package/dist/cli/program/register.configure.js +3 -18
- package/dist/cli/program/register.maintenance.js +2 -2
- package/dist/cli/program/register.onboard.js +2 -0
- package/dist/cli/program/register.status-health-sessions.js +16 -17
- package/dist/cli/program/register.subclis.js +93 -52
- package/dist/cli/route.js +11 -7
- package/dist/cli/system-cli.js +36 -46
- package/dist/cli/update-cli/shared.js +22 -9
- package/dist/cli/update-cli/update-command.js +89 -14
- package/dist/cli/update-cli/wizard.js +6 -12
- package/dist/commands/agent/run-context.js +18 -5
- package/dist/commands/agent/session-store.js +17 -4
- package/dist/commands/agent.js +22 -2
- package/dist/commands/agents.bindings.js +14 -7
- package/dist/commands/agents.commands.add.js +13 -9
- package/dist/commands/agents.commands.identity.js +12 -6
- package/dist/commands/agents.commands.list.js +11 -6
- package/dist/commands/agents.config.js +8 -10
- package/dist/commands/agents.providers.js +12 -6
- package/dist/commands/auth-choice-options.js +103 -75
- package/dist/commands/auth-choice.apply.byteplus.js +55 -0
- package/dist/commands/auth-choice.apply.js +4 -0
- package/dist/commands/auth-choice.apply.minimax.js +61 -13
- package/dist/commands/auth-choice.apply.openai.js +3 -1
- package/dist/commands/auth-choice.apply.volcengine.js +55 -0
- package/dist/commands/auth-choice.preferred-provider.js +2 -0
- package/dist/commands/channels/remove.js +13 -6
- package/dist/commands/channels/shared.js +4 -14
- package/dist/commands/configure.commands.js +14 -0
- package/dist/commands/configure.gateway.js +2 -4
- package/dist/commands/configure.js +1 -1
- package/dist/commands/configure.shared.js +11 -0
- package/dist/commands/daemon-install-helpers.js +2 -2
- package/dist/commands/dashboard.js +12 -10
- package/dist/commands/docs.js +14 -8
- package/dist/commands/doctor-config-flow.js +11 -9
- package/dist/commands/doctor-legacy-config.js +281 -0
- package/dist/commands/doctor-state-integrity.js +99 -23
- package/dist/commands/doctor-update.js +12 -9
- package/dist/commands/models/list.list-command.js +7 -5
- package/dist/commands/models/set-image.js +2 -21
- package/dist/commands/node-daemon-install-helpers.js +10 -8
- package/dist/commands/onboard-auth.config-minimax.js +54 -80
- package/dist/commands/onboard-auth.config-opencode.js +2 -18
- package/dist/commands/onboard-auth.credentials.js +90 -13
- package/dist/commands/onboard-auth.js +1 -1
- package/dist/commands/onboard-auth.models.js +6 -5
- package/dist/commands/onboard-hooks.js +1 -1
- package/dist/commands/onboard-non-interactive/api-keys.js +14 -7
- package/dist/commands/onboard-non-interactive/local/auth-choice.js +64 -49
- package/dist/commands/onboard-provider-auth-flags.js +14 -0
- package/dist/commands/onboard-remote.js +14 -7
- package/dist/commands/onboard.js +11 -13
- package/dist/commands/sandbox-display.js +6 -5
- package/dist/commands/status-all/diagnosis.js +14 -10
- package/dist/commands/status-all/format.js +1 -0
- package/dist/commands/status.gateway-probe.js +1 -16
- package/dist/commands/systemd-linger.js +12 -6
- package/dist/config/agent-limits.js +2 -0
- package/dist/config/commands.js +30 -16
- package/dist/config/config-paths.js +9 -11
- package/dist/config/defaults.js +22 -2
- package/dist/config/discord-preview-streaming.js +104 -0
- package/dist/config/env-vars.js +37 -8
- package/dist/config/includes.js +4 -0
- package/dist/config/io.js +97 -12
- package/dist/config/legacy.migrations.part-1.js +189 -78
- package/dist/config/legacy.shared.js +3 -1
- package/dist/config/merge-patch.js +4 -0
- package/dist/config/prototype-keys.js +4 -0
- package/dist/config/schema.help.js +44 -7
- package/dist/config/schema.labels.js +38 -6
- package/dist/config/sessions/delivery-info.js +10 -3
- package/dist/config/sessions/main-session.js +10 -5
- package/dist/config/sessions/session-file.js +33 -0
- package/dist/config/sessions/session-key.js +10 -5
- package/dist/config/sessions/store.js +1 -1
- package/dist/config/sessions.js +1 -0
- package/dist/config/zod-schema.agent-runtime.js +11 -0
- package/dist/config/zod-schema.js +148 -13
- package/dist/config/zod-schema.providers-core.js +78 -4
- package/dist/config/zod-schema.providers.js +6 -1
- package/dist/config/zod-schema.session.js +41 -2
- package/dist/cron/run-log.js +3 -0
- package/dist/cron/schedule.js +21 -10
- package/dist/cron/service/ops.js +35 -21
- package/dist/cron/service/timer.js +116 -16
- package/dist/cron/stagger.js +3 -1
- package/dist/discord/api.js +12 -6
- package/dist/discord/draft-chunking.js +22 -0
- package/dist/discord/draft-stream.js +124 -0
- package/dist/discord/monitor/agent-components.js +1 -1
- package/dist/discord/monitor/commands.js +5 -0
- package/dist/discord/monitor/gateway-plugin.js +2 -1
- package/dist/discord/monitor/listeners.js +37 -27
- package/dist/discord/monitor/message-handler.js +4 -1
- package/dist/discord/monitor/message-handler.preflight.js +65 -8
- package/dist/discord/monitor/message-handler.process.js +246 -217
- package/dist/discord/monitor/message-utils.js +143 -6
- package/dist/discord/monitor/model-picker-preferences.js +143 -0
- package/dist/discord/monitor/model-picker.js +651 -0
- package/dist/discord/monitor/native-command.js +573 -16
- package/dist/discord/monitor/provider.allowlist.js +223 -0
- package/dist/discord/monitor/provider.js +275 -347
- package/dist/discord/monitor/provider.lifecycle.js +100 -0
- package/dist/discord/monitor/reply-delivery.js +123 -16
- package/dist/discord/monitor/thread-bindings.discord-api.js +215 -0
- package/dist/discord/monitor/thread-bindings.js +4 -0
- package/dist/discord/monitor/thread-bindings.lifecycle.js +177 -0
- package/dist/discord/monitor/thread-bindings.manager.js +423 -0
- package/dist/discord/monitor/thread-bindings.messages.js +55 -0
- package/dist/discord/monitor/thread-bindings.state.js +358 -0
- package/dist/discord/monitor/thread-bindings.types.js +6 -0
- package/dist/discord/resolve-users.js +33 -21
- package/dist/discord/send.channels.js +15 -0
- package/dist/discord/send.js +3 -2
- package/dist/discord/send.outbound.js +82 -26
- package/dist/discord/send.permissions.js +83 -30
- package/dist/discord/send.reactions.js +8 -4
- package/dist/discord/token.js +10 -5
- package/dist/discord/voice/command.js +263 -0
- package/dist/discord/voice/manager.js +531 -0
- package/dist/gateway/auth.js +34 -10
- package/dist/gateway/call.js +4 -16
- package/dist/gateway/client.js +28 -4
- package/dist/gateway/config-reload.js +3 -4
- package/dist/gateway/control-ui.js +219 -96
- package/dist/gateway/hooks-mapping.js +88 -38
- package/dist/gateway/http-auth-helpers.js +3 -2
- package/dist/gateway/http-endpoint-helpers.js +1 -0
- package/dist/gateway/net.js +54 -12
- package/dist/gateway/node-invoke-system-run-approval.js +14 -35
- package/dist/gateway/node-registry.js +10 -5
- package/dist/gateway/openai-http.js +1 -0
- package/dist/gateway/openresponses-http.js +1 -0
- package/dist/gateway/origin-check.js +1 -18
- package/dist/gateway/protocol/index.js +4 -3
- package/dist/gateway/protocol/schema/cron.js +1 -0
- package/dist/gateway/protocol/schema/devices.js +1 -0
- package/dist/gateway/protocol/schema/protocol-schemas.js +2 -1
- package/dist/gateway/protocol/schema/sessions.js +6 -0
- package/dist/gateway/role-policy.js +17 -0
- package/dist/gateway/server/ws-connection/connect-policy.js +37 -0
- package/dist/gateway/server/ws-connection/message-handler.js +175 -148
- package/dist/gateway/server-chat.js +83 -25
- package/dist/gateway/server-constants.js +10 -9
- package/dist/gateway/server-cron.js +1 -0
- package/dist/gateway/server-http.js +16 -7
- package/dist/gateway/server-maintenance.js +20 -5
- package/dist/gateway/server-methods/chat.js +10 -6
- package/dist/gateway/server-methods/config.js +12 -14
- package/dist/gateway/server-methods/devices.js +17 -3
- package/dist/gateway/server-methods/models.js +11 -1
- package/dist/gateway/server-methods/sessions.js +64 -8
- package/dist/gateway/server-methods/usage.js +162 -75
- package/dist/gateway/server-node-events.js +29 -0
- package/dist/gateway/server-runtime-config.js +34 -13
- package/dist/gateway/server-startup-memory.js +17 -11
- package/dist/gateway/session-utils.fs.js +32 -34
- package/dist/gateway/sessions-resolve.js +17 -5
- package/dist/gateway/test-helpers.openai-mock.js +14 -7
- package/dist/gateway/tools-invoke-http.js +21 -10
- package/dist/hooks/bundled/bootstrap-extra-files/handler.js +3 -1
- package/dist/hooks/bundled/command-logger/handler.js +7 -2
- package/dist/hooks/bundled/session-memory/handler.js +6 -5
- package/dist/hooks/frontmatter.js +6 -6
- package/dist/hooks/gmail-watcher.js +11 -6
- package/dist/hooks/internal-hooks.js +11 -1
- package/dist/hooks/llm-slug-generator.js +4 -1
- package/dist/hooks/workspace.js +47 -17
- package/dist/imessage/accounts.js +9 -20
- package/dist/imessage/monitor/inbound-processing.js +2 -1
- package/dist/infra/archive.js +174 -73
- package/dist/infra/control-ui-assets.js +14 -6
- package/dist/infra/device-pairing.js +108 -29
- package/dist/infra/env.js +10 -5
- package/dist/infra/exec-approvals-allowlist.js +122 -0
- package/dist/infra/exec-approvals-analysis.js +34 -3
- package/dist/infra/exec-approvals.js +5 -17
- package/dist/infra/exec-safe-bin-policy.js +53 -45
- package/dist/infra/fs-safe.js +71 -39
- package/dist/infra/gateway-lock.js +6 -2
- package/dist/infra/heartbeat-wake.js +6 -12
- package/dist/infra/host-env-security-policy.json +19 -0
- package/dist/infra/host-env-security.js +66 -0
- package/dist/infra/net/ssrf.js +131 -38
- package/dist/infra/outbound/bound-delivery-router.js +88 -0
- package/dist/infra/outbound/channel-selection.js +12 -6
- package/dist/infra/outbound/envelope.js +1 -1
- package/dist/infra/outbound/format.js +12 -6
- package/dist/infra/outbound/payloads.js +14 -7
- package/dist/infra/outbound/session-binding-service.js +123 -0
- package/dist/infra/path-guards.js +25 -0
- package/dist/infra/provider-usage.fetch.codex.js +7 -15
- package/dist/infra/provider-usage.fetch.gemini.js +14 -11
- package/dist/infra/provider-usage.fetch.shared.js +30 -1
- package/dist/infra/provider-usage.fetch.zai.js +10 -9
- package/dist/infra/retry-policy.js +4 -2
- package/dist/infra/retry.js +9 -5
- package/dist/infra/session-cost-usage.js +107 -59
- package/dist/infra/session-maintenance-warning.js +3 -1
- package/dist/infra/shell-env.js +98 -34
- package/dist/infra/ssh-config.js +12 -6
- package/dist/infra/system-run-command.js +49 -4
- package/dist/infra/update-channels.js +10 -5
- package/dist/line/accounts.js +5 -7
- package/dist/line/bot-access.js +8 -20
- package/dist/line/bot-handlers.js +3 -1
- package/dist/link-understanding/detect.js +15 -7
- package/dist/media/constants.js +15 -6
- package/dist/media/image-ops.js +7 -0
- package/dist/media/local-roots.js +3 -2
- package/dist/media-understanding/apply.js +4 -1
- package/dist/media-understanding/concurrency.js +8 -20
- package/dist/memory/backend-config.js +45 -6
- package/dist/memory/embeddings.js +10 -4
- package/dist/memory/fs-utils.js +23 -0
- package/dist/memory/manager-search.js +12 -6
- package/dist/memory/manager-sync-ops.js +12 -2
- package/dist/memory/qmd-manager.js +466 -53
- package/dist/memory/query-expansion.js +167 -3
- package/dist/memory/status-format.js +10 -5
- package/dist/memory/sync-memory-files.js +1 -1
- package/dist/node-host/invoke-system-run.js +281 -0
- package/dist/node-host/invoke.js +55 -337
- package/dist/pairing/pairing-store.js +22 -0
- package/dist/plugin-sdk/allow-from.js +1 -1
- package/dist/plugin-sdk/command-auth.js +3 -1
- package/dist/plugin-sdk/index.js +6 -3
- package/dist/plugin-sdk/webhook-targets.js +32 -0
- package/dist/plugins/bundled-dir.js +9 -6
- package/dist/plugins/hooks.js +50 -0
- package/dist/plugins/install.js +28 -16
- package/dist/plugins/runtime.js +3 -17
- package/dist/plugins/update.js +78 -12
- package/dist/process/spawn-utils.js +14 -7
- package/dist/providers/github-copilot-token.js +11 -6
- package/dist/providers/qwen-portal-oauth.js +14 -6
- package/dist/routing/account-id.js +30 -0
- package/dist/routing/resolve-route.js +3 -7
- package/dist/routing/session-key.js +2 -16
- package/dist/security/audit-channel.js +93 -2
- package/dist/security/audit-extra.async.js +159 -5
- package/dist/security/audit-extra.js +1 -1
- package/dist/security/audit-extra.sync.js +85 -6
- package/dist/security/audit.js +40 -4
- package/dist/security/dm-policy-shared.js +44 -0
- package/dist/security/external-content.js +26 -6
- package/dist/shared/entry-status.js +6 -0
- package/dist/shared/frontmatter.js +5 -5
- package/dist/shared/node-match.js +11 -4
- package/dist/shared/operator-scope-compat.js +8 -3
- package/dist/signal/accounts.js +7 -20
- package/dist/signal/monitor/event-handler.js +3 -1
- package/dist/slack/accounts.js +6 -19
- package/dist/slack/actions.js +11 -3
- package/dist/slack/monitor/auth.js +1 -1
- package/dist/slack/monitor/message-handler/dispatch.js +50 -29
- package/dist/slack/monitor/replies.js +15 -7
- package/dist/slack/monitor/slash.js +22 -13
- package/dist/slack/resolve-channels.js +10 -5
- package/dist/slack/send.js +102 -12
- package/dist/slack/stream-mode.js +10 -0
- package/dist/slack/streaming.js +4 -2
- package/dist/telegram/accounts.js +19 -14
- package/dist/telegram/bot/helpers.js +3 -5
- package/dist/telegram/bot-access.js +35 -36
- package/dist/telegram/bot-handlers.js +120 -148
- package/dist/telegram/bot-message-context.js +68 -9
- package/dist/telegram/bot-message-dispatch.js +155 -90
- package/dist/telegram/bot-native-commands.js +16 -0
- package/dist/telegram/draft-stream.js +14 -1
- package/dist/telegram/inline-buttons.js +5 -15
- package/dist/telegram/monitor.js +11 -7
- package/dist/telegram/network-config.js +19 -7
- package/dist/telegram/send.js +3 -2
- package/dist/telegram/sent-message-cache.js +5 -6
- package/dist/telegram/status-reaction-variants.js +208 -0
- package/dist/telegram/sticker-cache.js +11 -9
- package/dist/terminal/theme.js +12 -12
- package/dist/tts/tts.js +80 -567
- package/dist/tui/components/chat-log.js +41 -8
- package/dist/tui/theme/theme.js +10 -12
- package/dist/tui/tui-local-shell.js +16 -6
- package/dist/tui/tui.js +58 -6
- package/dist/utils/account-id.js +2 -4
- package/dist/utils/boolean.js +10 -5
- package/dist/utils/directive-tags.js +11 -0
- package/dist/utils/queue-helpers.js +67 -12
- package/dist/web/auto-reply/deliver-reply.js +8 -4
- package/dist/web/auto-reply/mentions.js +10 -5
- package/dist/web/auto-reply/monitor/group-members.js +14 -7
- package/dist/web/auto-reply/monitor/process-message.js +45 -24
- package/dist/web/inbound/access-control.js +5 -2
- package/dist/web/login-qr.js +12 -6
- package/dist/web/media.js +123 -16
- package/extensions/bluebubbles/src/monitor-processing.ts +580 -139
- package/extensions/bluebubbles/src/monitor.ts +208 -1950
- package/package.json +1 -1
|
@@ -1,23 +1,31 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import { startBrowserBridgeServer, stopBrowserBridgeServer } from "../../browser/bridge-server.js";
|
|
2
3
|
import { resolveProfile } from "../../browser/config.js";
|
|
3
|
-
import { DEFAULT_BROWSER_EVALUATE_ENABLED, DEFAULT_CLAWD_BROWSER_COLOR, } from "../../browser/constants.js";
|
|
4
|
+
import { DEFAULT_BROWSER_EVALUATE_ENABLED, DEFAULT_CLAWD_BROWSER_COLOR, DEFAULT_CLAWD_BROWSER_PROFILE_NAME, } from "../../browser/constants.js";
|
|
5
|
+
import { defaultRuntime } from "../../runtime.js";
|
|
4
6
|
import { BROWSER_BRIDGES } from "./browser-bridges.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { computeSandboxBrowserConfigHash } from "./config-hash.js";
|
|
8
|
+
import { resolveSandboxBrowserDockerCreateConfig } from "./config.js";
|
|
9
|
+
import { DEFAULT_SANDBOX_BROWSER_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT, SANDBOX_BROWSER_SECURITY_HASH_EPOCH, } from "./constants.js";
|
|
10
|
+
import { buildSandboxCreateArgs, dockerContainerState, execDocker, readDockerContainerEnvVar, readDockerContainerLabel, readDockerPort, } from "./docker.js";
|
|
11
|
+
import { buildNoVncDirectUrl, buildNoVncObserverTokenUrl, consumeNoVncObserverToken, generateNoVncPassword, isNoVncEnabled, NOVNC_PASSWORD_ENV_KEY, issueNoVncObserverToken, } from "./novnc-auth.js";
|
|
12
|
+
import { readBrowserRegistry, updateBrowserRegistry } from "./registry.js";
|
|
13
|
+
import { resolveSandboxAgentId, slugifySessionKey } from "./shared.js";
|
|
9
14
|
import { isToolAllowed } from "./tool-policy.js";
|
|
15
|
+
const HOT_BROWSER_WINDOW_MS = 5 * 60 * 1000;
|
|
16
|
+
const CDP_SOURCE_RANGE_ENV_KEY = "POOLBOT_BROWSER_CDP_SOURCE_RANGE";
|
|
10
17
|
async function waitForSandboxCdp(params) {
|
|
11
18
|
const deadline = Date.now() + Math.max(0, params.timeoutMs);
|
|
12
19
|
const url = `http://127.0.0.1:${params.cdpPort}/json/version`;
|
|
13
20
|
while (Date.now() < deadline) {
|
|
14
21
|
try {
|
|
15
22
|
const ctrl = new AbortController();
|
|
16
|
-
const t = setTimeout(
|
|
23
|
+
const t = setTimeout(ctrl.abort.bind(ctrl), 1000);
|
|
17
24
|
try {
|
|
18
25
|
const res = await fetch(url, { signal: ctrl.signal });
|
|
19
|
-
if (res.ok)
|
|
26
|
+
if (res.ok) {
|
|
20
27
|
return true;
|
|
28
|
+
}
|
|
21
29
|
}
|
|
22
30
|
finally {
|
|
23
31
|
clearTimeout(t);
|
|
@@ -46,9 +54,13 @@ function buildSandboxBrowserResolvedConfig(params) {
|
|
|
46
54
|
headless: params.headless,
|
|
47
55
|
noSandbox: false,
|
|
48
56
|
attachOnly: true,
|
|
49
|
-
defaultProfile:
|
|
57
|
+
defaultProfile: DEFAULT_CLAWD_BROWSER_PROFILE_NAME,
|
|
58
|
+
extraArgs: [],
|
|
50
59
|
profiles: {
|
|
51
|
-
|
|
60
|
+
[DEFAULT_CLAWD_BROWSER_PROFILE_NAME]: {
|
|
61
|
+
cdpPort: params.cdpPort,
|
|
62
|
+
color: DEFAULT_CLAWD_BROWSER_COLOR,
|
|
63
|
+
},
|
|
52
64
|
},
|
|
53
65
|
};
|
|
54
66
|
}
|
|
@@ -56,26 +68,115 @@ async function ensureSandboxBrowserImage(image) {
|
|
|
56
68
|
const result = await execDocker(["image", "inspect", image], {
|
|
57
69
|
allowFailure: true,
|
|
58
70
|
});
|
|
59
|
-
if (result.code === 0)
|
|
71
|
+
if (result.code === 0) {
|
|
60
72
|
return;
|
|
73
|
+
}
|
|
61
74
|
throw new Error(`Sandbox browser image not found: ${image}. Build it with scripts/sandbox-browser-setup.sh.`);
|
|
62
75
|
}
|
|
76
|
+
async function ensureDockerNetwork(network) {
|
|
77
|
+
const normalized = network.trim().toLowerCase();
|
|
78
|
+
if (!normalized ||
|
|
79
|
+
normalized === "bridge" ||
|
|
80
|
+
normalized === "none" ||
|
|
81
|
+
normalized.startsWith("container:")) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const inspect = await execDocker(["network", "inspect", network], { allowFailure: true });
|
|
85
|
+
if (inspect.code === 0) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await execDocker(["network", "create", "--driver", "bridge", network]);
|
|
89
|
+
}
|
|
63
90
|
export async function ensureSandboxBrowser(params) {
|
|
64
|
-
if (!params.cfg.browser.enabled)
|
|
91
|
+
if (!params.cfg.browser.enabled) {
|
|
65
92
|
return null;
|
|
66
|
-
|
|
93
|
+
}
|
|
94
|
+
if (!isToolAllowed(params.cfg.tools, "browser")) {
|
|
67
95
|
return null;
|
|
96
|
+
}
|
|
68
97
|
const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(params.scopeKey);
|
|
69
98
|
const name = `${params.cfg.browser.containerPrefix}${slug}`;
|
|
70
99
|
const containerName = name.slice(0, 63);
|
|
71
100
|
const state = await dockerContainerState(containerName);
|
|
72
|
-
|
|
73
|
-
|
|
101
|
+
const browserImage = params.cfg.browser.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE;
|
|
102
|
+
const cdpSourceRange = params.cfg.browser.cdpSourceRange?.trim() || undefined;
|
|
103
|
+
const browserDockerCfg = resolveSandboxBrowserDockerCreateConfig({
|
|
104
|
+
docker: params.cfg.docker,
|
|
105
|
+
browser: { ...params.cfg.browser, image: browserImage },
|
|
106
|
+
});
|
|
107
|
+
const expectedHash = computeSandboxBrowserConfigHash({
|
|
108
|
+
docker: browserDockerCfg,
|
|
109
|
+
browser: {
|
|
110
|
+
cdpPort: params.cfg.browser.cdpPort,
|
|
111
|
+
vncPort: params.cfg.browser.vncPort,
|
|
112
|
+
noVncPort: params.cfg.browser.noVncPort,
|
|
113
|
+
headless: params.cfg.browser.headless,
|
|
114
|
+
enableNoVnc: params.cfg.browser.enableNoVnc,
|
|
115
|
+
cdpSourceRange,
|
|
116
|
+
},
|
|
117
|
+
securityEpoch: SANDBOX_BROWSER_SECURITY_HASH_EPOCH,
|
|
118
|
+
workspaceAccess: params.cfg.workspaceAccess,
|
|
119
|
+
workspaceDir: params.workspaceDir,
|
|
120
|
+
agentWorkspaceDir: params.agentWorkspaceDir,
|
|
121
|
+
});
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
let hasContainer = state.exists;
|
|
124
|
+
let running = state.running;
|
|
125
|
+
let currentHash = null;
|
|
126
|
+
let hashMismatch = false;
|
|
127
|
+
const noVncEnabled = isNoVncEnabled(params.cfg.browser);
|
|
128
|
+
let noVncPassword;
|
|
129
|
+
if (hasContainer) {
|
|
130
|
+
if (noVncEnabled) {
|
|
131
|
+
noVncPassword =
|
|
132
|
+
(await readDockerContainerEnvVar(containerName, NOVNC_PASSWORD_ENV_KEY)) ?? undefined;
|
|
133
|
+
}
|
|
134
|
+
const registry = await readBrowserRegistry();
|
|
135
|
+
const registryEntry = registry.entries.find((entry) => entry.containerName === containerName);
|
|
136
|
+
currentHash = await readDockerContainerLabel(containerName, "poolbot.configHash");
|
|
137
|
+
hashMismatch = !currentHash || currentHash !== expectedHash;
|
|
138
|
+
if (!currentHash) {
|
|
139
|
+
currentHash = registryEntry?.configHash ?? null;
|
|
140
|
+
hashMismatch = !currentHash || currentHash !== expectedHash;
|
|
141
|
+
}
|
|
142
|
+
if (hashMismatch) {
|
|
143
|
+
const lastUsedAtMs = registryEntry?.lastUsedAtMs;
|
|
144
|
+
const isHot = running && (typeof lastUsedAtMs !== "number" || now - lastUsedAtMs < HOT_BROWSER_WINDOW_MS);
|
|
145
|
+
if (isHot) {
|
|
146
|
+
const hint = (() => {
|
|
147
|
+
if (params.cfg.scope === "session") {
|
|
148
|
+
return `poolbot sandbox recreate --browser --session ${params.scopeKey}`;
|
|
149
|
+
}
|
|
150
|
+
if (params.cfg.scope === "agent") {
|
|
151
|
+
const agentId = resolveSandboxAgentId(params.scopeKey) ?? "main";
|
|
152
|
+
return `poolbot sandbox recreate --browser --agent ${agentId}`;
|
|
153
|
+
}
|
|
154
|
+
return "poolbot sandbox recreate --browser --all";
|
|
155
|
+
})();
|
|
156
|
+
defaultRuntime.log(`Sandbox browser config changed for ${containerName} (recently used). Recreate to apply: ${hint}`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
await execDocker(["rm", "-f", containerName], { allowFailure: true });
|
|
160
|
+
hasContainer = false;
|
|
161
|
+
running = false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!hasContainer) {
|
|
166
|
+
if (noVncEnabled) {
|
|
167
|
+
noVncPassword = generateNoVncPassword();
|
|
168
|
+
}
|
|
169
|
+
await ensureDockerNetwork(browserDockerCfg.network);
|
|
170
|
+
await ensureSandboxBrowserImage(browserImage);
|
|
74
171
|
const args = buildSandboxCreateArgs({
|
|
75
172
|
name: containerName,
|
|
76
|
-
cfg:
|
|
173
|
+
cfg: browserDockerCfg,
|
|
77
174
|
scopeKey: params.scopeKey,
|
|
78
|
-
labels: {
|
|
175
|
+
labels: {
|
|
176
|
+
"poolbot.sandboxBrowser": "1",
|
|
177
|
+
"poolbot.browserConfigEpoch": SANDBOX_BROWSER_SECURITY_HASH_EPOCH,
|
|
178
|
+
},
|
|
179
|
+
configHash: expectedHash,
|
|
79
180
|
});
|
|
80
181
|
const mainMountSuffix = params.cfg.workspaceAccess === "ro" && params.workspaceDir === params.agentWorkspaceDir
|
|
81
182
|
? ":ro"
|
|
@@ -86,48 +187,75 @@ export async function ensureSandboxBrowser(params) {
|
|
|
86
187
|
args.push("-v", `${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`);
|
|
87
188
|
}
|
|
88
189
|
args.push("-p", `127.0.0.1::${params.cfg.browser.cdpPort}`);
|
|
89
|
-
if (
|
|
190
|
+
if (noVncEnabled) {
|
|
90
191
|
args.push("-p", `127.0.0.1::${params.cfg.browser.noVncPort}`);
|
|
91
192
|
}
|
|
92
193
|
args.push("-e", `POOLBOT_BROWSER_HEADLESS=${params.cfg.browser.headless ? "1" : "0"}`);
|
|
93
|
-
args.push("-e", `CLAWDBOT_BROWSER_HEADLESS=${params.cfg.browser.headless ? "1" : "0"}`);
|
|
94
194
|
args.push("-e", `POOLBOT_BROWSER_ENABLE_NOVNC=${params.cfg.browser.enableNoVnc ? "1" : "0"}`);
|
|
95
|
-
args.push("-e", `CLAWDBOT_BROWSER_ENABLE_NOVNC=${params.cfg.browser.enableNoVnc ? "1" : "0"}`);
|
|
96
195
|
args.push("-e", `POOLBOT_BROWSER_CDP_PORT=${params.cfg.browser.cdpPort}`);
|
|
97
|
-
|
|
196
|
+
if (cdpSourceRange) {
|
|
197
|
+
args.push("-e", `${CDP_SOURCE_RANGE_ENV_KEY}=${cdpSourceRange}`);
|
|
198
|
+
}
|
|
98
199
|
args.push("-e", `POOLBOT_BROWSER_VNC_PORT=${params.cfg.browser.vncPort}`);
|
|
99
|
-
args.push("-e", `CLAWDBOT_BROWSER_VNC_PORT=${params.cfg.browser.vncPort}`);
|
|
100
200
|
args.push("-e", `POOLBOT_BROWSER_NOVNC_PORT=${params.cfg.browser.noVncPort}`);
|
|
101
|
-
|
|
102
|
-
|
|
201
|
+
if (noVncEnabled && noVncPassword) {
|
|
202
|
+
args.push("-e", `${NOVNC_PASSWORD_ENV_KEY}=${noVncPassword}`);
|
|
203
|
+
}
|
|
204
|
+
args.push(browserImage);
|
|
103
205
|
await execDocker(args);
|
|
104
206
|
await execDocker(["start", containerName]);
|
|
105
207
|
}
|
|
106
|
-
else if (!
|
|
208
|
+
else if (!running) {
|
|
107
209
|
await execDocker(["start", containerName]);
|
|
108
210
|
}
|
|
109
211
|
const mappedCdp = await readDockerPort(containerName, params.cfg.browser.cdpPort);
|
|
110
212
|
if (!mappedCdp) {
|
|
111
213
|
throw new Error(`Failed to resolve CDP port mapping for ${containerName}.`);
|
|
112
214
|
}
|
|
113
|
-
const mappedNoVnc =
|
|
215
|
+
const mappedNoVnc = noVncEnabled
|
|
114
216
|
? await readDockerPort(containerName, params.cfg.browser.noVncPort)
|
|
115
217
|
: null;
|
|
218
|
+
if (noVncEnabled && !noVncPassword) {
|
|
219
|
+
noVncPassword =
|
|
220
|
+
(await readDockerContainerEnvVar(containerName, NOVNC_PASSWORD_ENV_KEY)) ?? undefined;
|
|
221
|
+
}
|
|
116
222
|
const existing = BROWSER_BRIDGES.get(params.scopeKey);
|
|
117
|
-
const existingProfile = existing
|
|
223
|
+
const existingProfile = existing
|
|
224
|
+
? resolveProfile(existing.bridge.state.resolved, DEFAULT_CLAWD_BROWSER_PROFILE_NAME)
|
|
225
|
+
: null;
|
|
226
|
+
let desiredAuthToken = params.bridgeAuth?.token?.trim() || undefined;
|
|
227
|
+
let desiredAuthPassword = params.bridgeAuth?.password?.trim() || undefined;
|
|
228
|
+
if (!desiredAuthToken && !desiredAuthPassword) {
|
|
229
|
+
// Always require auth for the sandbox bridge server, even if gateway auth
|
|
230
|
+
// mode doesn't produce a shared secret (e.g. trusted-proxy).
|
|
231
|
+
// Keep it stable across calls by reusing the existing bridge auth.
|
|
232
|
+
desiredAuthToken = existing?.authToken;
|
|
233
|
+
desiredAuthPassword = existing?.authPassword;
|
|
234
|
+
if (!desiredAuthToken && !desiredAuthPassword) {
|
|
235
|
+
desiredAuthToken = crypto.randomBytes(24).toString("hex");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
118
238
|
const shouldReuse = existing && existing.containerName === containerName && existingProfile?.cdpPort === mappedCdp;
|
|
239
|
+
const authMatches = !existing ||
|
|
240
|
+
(existing.authToken === desiredAuthToken && existing.authPassword === desiredAuthPassword);
|
|
119
241
|
if (existing && !shouldReuse) {
|
|
120
242
|
await stopBrowserBridgeServer(existing.bridge.server).catch(() => undefined);
|
|
121
243
|
BROWSER_BRIDGES.delete(params.scopeKey);
|
|
122
244
|
}
|
|
245
|
+
if (existing && shouldReuse && !authMatches) {
|
|
246
|
+
await stopBrowserBridgeServer(existing.bridge.server).catch(() => undefined);
|
|
247
|
+
BROWSER_BRIDGES.delete(params.scopeKey);
|
|
248
|
+
}
|
|
123
249
|
const bridge = (() => {
|
|
124
|
-
if (shouldReuse && existing)
|
|
250
|
+
if (shouldReuse && authMatches && existing) {
|
|
125
251
|
return existing.bridge;
|
|
252
|
+
}
|
|
126
253
|
return null;
|
|
127
254
|
})();
|
|
128
255
|
const ensureBridge = async () => {
|
|
129
|
-
if (bridge)
|
|
256
|
+
if (bridge) {
|
|
130
257
|
return bridge;
|
|
258
|
+
}
|
|
131
259
|
const onEnsureAttachTarget = params.cfg.browser.autoStart
|
|
132
260
|
? async () => {
|
|
133
261
|
const state = await dockerContainerState(containerName);
|
|
@@ -150,28 +278,37 @@ export async function ensureSandboxBrowser(params) {
|
|
|
150
278
|
headless: params.cfg.browser.headless,
|
|
151
279
|
evaluateEnabled: params.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED,
|
|
152
280
|
}),
|
|
281
|
+
authToken: desiredAuthToken,
|
|
282
|
+
authPassword: desiredAuthPassword,
|
|
153
283
|
onEnsureAttachTarget,
|
|
284
|
+
resolveSandboxNoVncToken: consumeNoVncObserverToken,
|
|
154
285
|
});
|
|
155
286
|
};
|
|
156
287
|
const resolvedBridge = await ensureBridge();
|
|
157
|
-
if (!shouldReuse) {
|
|
288
|
+
if (!shouldReuse || !authMatches) {
|
|
158
289
|
BROWSER_BRIDGES.set(params.scopeKey, {
|
|
159
290
|
bridge: resolvedBridge,
|
|
160
291
|
containerName,
|
|
292
|
+
authToken: desiredAuthToken,
|
|
293
|
+
authPassword: desiredAuthPassword,
|
|
161
294
|
});
|
|
162
295
|
}
|
|
163
|
-
const now = Date.now();
|
|
164
296
|
await updateBrowserRegistry({
|
|
165
297
|
containerName,
|
|
166
298
|
sessionKey: params.scopeKey,
|
|
167
299
|
createdAtMs: now,
|
|
168
300
|
lastUsedAtMs: now,
|
|
169
|
-
image:
|
|
301
|
+
image: browserImage,
|
|
302
|
+
configHash: hashMismatch && running ? (currentHash ?? undefined) : expectedHash,
|
|
170
303
|
cdpPort: mappedCdp,
|
|
171
304
|
noVncPort: mappedNoVnc ?? undefined,
|
|
172
305
|
});
|
|
173
|
-
const noVncUrl = mappedNoVnc &&
|
|
174
|
-
?
|
|
306
|
+
const noVncUrl = mappedNoVnc && noVncEnabled
|
|
307
|
+
? (() => {
|
|
308
|
+
const directUrl = buildNoVncDirectUrl(mappedNoVnc, noVncPassword);
|
|
309
|
+
const token = issueNoVncObserverToken({ url: directUrl });
|
|
310
|
+
return buildNoVncObserverTokenUrl(resolvedBridge.baseUrl, token);
|
|
311
|
+
})()
|
|
175
312
|
: undefined;
|
|
176
313
|
return {
|
|
177
314
|
bridgeUrl: resolvedBridge.baseUrl,
|
|
@@ -1,45 +1,32 @@
|
|
|
1
|
-
import
|
|
2
|
-
function isPrimitive(value) {
|
|
3
|
-
return value === null || (typeof value !== "object" && typeof value !== "function");
|
|
4
|
-
}
|
|
1
|
+
import { hashTextSha256 } from "./hash.js";
|
|
5
2
|
function normalizeForHash(value) {
|
|
6
|
-
if (value === undefined)
|
|
3
|
+
if (value === undefined) {
|
|
7
4
|
return undefined;
|
|
5
|
+
}
|
|
8
6
|
if (Array.isArray(value)) {
|
|
9
|
-
|
|
10
|
-
.map(normalizeForHash)
|
|
11
|
-
.filter((item) => item !== undefined);
|
|
12
|
-
const primitives = normalized.filter(isPrimitive);
|
|
13
|
-
if (primitives.length === normalized.length) {
|
|
14
|
-
return [...primitives].sort((a, b) => primitiveToString(a).localeCompare(primitiveToString(b)));
|
|
15
|
-
}
|
|
16
|
-
return normalized;
|
|
7
|
+
return value.map(normalizeForHash).filter((item) => item !== undefined);
|
|
17
8
|
}
|
|
18
9
|
if (value && typeof value === "object") {
|
|
19
|
-
const entries = Object.entries(value).
|
|
10
|
+
const entries = Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b));
|
|
20
11
|
const normalized = {};
|
|
21
12
|
for (const [key, entryValue] of entries) {
|
|
22
13
|
const next = normalizeForHash(entryValue);
|
|
23
|
-
if (next !== undefined)
|
|
14
|
+
if (next !== undefined) {
|
|
24
15
|
normalized[key] = next;
|
|
16
|
+
}
|
|
25
17
|
}
|
|
26
18
|
return normalized;
|
|
27
19
|
}
|
|
28
20
|
return value;
|
|
29
21
|
}
|
|
30
|
-
function primitiveToString(value) {
|
|
31
|
-
if (value === null)
|
|
32
|
-
return "null";
|
|
33
|
-
if (typeof value === "string")
|
|
34
|
-
return value;
|
|
35
|
-
if (typeof value === "number")
|
|
36
|
-
return String(value);
|
|
37
|
-
if (typeof value === "boolean")
|
|
38
|
-
return value ? "true" : "false";
|
|
39
|
-
return JSON.stringify(value);
|
|
40
|
-
}
|
|
41
22
|
export function computeSandboxConfigHash(input) {
|
|
23
|
+
return computeHash(input);
|
|
24
|
+
}
|
|
25
|
+
export function computeSandboxBrowserConfigHash(input) {
|
|
26
|
+
return computeHash(input);
|
|
27
|
+
}
|
|
28
|
+
function computeHash(input) {
|
|
42
29
|
const payload = normalizeForHash(input);
|
|
43
30
|
const raw = JSON.stringify(payload);
|
|
44
|
-
return
|
|
31
|
+
return hashTextSha256(raw);
|
|
45
32
|
}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { resolveAgentConfig } from "../agent-scope.js";
|
|
2
|
-
import { DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS, DEFAULT_SANDBOX_BROWSER_CDP_PORT, DEFAULT_SANDBOX_BROWSER_IMAGE, DEFAULT_SANDBOX_BROWSER_NOVNC_PORT, DEFAULT_SANDBOX_BROWSER_PREFIX, DEFAULT_SANDBOX_BROWSER_VNC_PORT, DEFAULT_SANDBOX_CONTAINER_PREFIX, DEFAULT_SANDBOX_IDLE_HOURS, DEFAULT_SANDBOX_IMAGE, DEFAULT_SANDBOX_MAX_AGE_DAYS, DEFAULT_SANDBOX_WORKDIR, DEFAULT_SANDBOX_WORKSPACE_ROOT, } from "./constants.js";
|
|
2
|
+
import { DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS, DEFAULT_SANDBOX_BROWSER_CDP_PORT, DEFAULT_SANDBOX_BROWSER_IMAGE, DEFAULT_SANDBOX_BROWSER_NETWORK, DEFAULT_SANDBOX_BROWSER_NOVNC_PORT, DEFAULT_SANDBOX_BROWSER_PREFIX, DEFAULT_SANDBOX_BROWSER_VNC_PORT, DEFAULT_SANDBOX_CONTAINER_PREFIX, DEFAULT_SANDBOX_IDLE_HOURS, DEFAULT_SANDBOX_IMAGE, DEFAULT_SANDBOX_MAX_AGE_DAYS, DEFAULT_SANDBOX_WORKDIR, DEFAULT_SANDBOX_WORKSPACE_ROOT, } from "./constants.js";
|
|
3
3
|
import { resolveSandboxToolPolicyForAgent } from "./tool-policy.js";
|
|
4
|
+
export function resolveSandboxBrowserDockerCreateConfig(params) {
|
|
5
|
+
const browserNetwork = params.browser.network.trim();
|
|
6
|
+
const base = {
|
|
7
|
+
...params.docker,
|
|
8
|
+
// Browser container needs network access for Chrome, downloads, etc.
|
|
9
|
+
network: browserNetwork || DEFAULT_SANDBOX_BROWSER_NETWORK,
|
|
10
|
+
// For hashing and consistency, treat browser image as the docker image even though we
|
|
11
|
+
// pass it separately as the final `docker create` argument.
|
|
12
|
+
image: params.browser.image,
|
|
13
|
+
};
|
|
14
|
+
return params.browser.binds !== undefined ? { ...base, binds: params.browser.binds } : base;
|
|
15
|
+
}
|
|
4
16
|
export function resolveSandboxScope(params) {
|
|
5
|
-
if (params.scope)
|
|
17
|
+
if (params.scope) {
|
|
6
18
|
return params.scope;
|
|
19
|
+
}
|
|
7
20
|
if (typeof params.perSession === "boolean") {
|
|
8
21
|
return params.perSession ? "session" : "shared";
|
|
9
22
|
}
|
|
@@ -47,13 +60,18 @@ export function resolveSandboxDockerConfig(params) {
|
|
|
47
60
|
export function resolveSandboxBrowserConfig(params) {
|
|
48
61
|
const agentBrowser = params.scope === "shared" ? undefined : params.agentBrowser;
|
|
49
62
|
const globalBrowser = params.globalBrowser;
|
|
63
|
+
const binds = [...(globalBrowser?.binds ?? []), ...(agentBrowser?.binds ?? [])];
|
|
64
|
+
// Treat `binds: []` as an explicit override, so it can disable `docker.binds` for the browser container.
|
|
65
|
+
const bindsConfigured = globalBrowser?.binds !== undefined || agentBrowser?.binds !== undefined;
|
|
50
66
|
return {
|
|
51
67
|
enabled: agentBrowser?.enabled ?? globalBrowser?.enabled ?? false,
|
|
52
68
|
image: agentBrowser?.image ?? globalBrowser?.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE,
|
|
53
69
|
containerPrefix: agentBrowser?.containerPrefix ??
|
|
54
70
|
globalBrowser?.containerPrefix ??
|
|
55
71
|
DEFAULT_SANDBOX_BROWSER_PREFIX,
|
|
72
|
+
network: agentBrowser?.network ?? globalBrowser?.network ?? DEFAULT_SANDBOX_BROWSER_NETWORK,
|
|
56
73
|
cdpPort: agentBrowser?.cdpPort ?? globalBrowser?.cdpPort ?? DEFAULT_SANDBOX_BROWSER_CDP_PORT,
|
|
74
|
+
cdpSourceRange: agentBrowser?.cdpSourceRange ?? globalBrowser?.cdpSourceRange,
|
|
57
75
|
vncPort: agentBrowser?.vncPort ?? globalBrowser?.vncPort ?? DEFAULT_SANDBOX_BROWSER_VNC_PORT,
|
|
58
76
|
noVncPort: agentBrowser?.noVncPort ?? globalBrowser?.noVncPort ?? DEFAULT_SANDBOX_BROWSER_NOVNC_PORT,
|
|
59
77
|
headless: agentBrowser?.headless ?? globalBrowser?.headless ?? false,
|
|
@@ -63,6 +81,7 @@ export function resolveSandboxBrowserConfig(params) {
|
|
|
63
81
|
autoStartTimeoutMs: agentBrowser?.autoStartTimeoutMs ??
|
|
64
82
|
globalBrowser?.autoStartTimeoutMs ??
|
|
65
83
|
DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS,
|
|
84
|
+
binds: bindsConfigured ? binds : undefined,
|
|
66
85
|
};
|
|
67
86
|
}
|
|
68
87
|
export function resolveSandboxPruneConfig(params) {
|
|
@@ -43,3 +43,5 @@ const resolvedSandboxStateDir = STATE_DIR ?? path.join(os.homedir(), ".poolbot")
|
|
|
43
43
|
export const SANDBOX_STATE_DIR = path.join(resolvedSandboxStateDir, "sandbox");
|
|
44
44
|
export const SANDBOX_REGISTRY_PATH = path.join(SANDBOX_STATE_DIR, "containers.json");
|
|
45
45
|
export const SANDBOX_BROWSER_REGISTRY_PATH = path.join(SANDBOX_STATE_DIR, "browsers.json");
|
|
46
|
+
export const SANDBOX_BROWSER_SECURITY_HASH_EPOCH = "2026-02-21-novnc-auth-default";
|
|
47
|
+
export const DEFAULT_SANDBOX_BROWSER_NETWORK = "poolbot-sandbox-browser";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|
2
3
|
import { sanitizeEnvVars } from "./sanitize-env-vars.js";
|
|
3
4
|
function createAbortError() {
|
|
4
5
|
const err = new Error("Aborted");
|
|
@@ -82,6 +83,7 @@ import { DEFAULT_SANDBOX_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constant
|
|
|
82
83
|
import { readRegistry, updateRegistry } from "./registry.js";
|
|
83
84
|
import { resolveSandboxAgentId, resolveSandboxScopeKey, slugifySessionKey } from "./shared.js";
|
|
84
85
|
import { validateSandboxSecurity } from "./validate-sandbox-security.js";
|
|
86
|
+
const log = createSubsystemLogger("docker");
|
|
85
87
|
const HOT_CONTAINER_WINDOW_MS = 5 * 60 * 1000;
|
|
86
88
|
export async function execDocker(args, opts) {
|
|
87
89
|
const result = await execDockerRaw(args, opts);
|
|
@@ -102,6 +104,18 @@ export async function readDockerContainerLabel(containerName, label) {
|
|
|
102
104
|
}
|
|
103
105
|
return raw;
|
|
104
106
|
}
|
|
107
|
+
export async function readDockerContainerEnvVar(containerName, envVar) {
|
|
108
|
+
const result = await execDocker(["inspect", "-f", "{{range .Config.Env}}{{println .}}{{end}}", containerName], { allowFailure: true });
|
|
109
|
+
if (result.code !== 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
113
|
+
if (line.startsWith(`${envVar}=`)) {
|
|
114
|
+
return line.slice(envVar.length + 1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
105
119
|
export async function readDockerPort(containerName, port) {
|
|
106
120
|
const result = await execDocker(["port", containerName, `${port}/tcp`], {
|
|
107
121
|
allowFailure: true,
|
|
@@ -212,10 +226,10 @@ export function buildSandboxCreateArgs(params) {
|
|
|
212
226
|
}
|
|
213
227
|
const envSanitization = sanitizeEnvVars(params.cfg.env ?? {});
|
|
214
228
|
if (envSanitization.blocked.length > 0) {
|
|
215
|
-
|
|
229
|
+
log.warn(`Blocked sensitive environment variables: ${envSanitization.blocked.join(", ")}`);
|
|
216
230
|
}
|
|
217
231
|
if (envSanitization.warnings.length > 0) {
|
|
218
|
-
|
|
232
|
+
log.warn(`Suspicious environment variables: ${envSanitization.warnings.join(", ")}`);
|
|
219
233
|
}
|
|
220
234
|
for (const [key, value] of Object.entries(envSanitization.allowed)) {
|
|
221
235
|
args.push("--env", `${key}=${value}`);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
export const NOVNC_PASSWORD_ENV_KEY = "POOLBOT_BROWSER_NOVNC_PASSWORD";
|
|
3
|
+
const NOVNC_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
4
|
+
const NO_VNC_OBSERVER_TOKENS = new Map();
|
|
5
|
+
function pruneExpiredNoVncObserverTokens(now) {
|
|
6
|
+
for (const [token, entry] of NO_VNC_OBSERVER_TOKENS) {
|
|
7
|
+
if (entry.expiresAt <= now) {
|
|
8
|
+
NO_VNC_OBSERVER_TOKENS.delete(token);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function isNoVncEnabled(params) {
|
|
13
|
+
return params.enableNoVnc && !params.headless;
|
|
14
|
+
}
|
|
15
|
+
export function generateNoVncPassword() {
|
|
16
|
+
// VNC auth uses an 8-char password max.
|
|
17
|
+
return crypto.randomBytes(4).toString("hex");
|
|
18
|
+
}
|
|
19
|
+
export function buildNoVncDirectUrl(port, password) {
|
|
20
|
+
const query = new URLSearchParams({
|
|
21
|
+
autoconnect: "1",
|
|
22
|
+
resize: "remote",
|
|
23
|
+
});
|
|
24
|
+
if (password?.trim()) {
|
|
25
|
+
query.set("password", password);
|
|
26
|
+
}
|
|
27
|
+
return `http://127.0.0.1:${port}/vnc.html?${query.toString()}`;
|
|
28
|
+
}
|
|
29
|
+
export function issueNoVncObserverToken(params) {
|
|
30
|
+
const now = params.nowMs ?? Date.now();
|
|
31
|
+
pruneExpiredNoVncObserverTokens(now);
|
|
32
|
+
const token = crypto.randomBytes(24).toString("hex");
|
|
33
|
+
NO_VNC_OBSERVER_TOKENS.set(token, {
|
|
34
|
+
url: params.url,
|
|
35
|
+
expiresAt: now + Math.max(1, params.ttlMs ?? NOVNC_TOKEN_TTL_MS),
|
|
36
|
+
});
|
|
37
|
+
return token;
|
|
38
|
+
}
|
|
39
|
+
export function consumeNoVncObserverToken(token, nowMs) {
|
|
40
|
+
const now = nowMs ?? Date.now();
|
|
41
|
+
pruneExpiredNoVncObserverTokens(now);
|
|
42
|
+
const normalized = token.trim();
|
|
43
|
+
if (!normalized) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const entry = NO_VNC_OBSERVER_TOKENS.get(normalized);
|
|
47
|
+
if (!entry) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
NO_VNC_OBSERVER_TOKENS.delete(normalized);
|
|
51
|
+
if (entry.expiresAt <= now) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return entry.url;
|
|
55
|
+
}
|
|
56
|
+
export function buildNoVncObserverTokenUrl(baseUrl, token) {
|
|
57
|
+
const query = new URLSearchParams({ token });
|
|
58
|
+
return `${baseUrl}/sandbox/novnc?${query.toString()}`;
|
|
59
|
+
}
|
|
60
|
+
export function resetNoVncObserverTokensForTests() {
|
|
61
|
+
NO_VNC_OBSERVER_TOKENS.clear();
|
|
62
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
|
4
3
|
import { resolveUserPath } from "../../utils.js";
|
|
5
4
|
import { resolveAgentIdFromSessionKey } from "../agent-scope.js";
|
|
5
|
+
import { hashTextSha256 } from "./hash.js";
|
|
6
6
|
export function slugifySessionKey(value) {
|
|
7
7
|
const trimmed = value.trim() || "session";
|
|
8
|
-
const hash =
|
|
8
|
+
const hash = hashTextSha256(trimmed).slice(0, 8);
|
|
9
9
|
const safe = trimmed
|
|
10
10
|
.toLowerCase()
|
|
11
11
|
.replace(/[^a-z0-9._-]+/g, "-")
|
|
@@ -20,19 +20,23 @@ export function resolveSandboxWorkspaceDir(root, sessionKey) {
|
|
|
20
20
|
}
|
|
21
21
|
export function resolveSandboxScopeKey(scope, sessionKey) {
|
|
22
22
|
const trimmed = sessionKey.trim() || "main";
|
|
23
|
-
if (scope === "shared")
|
|
23
|
+
if (scope === "shared") {
|
|
24
24
|
return "shared";
|
|
25
|
-
|
|
25
|
+
}
|
|
26
|
+
if (scope === "session") {
|
|
26
27
|
return trimmed;
|
|
28
|
+
}
|
|
27
29
|
const agentId = resolveAgentIdFromSessionKey(trimmed);
|
|
28
30
|
return `agent:${agentId}`;
|
|
29
31
|
}
|
|
30
32
|
export function resolveSandboxAgentId(scopeKey) {
|
|
31
33
|
const trimmed = scopeKey.trim();
|
|
32
|
-
if (!trimmed || trimmed === "shared")
|
|
34
|
+
if (!trimmed || trimmed === "shared") {
|
|
33
35
|
return undefined;
|
|
36
|
+
}
|
|
34
37
|
const parts = trimmed.split(":").filter(Boolean);
|
|
35
|
-
if (parts[0] === "agent" && parts[1])
|
|
38
|
+
if (parts[0] === "agent" && parts[1]) {
|
|
36
39
|
return normalizeAgentId(parts[1]);
|
|
40
|
+
}
|
|
37
41
|
return resolveAgentIdFromSessionKey(trimmed);
|
|
38
42
|
}
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { isNotFoundPathError, isPathInside } from "../infra/path-guards.js";
|
|
5
6
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
6
7
|
const HTTP_URL_RE = /^https?:\/\//i;
|
|
7
8
|
const DATA_URL_RE = /^data:/i;
|
|
@@ -70,12 +71,32 @@ export async function resolveSandboxedMediaSource(params) {
|
|
|
70
71
|
throw new Error(`Invalid file:// URL for sandboxed media: ${raw}`);
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
|
-
const
|
|
74
|
+
const tmpMediaPath = await resolveAllowedTmpMediaPath({
|
|
75
|
+
candidate,
|
|
76
|
+
sandboxRoot: params.sandboxRoot,
|
|
77
|
+
});
|
|
78
|
+
if (tmpMediaPath) {
|
|
79
|
+
return tmpMediaPath;
|
|
80
|
+
}
|
|
81
|
+
const sandboxResult = await assertSandboxPath({
|
|
74
82
|
filePath: candidate,
|
|
75
83
|
cwd: params.sandboxRoot,
|
|
76
84
|
root: params.sandboxRoot,
|
|
77
85
|
});
|
|
78
|
-
return
|
|
86
|
+
return sandboxResult.resolved;
|
|
87
|
+
}
|
|
88
|
+
async function resolveAllowedTmpMediaPath(params) {
|
|
89
|
+
const candidateIsAbsolute = path.isAbsolute(expandPath(params.candidate));
|
|
90
|
+
if (!candidateIsAbsolute) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const resolved = path.resolve(resolveSandboxInputPath(params.candidate, params.sandboxRoot));
|
|
94
|
+
const tmpDir = path.resolve(os.tmpdir());
|
|
95
|
+
if (!isPathInside(tmpDir, resolved)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
await assertNoSymlinkEscape(path.relative(tmpDir, resolved), tmpDir);
|
|
99
|
+
return resolved;
|
|
79
100
|
}
|
|
80
101
|
async function assertNoSymlinkEscape(relative, root, options) {
|
|
81
102
|
if (!relative) {
|
|
@@ -104,8 +125,7 @@ async function assertNoSymlinkEscape(relative, root, options) {
|
|
|
104
125
|
}
|
|
105
126
|
}
|
|
106
127
|
catch (err) {
|
|
107
|
-
|
|
108
|
-
if (anyErr.code === "ENOENT") {
|
|
128
|
+
if (isNotFoundPathError(err)) {
|
|
109
129
|
return;
|
|
110
130
|
}
|
|
111
131
|
throw err;
|
|
@@ -120,13 +140,6 @@ async function tryRealpath(value) {
|
|
|
120
140
|
return path.resolve(value);
|
|
121
141
|
}
|
|
122
142
|
}
|
|
123
|
-
function isPathInside(root, target) {
|
|
124
|
-
const relative = path.relative(root, target);
|
|
125
|
-
if (!relative || relative === "") {
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
return !(relative.startsWith("..") || path.isAbsolute(relative));
|
|
129
|
-
}
|
|
130
143
|
function shortPath(value) {
|
|
131
144
|
if (value.startsWith(os.homedir())) {
|
|
132
145
|
return `~${value.slice(os.homedir().length)}`;
|