@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +1 -1
- package/dist/cli.js +4685 -4614
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +374 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +48 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +35 -0
- package/dist/types/live/visualizer.d.ts +30 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +321 -0
- package/dist/types/session/agent-session.d.ts +135 -753
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +51 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +24 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +158 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +7 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +10 -29
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/usage-cli.ts +10 -1
- package/src/cli.ts +8 -0
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +16 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +270 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +90 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +517 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +422 -0
- package/src/live/visualizer.ts +214 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +28 -112
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +255 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +80 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +176 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +215 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +343 -0
- package/src/session/agent-session.ts +1940 -12218
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +217 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +266 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +980 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +257 -20
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +45 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +116 -86
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +9 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +121 -29
- package/src/tts/streaming-player.ts +82 -283
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
package/src/tools/bash.ts
CHANGED
|
@@ -10,13 +10,17 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
10
10
|
import { ImageProtocol, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
11
11
|
import { getProjectDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import { type } from "arktype";
|
|
13
|
-
import { type BashResult, executeBash } from "../exec/bash-executor";
|
|
13
|
+
import { applyDirenvPreflight, type BashResult, executeBash } from "../exec/bash-executor";
|
|
14
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
15
15
|
import { InternalUrlRouter } from "../internal-urls";
|
|
16
16
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
17
17
|
import { highlightCode, type Theme } from "../modes/theme/theme";
|
|
18
18
|
import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
ClientBridgeTerminalExitStatus,
|
|
21
|
+
ClientBridgeTerminalHandle,
|
|
22
|
+
ClientBridgeTerminalOutput,
|
|
23
|
+
} from "../session/client-bridge";
|
|
20
24
|
import { DEFAULT_MAX_BYTES, enforceInlineByteCap, streamTailUpdates, TailBuffer } from "../session/streaming-output";
|
|
21
25
|
import { renderStatusLine } from "../tui";
|
|
22
26
|
import { CachedOutputBlock, markFramedBlockComponent, outputBlockContentWidth } from "../tui/output-block";
|
|
@@ -51,6 +55,8 @@ export const BASH_DEFAULT_PREVIEW_LINES = DEFAULT_TERMINAL_PREVIEW_LINES;
|
|
|
51
55
|
|
|
52
56
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
53
57
|
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
58
|
+
const BASH_APPROVAL_SHELL_CONTROL_RE = /[\n\r;&|<>`$()]/u;
|
|
59
|
+
const BASH_PATTERN_APPROVAL_VALUES = new Set(["allow", "deny", "prompt"]);
|
|
54
60
|
|
|
55
61
|
/**
|
|
56
62
|
* Shape a shell command line for an ACP-conformant `terminal/create` request.
|
|
@@ -127,6 +133,63 @@ export const CRITICAL_BASH_PATTERNS = [
|
|
|
127
133
|
/\bnc\b[^|;]*\s-[a-zA-Z]*[ec][a-zA-Z]*\s/i, // `nc -e` / `nc -c`.
|
|
128
134
|
] as const;
|
|
129
135
|
|
|
136
|
+
type BashPatternApproval = "allow" | "deny" | "prompt";
|
|
137
|
+
|
|
138
|
+
interface BashApprovalPatternRule {
|
|
139
|
+
match: string;
|
|
140
|
+
approval: BashPatternApproval;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeBashApprovalPattern(value: string): string {
|
|
144
|
+
return value.trim().replace(/\s+/gu, " ");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function bashApprovalPatternToRegExp(pattern: string): RegExp {
|
|
148
|
+
const escaped = normalizeBashApprovalPattern(pattern)
|
|
149
|
+
.split("*")
|
|
150
|
+
.map(part => part.replace(/[\\^$+?.()|[\]{}]/gu, "\\$&"))
|
|
151
|
+
.join(".*");
|
|
152
|
+
return new RegExp(`^${escaped}$`, "u");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeBashPatternApproval(value: unknown): BashPatternApproval | undefined {
|
|
156
|
+
if (typeof value !== "string") return undefined;
|
|
157
|
+
const normalized = value.trim().toLowerCase();
|
|
158
|
+
return BASH_PATTERN_APPROVAL_VALUES.has(normalized) ? (normalized as BashPatternApproval) : undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getBashApprovalPatternRules(value: unknown): BashApprovalPatternRule[] {
|
|
162
|
+
if (!Array.isArray(value)) return [];
|
|
163
|
+
return value
|
|
164
|
+
.map(item => {
|
|
165
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return undefined;
|
|
166
|
+
const record = item as Record<string, unknown>;
|
|
167
|
+
if (typeof record.match !== "string") return undefined;
|
|
168
|
+
const match = normalizeBashApprovalPattern(record.match);
|
|
169
|
+
const approval = normalizeBashPatternApproval(record.approval);
|
|
170
|
+
return match.length > 0 && approval ? { match, approval } : undefined;
|
|
171
|
+
})
|
|
172
|
+
.filter((rule): rule is BashApprovalPatternRule => !!rule);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function commandMatchesBashApprovalPattern(command: string, pattern: string): boolean {
|
|
176
|
+
const normalizedCommand = normalizeBashApprovalPattern(command);
|
|
177
|
+
if (normalizedCommand.length === 0) return false;
|
|
178
|
+
return bashApprovalPatternToRegExp(pattern).test(normalizedCommand);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function findBashApprovalPatternRule(
|
|
182
|
+
command: string,
|
|
183
|
+
rules: readonly BashApprovalPatternRule[],
|
|
184
|
+
): BashApprovalPatternRule | undefined {
|
|
185
|
+
return rules.find(rule => {
|
|
186
|
+
if (rule.approval === "allow" && BASH_APPROVAL_SHELL_CONTROL_RE.test(command)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return commandMatchesBashApprovalPattern(command, rule.match);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
130
193
|
async function saveBashOriginalArtifact(session: ToolSession, originalText: string): Promise<string | undefined> {
|
|
131
194
|
try {
|
|
132
195
|
const alloc = await session.allocateOutputArtifact?.("bash-original");
|
|
@@ -384,9 +447,29 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
384
447
|
readonly approval = (args: unknown): ToolApprovalDecision => {
|
|
385
448
|
const rawCommand = (args as Partial<BashToolInput>).command;
|
|
386
449
|
const command = typeof rawCommand === "string" ? rawCommand : "";
|
|
450
|
+
const patternRules = getBashApprovalPatternRules(this.session.settings.get("bash.patterns"));
|
|
451
|
+
const patternRule = patternRules.find(rule => commandMatchesBashApprovalPattern(command, rule.match));
|
|
452
|
+
if (patternRule?.approval === "deny") {
|
|
453
|
+
return {
|
|
454
|
+
tier: "exec",
|
|
455
|
+
override: true,
|
|
456
|
+
policy: "deny",
|
|
457
|
+
reason: `Blocked by bash pattern: ${patternRule.match}`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
387
460
|
if (command !== "" && CRITICAL_BASH_PATTERNS.some(pattern => pattern.test(command))) {
|
|
388
461
|
return { tier: "exec", override: true, reason: "Critical pattern detected" };
|
|
389
462
|
}
|
|
463
|
+
const safePatternRule = findBashApprovalPatternRule(command, patternRules);
|
|
464
|
+
if (safePatternRule?.approval === "allow") return { tier: "write", policy: "allow" };
|
|
465
|
+
if (safePatternRule?.approval === "prompt") {
|
|
466
|
+
return {
|
|
467
|
+
tier: "exec",
|
|
468
|
+
override: true,
|
|
469
|
+
policy: "prompt",
|
|
470
|
+
reason: `Prompt required by bash pattern: ${safePatternRule.match}`,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
390
473
|
return "exec";
|
|
391
474
|
};
|
|
392
475
|
readonly formatApprovalDetails = (args: unknown): string[] => {
|
|
@@ -703,28 +786,40 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
703
786
|
job: ManagedBashJobHandle,
|
|
704
787
|
thresholdMs: number,
|
|
705
788
|
signal?: AbortSignal,
|
|
706
|
-
|
|
789
|
+
steeringSignal?: AbortSignal,
|
|
790
|
+
): Promise<ManagedBashJobCompletion | { kind: "running" } | { kind: "steer" } | { kind: "aborted" }> {
|
|
707
791
|
if (signal?.aborted) {
|
|
708
792
|
return { kind: "aborted" };
|
|
709
793
|
}
|
|
794
|
+
if (steeringSignal?.aborted) {
|
|
795
|
+
return { kind: "steer" };
|
|
796
|
+
}
|
|
710
797
|
|
|
711
|
-
const waiters: Array<
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
];
|
|
798
|
+
const waiters: Array<
|
|
799
|
+
Promise<ManagedBashJobCompletion | { kind: "running" } | { kind: "steer" } | { kind: "aborted" }>
|
|
800
|
+
> = [job.completion, Bun.sleep(thresholdMs).then(() => ({ kind: "running" as const }))];
|
|
715
801
|
|
|
716
|
-
if (!signal) {
|
|
802
|
+
if (!signal && !steeringSignal) {
|
|
717
803
|
return await Promise.race(waiters);
|
|
718
804
|
}
|
|
719
805
|
|
|
720
806
|
const { promise: abortedPromise, resolve: resolveAborted } = Promise.withResolvers<{ kind: "aborted" }>();
|
|
721
807
|
const onAbort = () => resolveAborted({ kind: "aborted" });
|
|
722
|
-
|
|
723
|
-
|
|
808
|
+
const { promise: steerPromise, resolve: resolveSteer } = Promise.withResolvers<{ kind: "steer" }>();
|
|
809
|
+
const onSteer = () => resolveSteer({ kind: "steer" });
|
|
810
|
+
if (signal) {
|
|
811
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
812
|
+
waiters.push(abortedPromise);
|
|
813
|
+
}
|
|
814
|
+
if (steeringSignal) {
|
|
815
|
+
steeringSignal.addEventListener("abort", onSteer, { once: true });
|
|
816
|
+
waiters.push(steerPromise);
|
|
817
|
+
}
|
|
724
818
|
try {
|
|
725
819
|
return await Promise.race(waiters);
|
|
726
820
|
} finally {
|
|
727
|
-
signal
|
|
821
|
+
signal?.removeEventListener("abort", onAbort);
|
|
822
|
+
steeringSignal?.removeEventListener("abort", onSteer);
|
|
728
823
|
}
|
|
729
824
|
}
|
|
730
825
|
|
|
@@ -911,7 +1006,12 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
911
1006
|
// foreground-wait cannot also be injected by the delivery loop. Lifted
|
|
912
1007
|
// via resumeDeliveries() if we end up backgrounding after all.
|
|
913
1008
|
autoBgManager.acknowledgeDeliveries([job.jobId]);
|
|
914
|
-
const waitResult = await this.#waitForManagedBashJob(
|
|
1009
|
+
const waitResult = await this.#waitForManagedBashJob(
|
|
1010
|
+
job,
|
|
1011
|
+
autoBackgroundWaitMs,
|
|
1012
|
+
signal,
|
|
1013
|
+
ctx?.toolCall?.steeringSignal,
|
|
1014
|
+
);
|
|
915
1015
|
if (waitResult.kind === "completed") {
|
|
916
1016
|
return waitResult.result;
|
|
917
1017
|
}
|
|
@@ -924,52 +1024,94 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
924
1024
|
}
|
|
925
1025
|
job.stopUpdates();
|
|
926
1026
|
autoBgManager.resumeDeliveries([job.jobId]);
|
|
1027
|
+
// "steer": a queued user/peer message arrived mid-wait — background
|
|
1028
|
+
// the command (it keeps running) so the message injects promptly.
|
|
1029
|
+
const notices =
|
|
1030
|
+
waitResult.kind === "steer"
|
|
1031
|
+
? [...pendingNotices, "Backgrounded early to handle an incoming message; the command keeps running."]
|
|
1032
|
+
: pendingNotices;
|
|
927
1033
|
return this.#buildBackgroundStartResult(job.jobId, job.getLatestText(), timeoutSec, {
|
|
928
1034
|
requestedTimeoutSec,
|
|
929
|
-
notices
|
|
1035
|
+
notices,
|
|
930
1036
|
});
|
|
931
1037
|
}
|
|
932
1038
|
|
|
1039
|
+
// Fold direnv/devenv env into (command, env) ONCE for the two backends
|
|
1040
|
+
// that bypass `executeBash` — the ACP client terminal and the PTY. The
|
|
1041
|
+
// `executeBash` branch below is intentionally excluded: it runs its own
|
|
1042
|
+
// preflight internally, so routing the pre-applied command there too
|
|
1043
|
+
// would double-apply the unset prefix and re-merge the env. No
|
|
1044
|
+
// `commandPrefix` here: ACP applies the shell prefix via
|
|
1045
|
+
// `wrapShellLineForClientTerminal`, and the PTY path never wrapped one.
|
|
1046
|
+
// `callerTimeoutMs` clamps the direnv load to a positive command timeout
|
|
1047
|
+
// (the backend's own timeout is installed only after this await), matching
|
|
1048
|
+
// the executeBash branch so a cold `.envrc` can't outlast a short call.
|
|
1049
|
+
const backendPreflight =
|
|
1050
|
+
(clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) ||
|
|
1051
|
+
canUseInteractiveBashPty(pty, ctx)
|
|
1052
|
+
? await applyDirenvPreflight(command, commandCwd, {
|
|
1053
|
+
callerEnv: resolvedEnv,
|
|
1054
|
+
signal,
|
|
1055
|
+
timeoutMs: this.session.settings.get("bash.direnvLoadTimeoutMs"),
|
|
1056
|
+
callerTimeoutMs: timeoutMs,
|
|
1057
|
+
direnvSetting: this.session.settings.get("bash.direnv"),
|
|
1058
|
+
})
|
|
1059
|
+
: undefined;
|
|
1060
|
+
|
|
933
1061
|
// Route through the client terminal when the client advertises the terminal capability.
|
|
934
1062
|
// Skip when pty=true (PTY needs the local terminal UI).
|
|
935
1063
|
if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
: undefined,
|
|
945
|
-
outputByteLimit: DEFAULT_MAX_BYTES,
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
// Emit partial update so the editor can embed the live terminal card.
|
|
949
|
-
onUpdate?.({ content: [], details: { terminalId: handle.terminalId } });
|
|
950
|
-
|
|
951
|
-
const exitPromise = handle.waitForExit();
|
|
952
|
-
let exitStatus!: ClientBridgeTerminalExitStatus;
|
|
953
|
-
|
|
954
|
-
type BridgeRaceResult =
|
|
955
|
-
| { kind: "exit"; status: ClientBridgeTerminalExitStatus }
|
|
956
|
-
| { kind: "poll" }
|
|
957
|
-
| { kind: "timeout" }
|
|
958
|
-
| { kind: "aborted" };
|
|
1064
|
+
// Invariant (ACP terminal bridge): createTerminal has no signal in its
|
|
1065
|
+
// contract; allocation cannot be cancelled retroactively. Guard before
|
|
1066
|
+
// allocation. Shared timeout helper / pure AbortSignal fusion rejected:
|
|
1067
|
+
// we need explicit kill-before-read ordering and distinct abort vs
|
|
1068
|
+
// timeout result shapes. Per-route race retained for testability.
|
|
1069
|
+
if (signal?.aborted) {
|
|
1070
|
+
throw new ToolAbortError("Command aborted");
|
|
1071
|
+
}
|
|
959
1072
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
//
|
|
1073
|
+
const bridgeWallTimeStart = performance.now();
|
|
1074
|
+
const killGraceMs = 1000;
|
|
1075
|
+
const outputSnapshotGraceMs = 2000;
|
|
1076
|
+
// Cancellable timeout: a bare Bun.sleep(timeoutMs) would leave a live,
|
|
1077
|
+
// ref'd timer for the full command timeout after fast completions —
|
|
1078
|
+
// accumulating timers and delaying process shutdown in SDK/headless use.
|
|
1079
|
+
// `timeoutMs` is optional (#4642): without one, no timer is armed and
|
|
1080
|
+
// the promise simply never resolves.
|
|
1081
|
+
const { promise: timeoutPromise, resolve: resolveTimeout } = Promise.withResolvers<{
|
|
1082
|
+
kind: "timeout";
|
|
1083
|
+
}>();
|
|
1084
|
+
const timeoutTimer = timeoutMs ? setTimeout(() => resolveTimeout({ kind: "timeout" }), timeoutMs) : undefined;
|
|
964
1085
|
const { promise: abortedP, resolve: resolveAborted } = Promise.withResolvers<void>();
|
|
1086
|
+
let handle: ClientBridgeTerminalHandle | undefined;
|
|
965
1087
|
let killStarted = false;
|
|
966
1088
|
const fireKill = (): Promise<void> => {
|
|
967
1089
|
if (killStarted) return Promise.resolve();
|
|
1090
|
+
const currentHandle = handle;
|
|
1091
|
+
if (!currentHandle) return Promise.resolve();
|
|
968
1092
|
killStarted = true;
|
|
969
|
-
return
|
|
970
|
-
logger.warn("ACP terminal kill failed", { terminalId:
|
|
1093
|
+
return currentHandle.kill().catch((error: unknown) => {
|
|
1094
|
+
logger.warn("ACP terminal kill failed", { terminalId: currentHandle.terminalId, error });
|
|
971
1095
|
});
|
|
972
1096
|
};
|
|
1097
|
+
const cleanupLateCreate = (createP: Promise<ClientBridgeTerminalHandle>): void => {
|
|
1098
|
+
void createP
|
|
1099
|
+
.then(async lateHandle => {
|
|
1100
|
+
try {
|
|
1101
|
+
await lateHandle.kill();
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
logger.warn("ACP terminal kill failed", { terminalId: lateHandle.terminalId, error });
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
await lateHandle.release();
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
logger.warn("ACP terminal release failed", { terminalId: lateHandle.terminalId, error });
|
|
1109
|
+
}
|
|
1110
|
+
})
|
|
1111
|
+
.catch((error: unknown) => {
|
|
1112
|
+
logger.warn("ACP terminal create failed after cancellation", { error });
|
|
1113
|
+
});
|
|
1114
|
+
};
|
|
973
1115
|
const onAbortSignal = () => {
|
|
974
1116
|
resolveAborted();
|
|
975
1117
|
void fireKill();
|
|
@@ -977,90 +1119,150 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
977
1119
|
signal?.addEventListener("abort", onAbortSignal, { once: true });
|
|
978
1120
|
|
|
979
1121
|
try {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1122
|
+
// direnv-transformed command (carries any `unset -v` prefix) + merged
|
|
1123
|
+
// env; falls back to the raw command/env when direnv is off/absent.
|
|
1124
|
+
const bridgeCommand = backendPreflight?.command ?? command;
|
|
1125
|
+
const bridgeEnv = backendPreflight?.env ?? resolvedEnv;
|
|
1126
|
+
const shellSpawn = wrapShellLineForClientTerminal(bridgeCommand, this.session.settings.getShellConfig());
|
|
1127
|
+
const createP = clientBridge.createTerminal({
|
|
1128
|
+
command: shellSpawn.command,
|
|
1129
|
+
args: shellSpawn.args,
|
|
1130
|
+
cwd: commandCwd,
|
|
1131
|
+
env: bridgeEnv
|
|
1132
|
+
? Object.entries(bridgeEnv).map(([name, value]) => ({ name, value: value as string }))
|
|
1133
|
+
: undefined,
|
|
1134
|
+
outputByteLimit: DEFAULT_MAX_BYTES,
|
|
1135
|
+
});
|
|
1136
|
+
const createRaced = await Promise.race([
|
|
1137
|
+
createP.then(createdHandle => ({ kind: "created" as const, handle: createdHandle })),
|
|
1138
|
+
timeoutPromise,
|
|
1139
|
+
abortedP.then(() => ({ kind: "aborted" as const })),
|
|
1140
|
+
]);
|
|
1141
|
+
if (createRaced.kind === "aborted" || signal?.aborted) {
|
|
1142
|
+
cleanupLateCreate(createP);
|
|
1143
|
+
throw new ToolAbortError("Command aborted");
|
|
1144
|
+
}
|
|
1145
|
+
if (createRaced.kind === "timeout") {
|
|
1146
|
+
cleanupLateCreate(createP);
|
|
1147
|
+
const timedOutResult: BashInteractiveResult = {
|
|
1148
|
+
output: "",
|
|
1149
|
+
exitCode: undefined,
|
|
1150
|
+
cancelled: false,
|
|
1151
|
+
timedOut: true,
|
|
1152
|
+
truncated: false,
|
|
1153
|
+
totalLines: 0,
|
|
1154
|
+
totalBytes: 0,
|
|
1155
|
+
outputLines: 0,
|
|
1156
|
+
outputBytes: 0,
|
|
1157
|
+
};
|
|
1158
|
+
this.#throwIfUnfinished(timedOutResult, timeoutSec, this.#formatResultOutput(timedOutResult));
|
|
1159
|
+
throw new ToolError("Command timed out");
|
|
1160
|
+
}
|
|
985
1161
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1162
|
+
handle = createRaced.handle;
|
|
1163
|
+
|
|
1164
|
+
// Emit partial update so the editor can embed the live terminal card.
|
|
1165
|
+
onUpdate?.({ content: [], details: { terminalId: handle.terminalId } });
|
|
1166
|
+
|
|
1167
|
+
const exitPromise = handle.waitForExit();
|
|
1168
|
+
let exitStatus!: ClientBridgeTerminalExitStatus;
|
|
1169
|
+
|
|
1170
|
+
type BridgeRaceResult =
|
|
1171
|
+
| { kind: "exit"; status: ClientBridgeTerminalExitStatus }
|
|
1172
|
+
| { kind: "poll" }
|
|
1173
|
+
| { kind: "timeout" }
|
|
1174
|
+
| { kind: "aborted" };
|
|
1175
|
+
|
|
1176
|
+
const exitRacer = exitPromise.then(status => ({ kind: "exit" as const, status }));
|
|
1177
|
+
const abortRacer = abortedP.then(() => ({ kind: "aborted" as const }));
|
|
1178
|
+
const abortPollRacer = abortedP.then(() => undefined as ClientBridgeTerminalOutput | undefined);
|
|
1179
|
+
const timeoutPollRacer = timeoutPromise.then(() => undefined as ClientBridgeTerminalOutput | undefined);
|
|
1180
|
+
let lastPolledOutput: ClientBridgeTerminalOutput = { output: "", truncated: false };
|
|
1181
|
+
|
|
1182
|
+
// Poll until the process exits, times out, or the caller aborts.
|
|
1183
|
+
for (;;) {
|
|
1184
|
+
const racers: Array<Promise<BridgeRaceResult>> = [
|
|
1185
|
+
exitRacer,
|
|
1186
|
+
timeoutPromise,
|
|
1187
|
+
Bun.sleep(250).then(() => ({ kind: "poll" as const })),
|
|
1188
|
+
];
|
|
1189
|
+
if (signal) {
|
|
1190
|
+
racers.push(abortRacer);
|
|
1191
|
+
}
|
|
1192
|
+
const raced = await Promise.race(racers);
|
|
1000
1193
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1194
|
+
if (raced.kind === "aborted" || signal?.aborted) {
|
|
1195
|
+
await Promise.race([fireKill(), Bun.sleep(killGraceMs)]);
|
|
1196
|
+
throw new ToolAbortError("Command aborted");
|
|
1197
|
+
}
|
|
1005
1198
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
exitCode: undefined,
|
|
1024
|
-
cancelled: false,
|
|
1025
|
-
timedOut: true,
|
|
1026
|
-
truncated: current.truncated,
|
|
1027
|
-
totalLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
1028
|
-
totalBytes: current.output.length,
|
|
1029
|
-
outputLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
1030
|
-
outputBytes: current.output.length,
|
|
1031
|
-
};
|
|
1032
|
-
this.#throwIfUnfinished(timedOutResult, timeoutSec, this.#formatResultOutput(timedOutResult));
|
|
1033
|
-
throw new ToolError("Command timed out");
|
|
1199
|
+
if (raced.kind === "timeout") {
|
|
1200
|
+
// Kill before reading final output so a slow `terminal/output`
|
|
1201
|
+
// RPC cannot let a timed-out command keep running past the
|
|
1202
|
+
// enforced timeout. The handle stays valid post-kill so the
|
|
1203
|
+
// buffered output is still readable.
|
|
1204
|
+
await Promise.race([fireKill(), Bun.sleep(killGraceMs)]);
|
|
1205
|
+
let current = lastPolledOutput;
|
|
1206
|
+
try {
|
|
1207
|
+
current = await Promise.race([
|
|
1208
|
+
handle.currentOutput(),
|
|
1209
|
+
Bun.sleep(outputSnapshotGraceMs).then(() => lastPolledOutput),
|
|
1210
|
+
]);
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
logger.warn("ACP terminal final output read failed", {
|
|
1213
|
+
terminalId: handle.terminalId,
|
|
1214
|
+
error,
|
|
1215
|
+
});
|
|
1034
1216
|
}
|
|
1217
|
+
const timedOutResult: BashInteractiveResult = {
|
|
1218
|
+
output: current.output,
|
|
1219
|
+
exitCode: undefined,
|
|
1220
|
+
cancelled: false,
|
|
1221
|
+
timedOut: true,
|
|
1222
|
+
truncated: current.truncated,
|
|
1223
|
+
totalLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
1224
|
+
totalBytes: current.output.length,
|
|
1225
|
+
outputLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
1226
|
+
outputBytes: current.output.length,
|
|
1227
|
+
};
|
|
1228
|
+
this.#throwIfUnfinished(timedOutResult, timeoutSec, this.#formatResultOutput(timedOutResult));
|
|
1229
|
+
throw new ToolError("Command timed out");
|
|
1230
|
+
}
|
|
1035
1231
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1232
|
+
if (raced.kind === "exit") {
|
|
1233
|
+
exitStatus = raced.status;
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1040
1236
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
// Abort fired during the poll-tick read; let the next loop iteration
|
|
1050
|
-
// observe `signal?.aborted` and exit via the abort branch.
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
onUpdate?.({
|
|
1054
|
-
content: [{ type: "text", text: pollOutput.output }],
|
|
1055
|
-
details: { terminalId: handle.terminalId },
|
|
1056
|
-
});
|
|
1237
|
+
// Poll tick: push current output so agent-loop transcript stays consistent.
|
|
1238
|
+
// Race the read against abort/timeout so a stuck `terminal/output` RPC does
|
|
1239
|
+
// not delay cancellation or let the command outlive its deadline.
|
|
1240
|
+
const pollOutput = await Promise.race([handle.currentOutput(), abortPollRacer, timeoutPollRacer]);
|
|
1241
|
+
if (pollOutput === undefined) {
|
|
1242
|
+
// Abort or timeout fired during the poll-tick read; let the next loop
|
|
1243
|
+
// iteration exit via the matching abort/timeout branch.
|
|
1244
|
+
continue;
|
|
1057
1245
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1246
|
+
lastPolledOutput = pollOutput;
|
|
1247
|
+
onUpdate?.({
|
|
1248
|
+
content: [{ type: "text", text: pollOutput.output }],
|
|
1249
|
+
details: { terminalId: handle.terminalId },
|
|
1250
|
+
});
|
|
1060
1251
|
}
|
|
1061
1252
|
|
|
1062
1253
|
// Fetch final output; the terminal is released in the outer finally.
|
|
1063
|
-
|
|
1254
|
+
let finalOutput = lastPolledOutput;
|
|
1255
|
+
try {
|
|
1256
|
+
finalOutput = await Promise.race([
|
|
1257
|
+
handle.currentOutput(),
|
|
1258
|
+
Bun.sleep(outputSnapshotGraceMs).then(() => lastPolledOutput),
|
|
1259
|
+
]);
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
logger.warn("ACP terminal final output read failed", {
|
|
1262
|
+
terminalId: handle.terminalId,
|
|
1263
|
+
error,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1064
1266
|
|
|
1065
1267
|
// Map exit status: null exitCode with a signal → treat as signal kill (137).
|
|
1066
1268
|
const rawExitCode = exitStatus.exitCode;
|
|
@@ -1093,10 +1295,18 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
1093
1295
|
wallTimeMs: performance.now() - bridgeWallTimeStart,
|
|
1094
1296
|
});
|
|
1095
1297
|
} finally {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1298
|
+
clearTimeout(timeoutTimer);
|
|
1299
|
+
signal?.removeEventListener("abort", onAbortSignal);
|
|
1300
|
+
if (handle) {
|
|
1301
|
+
const releaseHandle = handle;
|
|
1302
|
+
// Bound release like kill/output: a hung `terminal/release` RPC must not
|
|
1303
|
+
// keep the tool pending after the result is already decided.
|
|
1304
|
+
await Promise.race([
|
|
1305
|
+
releaseHandle.release().catch((error: unknown) => {
|
|
1306
|
+
logger.warn("ACP terminal release failed", { terminalId: releaseHandle.terminalId, error });
|
|
1307
|
+
}),
|
|
1308
|
+
Bun.sleep(killGraceMs),
|
|
1309
|
+
]);
|
|
1100
1310
|
}
|
|
1101
1311
|
}
|
|
1102
1312
|
}
|
|
@@ -1114,15 +1324,21 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
1114
1324
|
const wallTimeStart = performance.now();
|
|
1115
1325
|
const result: BashResult | BashInteractiveResult = interactiveUi
|
|
1116
1326
|
? await runInteractiveBashPty(interactiveUi, {
|
|
1117
|
-
|
|
1327
|
+
// PTY bypasses executeBash, so feed it the direnv-transformed
|
|
1328
|
+
// command + merged env (backendPreflight is defined whenever this
|
|
1329
|
+
// branch runs, since both gate on canUseInteractiveBashPty).
|
|
1330
|
+
command: backendPreflight?.command ?? command,
|
|
1118
1331
|
cwd: commandCwd,
|
|
1119
1332
|
timeoutMs,
|
|
1120
1333
|
signal,
|
|
1121
|
-
env: resolvedEnv,
|
|
1334
|
+
env: backendPreflight?.env ?? resolvedEnv,
|
|
1122
1335
|
artifactPath,
|
|
1123
1336
|
artifactId,
|
|
1124
1337
|
})
|
|
1125
|
-
:
|
|
1338
|
+
: // executeBash runs its OWN direnv preflight internally — pass the RAW
|
|
1339
|
+
// command + resolvedEnv here so the unset prefix / env merge is not
|
|
1340
|
+
// applied twice.
|
|
1341
|
+
await executeBash(command, {
|
|
1126
1342
|
cwd: commandCwd,
|
|
1127
1343
|
sessionKey: this.session.getSessionId?.() ?? undefined,
|
|
1128
1344
|
timeout: timeoutMs ?? 0,
|
|
@@ -275,9 +275,14 @@ function resolveSystemChromium(): string | undefined {
|
|
|
275
275
|
return undefined;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
/** Options shared by headless Chromium consumers. */
|
|
278
279
|
export interface LaunchHeadlessOptions {
|
|
279
280
|
headless: boolean;
|
|
280
281
|
viewport?: { width: number; height: number; deviceScaleFactor?: number };
|
|
282
|
+
/** Additional Chromium arguments merged with the centralized launch defaults. */
|
|
283
|
+
args?: readonly string[];
|
|
284
|
+
/** Additional exact Puppeteer default arguments to suppress. */
|
|
285
|
+
ignoreDefaultArgs?: readonly string[];
|
|
281
286
|
}
|
|
282
287
|
|
|
283
288
|
export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promise<Browser> {
|
|
@@ -308,13 +313,16 @@ export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promis
|
|
|
308
313
|
if (ignoreCert === "true" || ignoreCert === "1" || ignoreCert === "yes" || ignoreCert === "on") {
|
|
309
314
|
launchArgs.push("--ignore-certificate-errors");
|
|
310
315
|
}
|
|
316
|
+
for (const arg of opts.args ?? []) {
|
|
317
|
+
if (!launchArgs.includes(arg)) launchArgs.push(arg);
|
|
318
|
+
}
|
|
311
319
|
const executablePath = await ensureChromiumExecutable();
|
|
312
320
|
return await puppeteer.launch({
|
|
313
321
|
headless: opts.headless,
|
|
314
322
|
defaultViewport: opts.headless ? initialViewport : null,
|
|
315
323
|
executablePath,
|
|
316
324
|
args: launchArgs,
|
|
317
|
-
ignoreDefaultArgs: stealthIgnoreDefaultArgs(executablePath),
|
|
325
|
+
ignoreDefaultArgs: [...new Set([...stealthIgnoreDefaultArgs(executablePath), ...(opts.ignoreDefaultArgs ?? [])])],
|
|
318
326
|
protocolTimeout: BROWSER_PROTOCOL_TIMEOUT_MS,
|
|
319
327
|
});
|
|
320
328
|
}
|
|
@@ -286,13 +286,16 @@ async function acquireTabImpl(
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
// If the caller aborted while we were spawning/initializing the worker,
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
289
|
+
// If the caller aborted while we were spawning/initializing the worker, tear
|
|
290
|
+
// the freshly-built worker down before publishing the tab so the browser
|
|
291
|
+
// refCount (which `holdBrowser` below would take) never grows for a tab
|
|
292
|
+
// nobody is waiting for. Mirror the error paths' `refCount === 0` release so
|
|
293
|
+
// a fresh browser held by nothing but this aborted open is not orphaned in
|
|
294
|
+
// the registry; a browser still leased/held elsewhere (refCount > 0) is left
|
|
295
|
+
// for its owner to release.
|
|
293
296
|
if (opts.signal?.aborted) {
|
|
294
297
|
await worker.terminate().catch(() => undefined);
|
|
295
|
-
if (tempHold) await releaseBrowser(browser, { kill: false }).catch(() => undefined);
|
|
298
|
+
if (tempHold || browser.refCount === 0) await releaseBrowser(browser, { kill: false }).catch(() => undefined);
|
|
296
299
|
throw new ToolAbortError("Browser tab open aborted");
|
|
297
300
|
}
|
|
298
301
|
|