From 5861452aa7683eef4842127e28debebcefdfbac3 Mon Sep 17 00:00:00 2001 From: ainaanraza Date: Wed, 5 Aug 2026 17:13:31 +0530 Subject: [PATCH] Fixes #1070: Rename internal bash tool to terminal for cross-platform accuracy --- packages/core/src/tool/builtins.ts | 4 +- .../core/src/tool/{bash.ts => terminal.ts} | 4 +- packages/core/src/v1/config/permission.ts | 13 +- packages/core/test/config/agent.test.ts | 4 +- .../test/session-runner-tool-registry.test.ts | 5 +- .../core/test/session-tool-progress.test.ts | 4 +- ...ool-bash.test.ts => tool-terminal.test.ts} | 32 +-- packages/opencode/specs/v2/api.ts | 2 +- packages/opencode/src/acp/event.ts | 2 +- packages/opencode/src/acp/tool.ts | 2 +- packages/opencode/src/agent/agent.ts | 32 +-- .../src/altimate/observability/viewer.ts | 6 +- .../opencode/src/altimate/telemetry/index.ts | 2 +- packages/opencode/src/altimate/tool-source.ts | 2 +- packages/opencode/src/cli/cmd/agent.ts | 3 +- packages/opencode/src/cli/cmd/run.ts | 6 +- packages/opencode/src/cli/cmd/run/demo.ts | 18 +- .../opencode/src/cli/cmd/run/session-data.ts | 12 +- .../src/cli/cmd/run/session-replay.ts | 2 +- packages/opencode/src/cli/cmd/run/tool.ts | 32 +-- packages/opencode/src/cli/cmd/skill.ts | 4 +- packages/opencode/src/permission/index.ts | 6 +- packages/opencode/src/permission/next.ts | 6 +- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/tool/registry.ts | 4 +- packages/opencode/src/tool/retrieval.ts | 3 +- packages/opencode/src/tool/shell/id.ts | 10 +- .../src/tool/{bash.ts => terminal.ts} | 8 +- packages/opencode/test/acp/event.test.ts | 6 +- packages/opencode/test/agent/agent.test.ts | 14 +- .../agent/plan-mode-subagent-bypass.test.ts | 4 +- .../carry-forward/agent-safety.test.ts | 16 +- .../test/altimate/trace-consumer.test.ts | 2 +- .../altimate/tracing-adversarial-2.test.ts | 30 +-- .../tracing-adversarial-final.test.ts | 14 +- .../tracing-adversarial-snapshot.test.ts | 24 +- .../test/altimate/tracing-adversarial.test.ts | 22 +- .../altimate/tracing-de-attributes.test.ts | 2 +- .../altimate/tracing-display-crash.test.ts | 2 +- .../test/altimate/tracing-e2e.test.ts | 16 +- .../test/altimate/tracing-final-audit.test.ts | 16 +- .../test/altimate/tracing-integration.test.ts | 6 +- .../test/altimate/tracing-rename-race.test.ts | 2 +- .../test/altimate/tracing-thorough.test.ts | 6 +- .../opencode/test/altimate/tracing.test.ts | 36 +-- .../test/branding/build-integrity.test.ts | 2 +- .../opencode/test/cli/run/entry.body.test.ts | 14 +- .../test/cli/run/footer.view.test.tsx | 4 +- .../test/cli/run/permission.shared.test.ts | 6 +- .../test/cli/run/scrollback.surface.test.ts | 18 +- .../test/cli/run/session-data.test.ts | 18 +- .../test/cli/run/session-replay.test.ts | 6 +- .../test/cli/run/stream.transport.test.ts | 2 +- .../test/cli/run/subagent-data.test.ts | 4 +- packages/opencode/test/config/config.test.ts | 4 +- .../opencode/test/file/security-e2e.test.ts | 34 +-- .../opencode/test/permission-task.test.ts | 6 +- .../opencode/test/permission-yolo.test.ts | 134 +++++----- .../opencode/test/permission/next.test.ts | 252 +++++++++--------- .../opencode/test/provider/transform.test.ts | 12 +- .../question-937-codex.test.ts | 2 +- .../release-validation/question-937.test.ts | 10 +- .../session-transcript-941.test.ts | 2 +- .../test/session/compaction-mask.test.ts | 8 +- .../opencode/test/session/compaction.test.ts | 2 +- .../opencode/test/session/llm-native.test.ts | 10 +- packages/opencode/test/session/llm.test.ts | 20 +- .../opencode/test/session/message-v2.test.ts | 42 +-- .../test/session/observation-mask.test.ts | 12 +- .../opencode/test/session/processor.test.ts | 12 +- packages/opencode/test/session/prompt.test.ts | 10 +- .../opencode/test/session/regression.test.ts | 2 +- .../test/session/revert-compact.test.ts | 2 +- .../test/session/schema-decoding.test.ts | 2 +- .../skill/release-v0.6.0-adversarial.test.ts | 4 +- .../skill/release-v0.8.0-adversarial.test.ts | 4 +- .../skill/release-v0.8.6-adversarial.test.ts | 2 +- .../opencode/test/telemetry/telemetry.test.ts | 4 +- .../opencode/test/tool/parameters.test.ts | 2 +- packages/opencode/test/tool/retrieval.test.ts | 2 +- packages/opencode/test/tool/shell.test.ts | 4 +- packages/opencode/test/tool/task.test.ts | 4 +- .../tool/{bash.test.ts => terminal.test.ts} | 44 +-- packages/opencode/test/tui-journeys/README.md | 2 +- .../test/tui-journeys/journeys.test.ts | 2 +- .../upstream/adversarial/upi-tool-api.test.ts | 2 +- .../test/upstream/altimate-features.test.ts | 24 +- .../test/upstream/v140-merge-fuzz.test.ts | 2 +- .../upstream/v140-permission-deadlock.test.ts | 18 +- .../test/v2/session-message-updater.test.ts | 4 +- .../src/feature-plugins/home/tips-view.tsx | 2 +- packages/tui/src/routes/session/index.tsx | 5 +- packages/tui/test/cli/tui/data.test.tsx | 4 +- .../tui/inline-tool-wrap-snapshot.test.tsx | 2 +- packages/tui/test/util/transcript.test.ts | 8 +- 95 files changed, 628 insertions(+), 617 deletions(-) rename packages/core/src/tool/{bash.ts => terminal.ts} (99%) rename packages/core/test/{tool-bash.test.ts => tool-terminal.test.ts} (95%) rename packages/opencode/src/tool/{bash.ts => terminal.ts} (97%) rename packages/opencode/test/tool/{bash.test.ts => terminal.test.ts} (93%) diff --git a/packages/core/src/tool/builtins.ts b/packages/core/src/tool/builtins.ts index 62926202dc..9a73d1f95a 100644 --- a/packages/core/src/tool/builtins.ts +++ b/packages/core/src/tool/builtins.ts @@ -1,7 +1,7 @@ export * as BuiltInTools from "./builtins" import { Layer } from "effect" -import { BashTool } from "./bash" +import { TerminalTool } from "./terminal" import { ApplyPatchTool } from "./apply-patch" import { EditTool } from "./edit" import { GlobTool } from "./glob" @@ -30,7 +30,7 @@ import { WriteTool } from "./write" */ export const locationLayer = Layer.mergeAll( ApplyPatchTool.layer, - BashTool.layer, + TerminalTool.layer, EditTool.layer, GlobTool.layer, GrepTool.layer, diff --git a/packages/core/src/tool/bash.ts b/packages/core/src/tool/terminal.ts similarity index 99% rename from packages/core/src/tool/bash.ts rename to packages/core/src/tool/terminal.ts index bd6f175ada..e24cce3310 100644 --- a/packages/core/src/tool/bash.ts +++ b/packages/core/src/tool/terminal.ts @@ -1,4 +1,4 @@ -export * as BashTool from "./bash" +export * as TerminalTool from "./terminal" import path from "path" import { ToolFailure } from "@opencode-ai/llm" @@ -13,7 +13,7 @@ import { PositiveInt } from "../schema" import { Tool } from "./tool" import { Tools } from "./tools" -export const name = "bash" +export const name = "terminal" export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000 export const MAX_TIMEOUT_MS = 10 * 60 * 1_000 export const MAX_CAPTURE_BYTES = 1024 * 1024 diff --git a/packages/core/src/v1/config/permission.ts b/packages/core/src/v1/config/permission.ts index 475dc7bbf3..2945575641 100644 --- a/packages/core/src/v1/config/permission.ts +++ b/packages/core/src/v1/config/permission.ts @@ -21,7 +21,8 @@ const InputObject = Schema.StructWithRest( glob: Schema.optional(Rule), grep: Schema.optional(Rule), list: Schema.optional(Rule), - bash: Schema.optional(Rule), + bash: Schema.optional(Rule), // kept for backward compatibility but mapped to "terminal" + terminal: Schema.optional(Rule), task: Schema.optional(Rule), external_directory: Schema.optional(Rule), todowrite: Schema.optional(Action), @@ -37,8 +38,14 @@ const InputObject = Schema.StructWithRest( const InputSchema = Schema.Union([Action, InputObject]) -const normalizeInput = (input: Schema.Schema.Type): Schema.Schema.Type => - typeof input === "string" ? { "*": input } : input +const normalizeInput = (input: Schema.Schema.Type): Schema.Schema.Type => { + if (typeof input === "string") return { "*": input } + const result = { ...input } + if (result.terminal !== undefined && result.bash !== undefined) { + delete (result as any).bash + } + return result +} export const Info = InputSchema.pipe( Schema.decodeTo(InputObject, { diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index 79e872f74e..662433bd38 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -81,8 +81,8 @@ describe("ConfigAgentPlugin.Plugin", () => { { action: "read", resource: "*", effect: "allow" }, { action: "bash", resource: "git *", effect: "allow" }, ]) - expect(PermissionV2.evaluate("bash", "git status", buildAgent.permissions).effect).toBe("allow") - expect(PermissionV2.evaluate("bash", "bun test", buildAgent.permissions).effect).toBe("ask") + expect(PermissionV2.evaluate("terminal", "git status", buildAgent.permissions).effect).toBe("allow") + expect(PermissionV2.evaluate("terminal", "bun test", buildAgent.permissions).effect).toBe("ask") const reviewer = yield* agents.get(AgentV2.ID.make("reviewer")) if (!reviewer) throw new Error("expected configured reviewer agent") diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index 7c326a9cad..7a91a94c37 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -66,8 +66,7 @@ describe("ToolRegistry", () => { const names = (rules: Parameters[0]) => toolDefinitions(service, rules).pipe(Effect.map((definitions) => definitions.map((tool) => tool.name))) - expect(yield* names([{ action: "question", resource: "*", effect: "deny" }])).toEqual([ - "bash", + expect(yield* names([{ action: "question", resource: "*", effect: "deny" }])).toEqual(["terminal", "edit", "write", "apply_patch", @@ -84,7 +83,7 @@ describe("ToolRegistry", () => { { action: "*", resource: "*", effect: "deny" }, ]), ).toEqual([]) - expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual(["question", "bash"]) + expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual(["question", "terminal"]) }), ) diff --git a/packages/core/test/session-tool-progress.test.ts b/packages/core/test/session-tool-progress.test.ts index 09cc159a20..470dce36d8 100644 --- a/packages/core/test/session-tool-progress.test.ts +++ b/packages/core/test/session-tool-progress.test.ts @@ -74,14 +74,14 @@ describe("Tool.Progress", () => { timestamp, assistantMessageID, callID, - name: "bash", + name: "terminal", }) yield* service.publish(SessionEvent.Tool.Called, { sessionID, timestamp, assistantMessageID, callID, - tool: "bash", + tool: "terminal", input: { command: "pwd" }, provider: { executed: false }, }) diff --git a/packages/core/test/tool-bash.test.ts b/packages/core/test/tool-terminal.test.ts similarity index 95% rename from packages/core/test/tool-bash.test.ts rename to packages/core/test/tool-terminal.test.ts index 0fb2cd7355..7fd73a5e2c 100644 --- a/packages/core/test/tool-bash.test.ts +++ b/packages/core/test/tool-terminal.test.ts @@ -12,7 +12,7 @@ import { PermissionV2 } from "@opencode-ai/core/permission" import { AppProcess } from "@opencode-ai/core/process" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionV2 } from "@opencode-ai/core/session" -import { BashTool } from "@opencode-ai/core/tool/bash" +import { TerminalTool } from "@opencode-ai/core/tool/terminal" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" @@ -102,7 +102,7 @@ const withTool = ( ) const mutation = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation)) const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission)) - const bash = BashTool.layer.pipe( + const bash = TerminalTool.layer.pipe( Layer.provide(registry), Layer.provide(permission), Layer.provide(mutation), @@ -115,15 +115,15 @@ const withTool = ( }).pipe(Effect.provide(Layer.mergeAll(registry, bash))) } -const call = (input: typeof BashTool.Input.Type, id = "call-bash") => ({ +const call = (input: typeof TerminalTool.Input.Type, id = "call-bash") => ({ sessionID, ...toolIdentity, - call: { type: "tool-call" as const, id, name: "bash", input }, + call: { type: "tool-call" as const, id, name: "terminal", input }, }) const it = testEffect(Layer.empty) -describe("BashTool", () => { +describe("TerminalTool", () => { it.live("registers and returns structured successful output from the active Location", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), @@ -132,9 +132,9 @@ describe("BashTool", () => { return withTool(tmp.path, (registry) => Effect.gen(function* () { const definitions = yield* toolDefinitions(registry) - expect(definitions.map((tool) => tool.name)).toEqual(["bash"]) + expect(definitions.map((tool) => tool.name)).toEqual(["terminal"]) expect(definitions[0]?.inputSchema).not.toHaveProperty("properties.background") - expect(yield* toolDefinitions(registry, [{ action: "bash", resource: "*", effect: "deny" }])).toEqual([]) + expect(yield* toolDefinitions(registry, [{ action: "terminal", resource: "*", effect: "deny" }])).toEqual([]) expect( yield* settleTool(registry, call({ command: "pwd", description: "Print working directory" })), ).toEqual({ @@ -152,10 +152,10 @@ describe("BashTool", () => { }) expect(runs).toMatchObject([{ command: "pwd", cwd: realpathSync(tmp.path) }]) expect(runs[0]?.options).toMatchObject({ - maxOutputBytes: BashTool.MAX_CAPTURE_BYTES, - maxErrorBytes: BashTool.MAX_CAPTURE_BYTES, + maxOutputBytes: TerminalTool.MAX_CAPTURE_BYTES, + maxErrorBytes: TerminalTool.MAX_CAPTURE_BYTES, }) - expect(assertions).toMatchObject([{ sessionID, action: "bash", resources: ["pwd"], save: ["pwd"] }]) + expect(assertions).toMatchObject([{ sessionID, action: "terminal", resources: ["pwd"], save: ["pwd"] }]) }), ) }, @@ -188,7 +188,7 @@ describe("BashTool", () => { reset() const workdir = path.join(tmp.path, "src") afterPermission = (input) => - input.action === "bash" + input.action === "terminal" ? Effect.promise(async () => { await fs.rm(workdir, { recursive: true }) await fs.writeFile(workdir, "not a directory") @@ -201,7 +201,7 @@ describe("BashTool", () => { Effect.andThen( Effect.sync(() => { expect(runs).toEqual([]) - expect(assertions.map((input) => input.action)).toEqual(["bash"]) + expect(assertions.map((input) => input.action)).toEqual(["terminal"]) }), ), ) @@ -249,7 +249,7 @@ describe("BashTool", () => { ).pipe( Effect.andThen( Effect.sync(() => { - expect(assertions.map((item) => item.action)).toEqual(["external_directory", "bash"]) + expect(assertions.map((item) => item.action)).toEqual(["external_directory", "terminal"]) expect(assertions[0]).toMatchObject({ resources: [path.join(realpathSync(outside.path), "*").replaceAll("\\", "/")], }) @@ -281,7 +281,7 @@ describe("BashTool", () => { reset() denyAction = "bash" yield* withTool(active.path, (registry) => executeTool(registry, call({ command: "pwd" }))) - expect(assertions.map((item) => item.action)).toEqual(["bash"]) + expect(assertions.map((item) => item.action)).toEqual(["terminal"]) expect(runs).toEqual([]) }), ([active, outside]) => @@ -301,7 +301,7 @@ describe("BashTool", () => { return withTool(active.path, (registry) => settleTool(registry, call({ command: `cat ${target}` }))).pipe( Effect.andThen((settled) => Effect.sync(() => { - expect(assertions.map((item) => item.action)).toEqual(["bash"]) + expect(assertions.map((item) => item.action)).toEqual(["terminal"]) expect(runs).toHaveLength(1) expect(settled.output?.structured).toMatchObject({ warnings: [ @@ -399,7 +399,7 @@ describe("BashTool", () => { }) test("keeps locked deferred parity TODOs visible", async () => { - const source = await fs.readFile(new URL("../src/tool/bash.ts", import.meta.url), "utf8") + const source = await fs.readFile(new URL("../src/tool/terminal.ts", import.meta.url), "utf8") for (const todo of [ "Port tree-sitter bash / PowerShell parser-based approval reduction.", "Port BashArity reusable command-prefix approvals.", diff --git a/packages/opencode/specs/v2/api.ts b/packages/opencode/specs/v2/api.ts index b8b5d6abce..cdd6532f3a 100644 --- a/packages/opencode/specs/v2/api.ts +++ b/packages/opencode/specs/v2/api.ts @@ -8,7 +8,7 @@ const opencode = OpenCode.make({}) opencode.tool.add(ReadTool) opencode.tool.add({ - name: "bash", + name: "terminal", schema: { type: "object", properties: { diff --git a/packages/opencode/src/acp/event.ts b/packages/opencode/src/acp/event.ts index 7d05fa6ee5..f17ccdae79 100644 --- a/packages/opencode/src/acp/event.ts +++ b/packages/opencode/src/acp/event.ts @@ -281,7 +281,7 @@ export class Subscription { private async runningTool(sessionId: string, part: ToolPart, cwd: string) { if (part.state.status !== "running") return - const output = part.tool === "bash" ? shellOutputSnapshot(part.state) : undefined + const output = part.tool === "terminal" ? shellOutputSnapshot(part.state) : undefined if (output !== undefined) { if (this.shellSnapshots.get(part.callID) === output) { await this.input.connection.sessionUpdate({ diff --git a/packages/opencode/src/acp/tool.ts b/packages/opencode/src/acp/tool.ts index d0e57cc2ec..561b7864bf 100644 --- a/packages/opencode/src/acp/tool.ts +++ b/packages/opencode/src/acp/tool.ts @@ -296,7 +296,7 @@ function shellCommand(input: ToolInput) { function isShell(toolName: string) { const tool = toolName.toLocaleLowerCase() - return tool === "bash" || tool === "shell" + return tool === "terminal" || tool === "shell" } export const mapToolKind = toToolKind diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 1252bb75b5..ea11f97799 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -151,8 +151,8 @@ export const layer = Layer.effect( "*.env.*": "ask", "*.env.example": "allow", }, - // altimate_change start - bash safety defaults for destructive file/git/DDL commands - // Safety defaults for bash commands. + // altimate_change start - terminal safety defaults for destructive file/git/DDL commands + // Safety defaults for terminal commands. // IMPORTANT: "*": "ask" must come FIRST because evaluation uses last-match-wins. // // "ask" = user sees prompt and can approve. Used for destructive file/git @@ -162,7 +162,7 @@ export const layer = Layer.effect( // almost never intentional in an agent context. // // Users can override any of these in altimate-code.json. - bash: { + terminal: { "*": "ask", "rm -rf *": "ask", "rm -fr *": "ask", @@ -186,11 +186,11 @@ export const layer = Layer.effect( // Safety deny rules that CANNOT be overridden by wildcard allows. // Appended after user config so they always take precedence via last-match-wins. // Users who need to override must use specific patterns like - // `"DROP DATABASE test_db": "allow"` — wildcard `bash: "allow"` won't work. + // `"DROP DATABASE test_db": "allow"` — wildcard `terminal: "allow"` won't work. // Both UPPER and lowercase variants are included because Wildcard.match // is case-sensitive on Linux/macOS. const safetyDenials = Permission.fromConfig({ - bash: { + terminal: { "DROP DATABASE *": "deny", "DROP SCHEMA *": "deny", "TRUNCATE *": "deny", @@ -291,8 +291,8 @@ export const layer = Layer.effect( websearch: "allow", question: "allow", tool_lookup: "allow", - // Bash: last-match-wins — "*": "deny" MUST come first, then specific allows override - bash: { + // Terminal: last-match-wins — "*": "deny" MUST come first, then specific allows override + terminal: { "*": "deny", "ls *": "allow", "grep *": "allow", @@ -319,7 +319,7 @@ export const layer = Layer.effect( reviewer: { name: "reviewer", description: - "dbt PR reviewer. Runs the dbt_pr_review verdict engine (lineage, equivalence, PII, grade) plus read-only analysis tools and posts findings. Edit/write tools are denied; bash prompts for approval.", + "dbt PR reviewer. Runs the dbt_pr_review verdict engine (lineage, equivalence, PII, grade) plus read-only analysis tools and posts findings. Edit/write tools are denied; terminal prompts for approval.", prompt: PROMPT_REVIEWER, options: {}, permission: Permission.merge( @@ -340,7 +340,7 @@ export const layer = Layer.effect( schema_detect_pii: "allow", // Writes denied — review never mutates the project. sql_execute_write: "deny", - // Read-only file + repo access (structured tools, not bash). + // Read-only file + repo access (structured tools, not terminal). read: "allow", grep: "allow", glob: "allow", @@ -354,20 +354,20 @@ export const layer = Layer.effect( // Read-only web access so the reviewer can pull PR/issue URLs. webfetch: "allow", websearch: "allow", - // Bash PROMPTS instead of hard-denying (#978: `gh pr view` is the + // terminal PROMPTS instead of hard-denying (#978: `gh pr view` is the // primary way to review a PR URL). A string-prefix allowlist can't // safely bound argv (redirects ride inside the matched command), so - // every bash command requires explicit user approval here — the + // every terminal command requires explicit user approval here — the // reviewer still never runs shell commands silently. - bash: "ask", + terminal: "ask", }), // altimate_change start — reviewer safety must not be overridable by a permissive user - // config (e.g. global `permission: {"*":"allow"}` or `bash:"allow"`). Merge user config, + // config (e.g. global `permission: {"*":"allow"}` or `terminal:"allow"`). Merge user config, // THEN re-apply the reviewer read-only invariants, THEN safetyDenials LAST so DDL denies - // still win over the reviewer's bash:"ask". (edit covers write/edit/apply_patch.) + // still win over the reviewer's terminal:"ask". (edit covers write/edit/apply_patch.) user, Permission.fromConfig({ - bash: "ask", + terminal: "ask", edit: "deny", sql_execute_write: "deny", }), @@ -435,7 +435,7 @@ export const layer = Layer.effect( codesearch: "allow", // altimate_change end list: "allow", - bash: "allow", + terminal: "allow", webfetch: "allow", websearch: "allow", read: "allow", diff --git a/packages/opencode/src/altimate/observability/viewer.ts b/packages/opencode/src/altimate/observability/viewer.ts index 8fc6ab2562..d033bd6834 100644 --- a/packages/opencode/src/altimate/observability/viewer.ts +++ b/packages/opencode/src/altimate/observability/viewer.ts @@ -595,7 +595,7 @@ function showDetail(span) { if (fp) changedFiles[fp] = lname.indexOf('write') >= 0 ? 'write' : 'edit'; } else if (lname.indexOf('read') >= 0 || lname === 'glob' || lname === 'grep') { if (fp && !changedFiles[fp]) readFiles[fp] = 1; - } else if (lname === 'bash' || lname.indexOf('shell') >= 0) { + } else if (lname === 'terminal' || lname.indexOf('shell') >= 0) { var cmd = inpObj ? (inpObj.command || '') : (typeof inp === 'string' ? inp : ''); if (cmd) { // Extract the meaningful command — strip cd prefixes, take last command in chain @@ -652,7 +652,7 @@ function showDetail(span) { } // For bash/shell commands — extract meaningful command and its outcome - if ((lname === 'bash' || lname.indexOf('shell') >= 0) && outStr) { + if ((lname === 'terminal' || lname.indexOf('shell') >= 0) && outStr) { var rawCmd = String(inp.command || ''); var cmdParts = rawCmd.split(/\\s*&&\\s*/); var displayCmd = cmdParts[cmdParts.length - 1].trim(); @@ -1475,7 +1475,7 @@ function showDetail(span) { var fp = inp.file_path || inp.filePath || inp.path || null; if (nm.indexOf('write') >= 0 || nm.indexOf('edit') >= 0) { if (fp) mdChanged[fp] = nm.indexOf('write') >= 0 ? 'new' : 'edited'; } else if (nm.indexOf('read') >= 0) { mdReadCount++; } - else if (nm === 'bash') { + else if (nm === 'terminal' || nm.indexOf('shell') >= 0) { var cmd = inp.command || ''; var parts = cmd.split(/\\s*&&\\s*/); var last = parts[parts.length - 1].trim().toLowerCase(); diff --git a/packages/opencode/src/altimate/telemetry/index.ts b/packages/opencode/src/altimate/telemetry/index.ts index bb4c9ee467..3bd64cc8ec 100644 --- a/packages/opencode/src/altimate/telemetry/index.ts +++ b/packages/opencode/src/altimate/telemetry/index.ts @@ -1429,7 +1429,7 @@ export namespace Telemetry { return JSON.stringify({ "...": `${Object.keys(masked).length} keys` }) } - const FILE_TOOLS = new Set(["read", "write", "edit", "glob", "grep", "bash"]) + const FILE_TOOLS = new Set(["read", "write", "edit", "glob", "grep", "terminal"]) // Order matters: more specific patterns (e.g. "warehouse_usage") are checked // before broader ones (e.g. "warehouse") to avoid miscategorization. diff --git a/packages/opencode/src/altimate/tool-source.ts b/packages/opencode/src/altimate/tool-source.ts index e26bad8ced..5e226fc032 100644 --- a/packages/opencode/src/altimate/tool-source.ts +++ b/packages/opencode/src/altimate/tool-source.ts @@ -42,7 +42,7 @@ export type RegistryToolOrigin = "native" | "altimate" | "external" const NATIVE_TOOL_IDS = new Set([ "invalid", "question", - "bash", + "terminal", "batch", "read", "glob", diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index f31bf206ee..bef931f121 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -20,8 +20,7 @@ type AgentMode = "all" | "primary" | "subagent" // Permission keys (not raw tool names). Multiple tools can map to a single // permission — e.g. write/edit/apply_patch all gate on `edit` — so we configure // agents at the permission level to match how the runtime actually enforces it. -export const AVAILABLE_PERMISSIONS = [ - "bash", +export const AVAILABLE_PERMISSIONS = ["terminal", "read", "edit", // altimate_change start — upstream_fix: keep legacy list permission denyable diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 50638bcd14..cdcd849c1d 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -24,7 +24,7 @@ import { CodeSearchTool } from "../../tool/codesearch" import { WebSearchTool } from "../../tool/websearch" import { TaskTool } from "../../tool/task" import { SkillTool } from "../../tool/skill" -import { BashTool } from "../../tool/bash" +import { TerminalTool } from "../../tool/terminal" import { TodoWriteTool } from "../../tool/todo" import { Locale } from "../../util/locale" import { Tracer, FileExporter, HttpExporter, type TraceExporter } from "../../altimate/observability/tracing" @@ -200,7 +200,7 @@ function skill(info: ToolProps) { }) } -function bash(info: ToolProps) { +function terminal(info: ToolProps) { const output = info.part.state.status === "completed" ? info.part.state.output?.trim() : undefined block( { @@ -566,7 +566,7 @@ You are speaking to a non-technical business executive. Follow these rules stric function tool(part: ToolPart) { try { - if (part.tool === "bash") return bash(props(part)) + if (part.tool === "terminal") return terminal(props(part)) if (part.tool === "glob") return glob(props(part)) if (part.tool === "grep") return grep(props(part)) if (part.tool === "list") return list(props(part)) diff --git a/packages/opencode/src/cli/cmd/run/demo.ts b/packages/opencode/src/cli/cmd/run/demo.ts index 94450f1242..d92e6935f7 100644 --- a/packages/opencode/src/cli/cmd/run/demo.ts +++ b/packages/opencode/src/cli/cmd/run/demo.ts @@ -35,7 +35,7 @@ const KINDS = [ "error", "mix", ] -const PERMISSIONS = ["edit", "bash", "read", "task", "external", "doom"] as const +const PERMISSIONS = ["edit", "terminal", "read", "task", "external", "doom"] as const const QUESTIONS = ["multi", "single", "checklist", "custom"] as const type PermissionKind = (typeof PERMISSIONS)[number] @@ -582,8 +582,8 @@ function emitError(state: State, text: string): void { feed(state, event) } -async function emitBash(state: State, signal?: AbortSignal): Promise { - const ref = make(state, "bash", { +async function emitTerminal(state: State, signal?: AbortSignal): Promise { + const ref = make(state, "terminal", { command: "git status", workdir: process.cwd(), description: "Show git status", @@ -794,16 +794,16 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { const root = process.cwd() const file = path.join(root, "src", "demo-format.ts") - if (kind === "bash") { + if (kind === "terminal") { const command = "git status --short" - const ref = make(state, "bash", { + const ref = make(state, "terminal", { command, workdir: root, description: "Inspect worktree changes", }) askPermission(state, { ref, - permission: "bash", + permission: "terminal", patterns: [command], always: ["*"], done: { @@ -1041,8 +1041,8 @@ async function emitFmt(state: State, kind: string, body: string, signal?: AbortS return true } - if (kind === "bash") { - await emitBash(state, signal) + if (kind === "terminal") { + await emitTerminal(state, signal) return true } @@ -1086,7 +1086,7 @@ async function emitFmt(state: State, kind: string, body: string, signal?: AbortS await wait(50, signal) await emitReasoning(state, "Thinking through formatter edge cases [REDACTED].", signal) await wait(50, signal) - await emitBash(state, signal) + await emitTerminal(state, signal) emitWrite(state) emitEdit(state) emitPatch(state) diff --git a/packages/opencode/src/cli/cmd/run/session-data.ts b/packages/opencode/src/cli/cmd/run/session-data.ts index e1716b550a..3a9eafd1fc 100644 --- a/packages/opencode/src/cli/cmd/run/session-data.ts +++ b/packages/opencode/src/cli/cmd/run/session-data.ts @@ -462,7 +462,7 @@ function syncText(data: SessionData, partID: string, next: string) { // verbatim at the start of their next text part. We save both the raw and // trimmed forms so stripEcho() can match either. function stashEcho(data: SessionData, part: ToolPart) { - if (part.tool !== "bash") { + if (part.tool !== "terminal") { return } @@ -658,8 +658,8 @@ function claimShell(data: SessionData, callID: string, source: ShellCall["source return next } -function bashCommand(part: ToolPart): string | undefined { - if (part.tool !== "bash") { +function terminalCommand(part: ToolPart): string | undefined { + if (part.tool !== "terminal") { return undefined } @@ -683,7 +683,7 @@ function shellCommit( kind: "tool", source: "tool", partID: shellPartID(input.callID), - tool: "bash", + tool: "terminal", shell: input, ...next, } @@ -925,8 +925,8 @@ export function reduceSessionData(input: SessionDataInput): SessionDataOutput { if (part.type === "tool") { const view = syncPermission(data, part) ?? syncQuestion(data, part) - if (part.tool === "bash" && part.callID) { - if (claimShell(data, part.callID, "tool", bashCommand(part)).source === "shell") { + if (part.tool === "terminal" && part.callID) { + if (claimShell(data, part.callID, "tool", terminalCommand(part)).source === "shell") { return out(data, commits, view) } } diff --git a/packages/opencode/src/cli/cmd/run/session-replay.ts b/packages/opencode/src/cli/cmd/run/session-replay.ts index 69a24f2719..49a3e3c288 100644 --- a/packages/opencode/src/cli/cmd/run/session-replay.ts +++ b/packages/opencode/src/cli/cmd/run/session-replay.ts @@ -116,7 +116,7 @@ function isShellSyntheticAssistant(message: SessionMessages[number], shellParent return ( message.info.role === "assistant" && shellParents.has(message.info.parentID) && - message.parts.some((part) => part.type === "tool" && part.tool === "bash") + message.parts.some((part) => part.type === "tool" && (part.tool === "terminal" || part.tool === "bash")) ) } diff --git a/packages/opencode/src/cli/cmd/run/tool.ts b/packages/opencode/src/cli/cmd/run/tool.ts index e751919df2..3924600e8d 100644 --- a/packages/opencode/src/cli/cmd/run/tool.ts +++ b/packages/opencode/src/cli/cmd/run/tool.ts @@ -18,7 +18,7 @@ import stripAnsi from "strip-ansi" import type { ToolPart } from "@opencode-ai/sdk/v2" import type * as Tool from "@/tool/tool" import type { ApplyPatchTool } from "@/tool/apply_patch" -import type { ShellTool as BashTool } from "@/tool/shell" +import type { ShellTool as TerminalTool } from "@/tool/shell" import type { EditTool } from "@/tool/edit" import type { GlobTool } from "@/tool/glob" import type { GrepTool } from "@/tool/grep" @@ -103,7 +103,7 @@ type ToolPermissionCtx = { type ToolDefs = { invalid: typeof InvalidTool - bash: typeof BashTool + terminal: typeof TerminalTool write: typeof WriteTool edit: typeof EditTool apply_patch: typeof ApplyPatchTool @@ -631,7 +631,7 @@ function snapQuestion(p: ToolProps): ToolSnapshot { } } -function scrollBashStart(p: ToolProps): string { +function scrollTerminalStart(p: ToolProps): string { const cmd = p.input.command ?? "" const desc = p.input.description || "Shell" const wd = p.input.workdir ?? "" @@ -649,7 +649,7 @@ function scrollBashStart(p: ToolProps): string { return `# ${title}\n$ ${cmd}` } -function scrollBashProgress(p: ToolProps): string { +function scrollTerminalProgress(p: ToolProps): string { const out = stripAnsi(p.frame.raw) const cmd = (p.input.command ?? "").trim() const fmt = (text: string) => { @@ -682,18 +682,18 @@ function scrollBashProgress(p: ToolProps): string { return fmt(out) } -function scrollBashFinal(p: ToolProps): string { +function scrollTerminalFinal(p: ToolProps): string { const code = p.metadata.exit ?? num(p.frame.meta.exitCode) ?? num(p.frame.meta.exit_code) const time = span(p.frame.state) if (code === undefined) { if (!time) { - return "bash completed" + return "terminal completed" } - return `bash completed · ${time}` + return `terminal completed · ${time}` } - return `bash completed (exit ${code})${time ? ` · ${time}` : ""}` + return `terminal completed (exit ${code})${time ? ` · ${time}` : ""}` } function scrollReadStart(p: ToolProps): string { @@ -977,7 +977,7 @@ function permList(p: ToolPermissionProps): ToolPermissionInfo { } } -function permBash(p: ToolPermissionProps): ToolPermissionInfo { +function permTerminal(p: ToolPermissionProps): ToolPermissionInfo { const title = p.input.description || "Shell command" const cmd = p.input.command || "" return { @@ -1043,18 +1043,18 @@ const TOOL_RULES = { start: () => "", }, }, - bash: { + terminal: { view: { output: true, final: false, }, - run: runBash, + run: runTerminal, scroll: { - start: scrollBashStart, - progress: scrollBashProgress, - final: scrollBashFinal, + start: scrollTerminalStart, + progress: scrollTerminalProgress, + final: scrollTerminalFinal, }, - permission: permBash, + permission: permTerminal, }, write: { view: { @@ -1287,7 +1287,7 @@ export function toolFrame(commit: StreamCommit, raw: string): ToolFrame { } } -function runBash(p: ToolProps): ToolInline { +function runTerminal(p: ToolProps): ToolInline { return { icon: "$", title: p.input.command || "", diff --git a/packages/opencode/src/cli/cmd/skill.ts b/packages/opencode/src/cli/cmd/skill.ts index a658e6955c..fcae018722 100644 --- a/packages/opencode/src/cli/cmd/skill.ts +++ b/packages/opencode/src/cli/cmd/skill.ts @@ -48,7 +48,7 @@ ${cliSection} ` } -function bashToolTemplate(name: string): string { +function terminalToolTemplate(name: string): string { return `#!/usr/bin/env bash set -euo pipefail # ${name} — TODO describe what this tool does @@ -310,7 +310,7 @@ const SkillCreateCommand = cmd({ template = nodeToolTemplate(name) break default: - template = bashToolTemplate(name) + template = terminalToolTemplate(name) } await fs.writeFile(toolFile, template, { mode: 0o755 }) diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 2e06d7be0e..201f9983ee 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -206,7 +206,11 @@ function expand(pattern: string): string { export function fromConfig(permission: ConfigPermissionV1.Info) { const ruleset: PermissionV1.Rule[] = [] - for (const [key, value] of Object.entries(permission)) { + for (let [key, value] of Object.entries(permission)) { + if (key === "bash") { + if (permission.terminal !== undefined) continue // terminal takes precedence + key = "terminal" + } if (typeof value === "string") { ruleset.push({ permission: key, action: value, pattern: "*" }) continue diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index edc2ca1d03..49f9067d4c 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -48,7 +48,11 @@ export namespace PermissionNext { export function fromConfig(permission: ConfigPermissionV1.Info) { const ruleset: Ruleset = [] - for (const [key, value] of Object.entries(permission)) { + for (let [key, value] of Object.entries(permission)) { + if (key === "bash") { + if (permission.terminal !== undefined) continue // terminal takes precedence + key = "terminal" + } if (typeof value === "string") { ruleset.push({ permission: key, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index afe4300d34..536e9f59d5 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -2661,7 +2661,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the id: PartID.ascending(), messageID: msg.id, sessionID: input.sessionID, - tool: "bash", + tool: "terminal", callID: ulid(), state: { status: "running", diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 20e8bd1ba6..0f5c7ff28a 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -1,6 +1,6 @@ import { PlanExitTool } from "./plan" import { QuestionTool } from "./question" -import { BashTool } from "./bash" +import { TerminalTool } from "./terminal" import { EditTool } from "./edit" import { GlobTool } from "./glob" import { GrepTool } from "./grep" @@ -366,7 +366,7 @@ export namespace ToolRegistry { const builtins = [ InvalidTool, ...(question ? [QuestionTool] : []), - BashTool, + TerminalTool, ReadTool, GlobTool, GrepTool, diff --git a/packages/opencode/src/tool/retrieval.ts b/packages/opencode/src/tool/retrieval.ts index f81161d215..4275431e09 100644 --- a/packages/opencode/src/tool/retrieval.ts +++ b/packages/opencode/src/tool/retrieval.ts @@ -21,8 +21,7 @@ export namespace Retrieval { * directory-listing tool registered (file discovery is `glob` / `bash ls`), * so no "list"/"ls" entry here. */ - export const CORE = [ - "bash", "read", "write", "edit", "glob", "grep", + export const CORE = ["terminal", "read", "write", "edit", "glob", "grep", "task", "todowrite", "skill", ] diff --git a/packages/opencode/src/tool/shell/id.ts b/packages/opencode/src/tool/shell/id.ts index 061253f8fb..6ab04e71d0 100644 --- a/packages/opencode/src/tool/shell/id.ts +++ b/packages/opencode/src/tool/shell/id.ts @@ -1,4 +1,4 @@ -const kinds = ["bash", "pwsh", "powershell", "cmd"] as const +const kinds = ["terminal", "pwsh", "powershell", "cmd"] as const export type Kind = (typeof kinds)[number] const shellKinds = new Set(kinds) @@ -8,12 +8,12 @@ function isKind(value: string): value is Kind { } export function toKind(value: string): Kind { - return isKind(value) ? value : "bash" + return isKind(value) ? value : "terminal" } -// Keep the exposed tool ID and permission key as "bash" for compatibility with -// existing plugins, users, and saved permissions. Rename with opencode 2.0. -export const ToolID = "bash" +// Keep the exposed tool ID and permission key as "terminal" for compatibility with +// existing plugins, users, and saved permissions. +export const ToolID = "terminal" export type ToolID = typeof ToolID export * as ShellID from "./id" diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/terminal.ts similarity index 97% rename from packages/opencode/src/tool/bash.ts rename to packages/opencode/src/tool/terminal.ts index 4773f11ec6..8cc8905080 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/terminal.ts @@ -53,9 +53,9 @@ const parser = lazy(async () => { }) // TODO: we may wanna rename this tool so it works better on other shells -export const BashTool = Tool.define("bash", async () => { +export const TerminalTool = Tool.define("terminal", async () => { const shell = Shell.acceptable() - log.info("bash tool using shell", { shell }) + log.info("terminal tool using shell", { shell }) return { description: DESCRIPTION.replaceAll("${directory}", Instance.directory) @@ -153,7 +153,7 @@ export const BashTool = Tool.define("bash", async () => { if (patterns.size > 0) { await ctx.ask({ - permission: "bash", + permission: "terminal", patterns: Array.from(patterns), always: Array.from(always), metadata: {}, @@ -308,7 +308,7 @@ export const BashTool = Tool.define("bash", async () => { } if (resultMetadata.length > 0) { - output += "\n\n\n" + resultMetadata.join("\n") + "\n" + output += "\n\n\n" + resultMetadata.join("\n") + "\n" } return { diff --git a/packages/opencode/test/acp/event.test.ts b/packages/opencode/test/acp/event.test.ts index 8a72754f0e..b9c64040bc 100644 --- a/packages/opencode/test/acp/event.test.ts +++ b/packages/opencode/test/acp/event.test.ts @@ -235,7 +235,7 @@ function runningTool( messageID: `msg_${callID}`, type: "tool", callID, - tool: "bash", + tool: "terminal", state: { status: "running", input, @@ -283,7 +283,7 @@ function errorTool(sessionID: string, callID: string) { messageID: `msg_${callID}`, type: "tool", callID, - tool: "bash", + tool: "terminal", state: { status: "error", input: { cmd: "exit 1" }, @@ -614,7 +614,7 @@ describe("acp event routing", () => { messageID: "msg_call_pending", type: "tool", callID: "call_pending", - tool: "bash", + tool: "terminal", state: { status: "pending", input: { cmd: "printf repeat" }, diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index f5806da817..16bfb6e70e 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -73,8 +73,8 @@ it.instance("build agent has correct default properties", () => expect(build?.mode).toBe("primary") expect(build?.native).toBe(true) expect(evalPerm(build, "edit")).toBe("allow") - // altimate fork: bash defaults to "ask" (safety default) rather than "allow" - expect(evalPerm(build, "bash")).toBe("ask") + // altimate fork: terminal defaults to "ask" (safety default) rather than "allow" + expect(evalPerm(build, "terminal")).toBe("ask") }), ) @@ -91,12 +91,12 @@ it.instance("reviewer agent is read-only but usable outside the project (#978)", expect(Permission.evaluate("external_directory", "/some/sibling/repo", reviewer!.permission).action).toBe("ask") // #978: PR/issue URLs are reviewable expect(evalPerm(reviewer, "webfetch")).toBe("allow") - // #978: bash prompts (e.g. `gh pr view`) instead of hard-denying - expect(Permission.evaluate("bash", "gh pr view 66 --repo AltimateAI/.claude", reviewer!.permission).action).toBe( + // #978: terminal prompts (e.g. `gh pr view`) instead of hard-denying + expect(Permission.evaluate("terminal", "gh pr view 66 --repo AltimateAI/.claude", reviewer!.permission).action).toBe( "ask", ) - // Safety denials still hold even though bash asks - expect(Permission.evaluate("bash", "DROP DATABASE prod", reviewer!.permission).action).toBe("deny") + // Safety denials still hold even though terminal asks + expect(Permission.evaluate("terminal", "DROP DATABASE prod", reviewer!.permission).action).toBe("deny") // Review never mutates expect(evalPerm(reviewer, "edit")).toBe("deny") expect(evalPerm(reviewer, "sql_execute_write")).toBe("deny") @@ -311,7 +311,7 @@ it.instance( const build = yield* load((svc) => svc.get("build")) expect(build).toBeDefined() // Specific pattern is denied - expect(Permission.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny") + expect(Permission.evaluate("terminal", "rm -rf *", build!.permission).action).toBe("deny") // Edit still allowed expect(evalPerm(build, "edit")).toBe("allow") }), diff --git a/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts b/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts index a58a5ddf2f..a818a64a09 100644 --- a/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts +++ b/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts @@ -130,7 +130,7 @@ it.effect("subagent self permissions are preserved", () => ) expect(Permission.evaluate("read", "README.md", effective).action).toBe("allow") - expect(Permission.evaluate("bash", "git status", effective).action).toBe("allow") + expect(Permission.evaluate("terminal", "git status", effective).action).toBe("allow") expect(Permission.evaluate("task", "worker", effective).action).toBe("allow") expect(Permission.evaluate("task", "other", effective).action).toBe("deny") expect(Permission.disabled(["edit", "write", "apply_patch"], effective)).toEqual(new Set()) @@ -154,6 +154,6 @@ it.effect("subagent inherits parent session deny rules as hard runtime ceilings" }), ) - expect(Permission.evaluate("bash", "git status", effective).action).toBe("deny") + expect(Permission.evaluate("terminal", "git status", effective).action).toBe("deny") }), ) diff --git a/packages/opencode/test/altimate/carry-forward/agent-safety.test.ts b/packages/opencode/test/altimate/carry-forward/agent-safety.test.ts index 975156d54b..249a0356fa 100644 --- a/packages/opencode/test/altimate/carry-forward/agent-safety.test.ts +++ b/packages/opencode/test/altimate/carry-forward/agent-safety.test.ts @@ -86,12 +86,12 @@ it.instance("builder denies destructive DDL and defaults destructive shell to as const builder = yield* load((svc) => svc.get("builder")) expect(builder).toBeDefined() // DDL is denied outright (both upper + lower case patterns installed) - expect(Permission.evaluate("bash", "DROP DATABASE prod", builder!.permission).action).toBe("deny") - expect(Permission.evaluate("bash", "drop schema staging", builder!.permission).action).toBe("deny") - expect(Permission.evaluate("bash", "TRUNCATE events", builder!.permission).action).toBe("deny") + expect(Permission.evaluate("terminal", "DROP DATABASE prod", builder!.permission).action).toBe("deny") + expect(Permission.evaluate("terminal", "drop schema staging", builder!.permission).action).toBe("deny") + expect(Permission.evaluate("terminal", "TRUNCATE events", builder!.permission).action).toBe("deny") // Destructive file/git ops default to "ask" - expect(Permission.evaluate("bash", "rm -rf ./build", builder!.permission).action).toBe("ask") - expect(Permission.evaluate("bash", "git push --force origin main", builder!.permission).action).toBe("ask") + expect(Permission.evaluate("terminal", "rm -rf ./build", builder!.permission).action).toBe("ask") + expect(Permission.evaluate("terminal", "git push --force origin main", builder!.permission).action).toBe("ask") // Bare bash defaults to "ask", not "allow" expect(evalPerm(builder, "bash")).toBe("ask") }), @@ -105,8 +105,8 @@ it.instance( expect(builder).toBeDefined() // Even though the user set bash: allow, DDL stays denied (last-match-wins safetyDenials) expect(evalPerm(builder, "bash")).toBe("allow") - expect(Permission.evaluate("bash", "DROP DATABASE prod", builder!.permission).action).toBe("deny") - expect(Permission.evaluate("bash", "drop database prod", builder!.permission).action).toBe("deny") + expect(Permission.evaluate("terminal", "DROP DATABASE prod", builder!.permission).action).toBe("deny") + expect(Permission.evaluate("terminal", "drop database prod", builder!.permission).action).toBe("deny") }), { config: { @@ -133,7 +133,7 @@ it.instance("reviewer agent never mutates (edit/write deny, bash asks) but allow expect(reviewer?.mode).toBe("primary") // #978: bash asks (user approves each command, e.g. `gh pr view`) — never runs silently expect(evalPerm(reviewer, "bash")).toBe("ask") - expect(Permission.evaluate("bash", "DROP DATABASE prod", reviewer!.permission).action).toBe("deny") + expect(Permission.evaluate("terminal", "DROP DATABASE prod", reviewer!.permission).action).toBe("deny") expect(evalPerm(reviewer, "edit")).toBe("deny") expect(evalPerm(reviewer, "write")).toBe("deny") // The dbt PR review verdict engine + read-only analysis tools are allowed diff --git a/packages/opencode/test/altimate/trace-consumer.test.ts b/packages/opencode/test/altimate/trace-consumer.test.ts index 8bbe29b321..6e2059cba3 100644 --- a/packages/opencode/test/altimate/trace-consumer.test.ts +++ b/packages/opencode/test/altimate/trace-consumer.test.ts @@ -132,7 +132,7 @@ function sessionEvents(sessionID: string) { part: { sessionID, type: "tool", - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", diff --git a/packages/opencode/test/altimate/tracing-adversarial-2.test.ts b/packages/opencode/test/altimate/tracing-adversarial-2.test.ts index 09f22799b4..89d2300d97 100644 --- a/packages/opencode/test/altimate/tracing-adversarial-2.test.ts +++ b/packages/opencode/test/altimate/tracing-adversarial-2.test.ts @@ -51,7 +51,7 @@ describe("Clock skew and timing", () => { tracer.startTrace("s-clock-skew", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -76,7 +76,7 @@ describe("Clock skew and timing", () => { tracer.startTrace("s-zero-dur", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -96,7 +96,7 @@ describe("Clock skew and timing", () => { tracer.startTrace("s-epoch0", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -116,7 +116,7 @@ describe("Clock skew and timing", () => { tracer.startTrace("s-future", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -136,7 +136,7 @@ describe("Clock skew and timing", () => { tracer.startTrace("s-neg-ts", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -163,7 +163,7 @@ describe("Prototype pollution and exotic objects", () => { const malicious = JSON.parse('{"__proto__": {"polluted": true}, "safe": 1}') tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -190,7 +190,7 @@ describe("Prototype pollution and exotic objects", () => { const sym = Symbol("secret") const input = { normal: "value", [sym]: "hidden" } tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -214,7 +214,7 @@ describe("Prototype pollution and exotic objects", () => { const frozen = Object.freeze({ command: "ls", args: Object.freeze(["-la"]) }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -235,7 +235,7 @@ describe("Prototype pollution and exotic objects", () => { const sealed = Object.seal({ command: "ls" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -260,7 +260,7 @@ describe("Prototype pollution and exotic objects", () => { regular: "normal", } tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -288,7 +288,7 @@ describe("Prototype pollution and exotic objects", () => { }, } tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -315,7 +315,7 @@ describe("Prototype pollution and exotic objects", () => { }, } tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -341,7 +341,7 @@ describe("Prototype pollution and exotic objects", () => { }, } tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -392,7 +392,7 @@ describe("Attribute and metadata explosion", () => { bigInput[`key_${i}`] = `value_${i}` } tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -827,7 +827,7 @@ describe("Empty and minimal traces", () => { tracer.startTrace("s-tools-only", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1000, end: 2000 } }, }) diff --git a/packages/opencode/test/altimate/tracing-adversarial-final.test.ts b/packages/opencode/test/altimate/tracing-adversarial-final.test.ts index 7232aa5ef4..4b9c8693c2 100644 --- a/packages/opencode/test/altimate/tracing-adversarial-final.test.ts +++ b/packages/opencode/test/altimate/tracing-adversarial-final.test.ts @@ -86,10 +86,10 @@ describe("toolCallCount accuracy", () => { tracer.logStepStart({ id: "1" }) // This will fail inside try/catch because state is null - tracer.logToolCall({ tool: "bash", callID: "c1", state: null as any }) + tracer.logToolCall({ tool: "terminal", callID: "c1", state: null as any }) // This should succeed tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c2", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -108,7 +108,7 @@ describe("toolCallCount accuracy", () => { tracer.startTrace("s1", { prompt: "test" }) tracer.logStepStart({ id: "1" }) - tracer.logToolCall({ tool: "bash", callID: "c1", state: undefined as any }) + tracer.logToolCall({ tool: "terminal", callID: "c1", state: undefined as any }) tracer.logToolCall({ tool: "read", callID: "c2", @@ -226,7 +226,7 @@ describe("Orphaned generation — endTrace with unclosed generation", () => { await new Promise((r) => setTimeout(r, 200)) // wait for initial snapshot tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -270,7 +270,7 @@ describe("Worker race — events after endTrace", () => { const tracer = getOrCreateRecap("race-session")! tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -327,7 +327,7 @@ describe("Worker race — events after endTrace", () => { const t1 = getOrCreateRecap("cycle-test")! t1.logStepStart({ id: "1" }) t1.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "cycle1", time: { start: 1, end: 2 } }, }) @@ -412,7 +412,7 @@ describe("buildTraceFile — status transitions", () => { // Start generation — internal state now has currentGenerationSpanId tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) diff --git a/packages/opencode/test/altimate/tracing-adversarial-snapshot.test.ts b/packages/opencode/test/altimate/tracing-adversarial-snapshot.test.ts index 02922b50b2..4493dbbf28 100644 --- a/packages/opencode/test/altimate/tracing-adversarial-snapshot.test.ts +++ b/packages/opencode/test/altimate/tracing-adversarial-snapshot.test.ts @@ -70,7 +70,7 @@ describe("buildTraceFile — snapshot isolation", () => { }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -98,7 +98,7 @@ describe("buildTraceFile — snapshot isolation", () => { tracer.startTrace("s-span-isolate", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -133,7 +133,7 @@ describe("buildTraceFile — snapshot isolation", () => { tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -164,7 +164,7 @@ describe("snapshot — debouncing and atomicity", () => { // Fire 20 tool calls rapidly — each triggers snapshot() for (let i = 0; i < 20; i++) { tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: `c-${i}`, state: { status: "completed", input: {}, output: `out-${i}`, time: { start: 1, end: 2 } }, }) @@ -192,7 +192,7 @@ describe("snapshot — debouncing and atomicity", () => { tracer.startTrace("s-unwritable", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -213,7 +213,7 @@ describe("snapshot — debouncing and atomicity", () => { // Trigger a tool call (which triggers snapshot) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -242,7 +242,7 @@ describe("snapshot — debouncing and atomicity", () => { // Now log more events (should be no-ops, but they'd trigger snapshot too) tracer.logStepStart({ id: "2" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c-post", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -284,7 +284,7 @@ describe("Worker tracing — session lifecycle simulation", () => { const t1 = getOrCreateRecap("session-lifecycle")! t1.logStepStart({ id: "1" }) t1.logToolCall({ - tool: "bash", callID: "c1", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "cycle 1", time: { start: 1, end: 2 } }, }) t1.logStepFinish(ZERO_STEP) @@ -398,7 +398,7 @@ describe("Concurrent snapshot + endTrace race", () => { // Trigger snapshot via tool call tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -510,7 +510,7 @@ describe("Live trace viewer — /api/trace", () => { // Add a tool call and wait for snapshot tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", callID: "c1", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) await new Promise((r) => setTimeout(r, 50)) @@ -576,7 +576,7 @@ describe("Snapshot with non-serializable data in spans", () => { tracer.startTrace("s-func-attr", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", callID: "c1", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) // Wait for the tool snapshot to settle first @@ -636,7 +636,7 @@ describe("Stress test — snapshot interleaving", () => { tracer.startTrace(`stress-${i}`, { prompt: `prompt-${i}` }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", callID: `c-${i}`, + tool: "terminal", callID: `c-${i}`, state: { status: "completed", input: { i }, output: `ok-${i}`, time: { start: 1, end: 2 } }, }) tracer.logStepFinish({ diff --git a/packages/opencode/test/altimate/tracing-adversarial.test.ts b/packages/opencode/test/altimate/tracing-adversarial.test.ts index ede580ee7a..bddda5efc7 100644 --- a/packages/opencode/test/altimate/tracing-adversarial.test.ts +++ b/packages/opencode/test/altimate/tracing-adversarial.test.ts @@ -101,7 +101,7 @@ describe("Adversarial — malformed input", () => { circular.self = circular tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -178,7 +178,7 @@ describe("Adversarial — malformed input", () => { tracer.startTrace("s-err-type", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "error", @@ -202,7 +202,7 @@ describe("Adversarial — malformed input", () => { tracer.startTrace("s-undef-err", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "error", @@ -250,7 +250,7 @@ describe("Adversarial — malformed input", () => { tracer.startTrace("s-no-time", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -280,7 +280,7 @@ describe("Adversarial — unicode and special characters", () => { tracer.startTrace("s-emoji", { prompt: "Fix the 🐛 in the 🔧 pipeline 🚀" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -486,7 +486,7 @@ describe("Adversarial — concurrency", () => { // Fire 100 tool calls synchronously as fast as possible for (let i = 0; i < 100; i++) { tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: `rapid-${i}`, state: { status: "completed", @@ -700,7 +700,7 @@ describe("Adversarial — state machine", () => { tracer.startTrace("s-interleave", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -742,7 +742,7 @@ describe("Adversarial — state machine", () => { // These should all be no-ops, not crashes tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -774,7 +774,7 @@ describe("Adversarial — JSON serialization", () => { tracer.startTrace("s-date", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -800,7 +800,7 @@ describe("Adversarial — JSON serialization", () => { tracer.startTrace("s-bigint", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -854,7 +854,7 @@ describe("Adversarial — JSON serialization", () => { tracer.startTrace("s-deep", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", diff --git a/packages/opencode/test/altimate/tracing-de-attributes.test.ts b/packages/opencode/test/altimate/tracing-de-attributes.test.ts index 826d9849d1..5765078113 100644 --- a/packages/opencode/test/altimate/tracing-de-attributes.test.ts +++ b/packages/opencode/test/altimate/tracing-de-attributes.test.ts @@ -310,7 +310,7 @@ describe("Real-world data engineering scenarios", () => { // Tool call for dbt run tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", diff --git a/packages/opencode/test/altimate/tracing-display-crash.test.ts b/packages/opencode/test/altimate/tracing-display-crash.test.ts index 5017cd3c0c..3c77e7131e 100644 --- a/packages/opencode/test/altimate/tracing-display-crash.test.ts +++ b/packages/opencode/test/altimate/tracing-display-crash.test.ts @@ -104,7 +104,7 @@ describe("flushSync — crash recovery", () => { tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) diff --git a/packages/opencode/test/altimate/tracing-e2e.test.ts b/packages/opencode/test/altimate/tracing-e2e.test.ts index 420f6db9d1..07502a5833 100644 --- a/packages/opencode/test/altimate/tracing-e2e.test.ts +++ b/packages/opencode/test/altimate/tracing-e2e.test.ts @@ -179,7 +179,7 @@ describe("Incremental snapshots", () => { // Now add tool call and wait for its snapshot tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -218,7 +218,7 @@ describe("Incremental snapshots", () => { // First tool tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: Date.now() - 100, end: Date.now() } }, }) @@ -353,7 +353,7 @@ describe("Performance", () => { const start = performance.now() for (let i = 0; i < 1000; i++) { tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: `c-${i}`, state: { status: "completed", @@ -400,7 +400,7 @@ describe("Performance", () => { tracer.startTrace("s-perf-attrs", { prompt: "perf test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", callID: "c1", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) @@ -428,7 +428,7 @@ describe("Performance", () => { tracer.logStepStart({ id: "1" }) for (let i = 0; i < 1000; i++) { tracer.logToolCall({ - tool: "bash", callID: `c-${i}`, + tool: "terminal", callID: `c-${i}`, state: { status: "completed", input: { i }, output: `out-${i}`, time: { start: 1, end: 2 } }, }) } @@ -455,7 +455,7 @@ describe("Performance", () => { // logToolCall triggers snapshot — measure that it returns immediately const start = performance.now() tracer.logToolCall({ - tool: "bash", callID: "c1", + tool: "terminal", callID: "c1", state: { status: "completed", input: { data: "x".repeat(100000) }, // Large input @@ -567,7 +567,7 @@ describe("Worker event simulation", () => { const events = [ { type: "message.updated", properties: { info: { role: "assistant", modelID: "claude-sonnet-4-20250514", providerID: "anthropic", agent: "builder", variant: "high", parentID: "session-tui-1" } } }, { type: "message.part.updated", properties: { part: { sessionID: "session-tui-1", type: "step-start", id: "step-1" } } }, - { type: "message.part.updated", properties: { part: { sessionID: "session-tui-1", type: "tool", tool: "bash", callID: "c1", state: { status: "completed", input: { command: "ls" }, output: "file1.ts\nfile2.ts", time: { start: Date.now() - 1000, end: Date.now() } } } } }, + { type: "message.part.updated", properties: { part: { sessionID: "session-tui-1", type: "tool", tool: "terminal", callID: "c1", state: { status: "completed", input: { command: "ls" }, output: "file1.ts\nfile2.ts", time: { start: Date.now() - 1000, end: Date.now() } } } } }, { type: "message.part.updated", properties: { part: { sessionID: "session-tui-1", type: "text", text: "Found files.", time: { end: Date.now() } } } }, { type: "message.part.updated", properties: { part: { sessionID: "session-tui-1", type: "step-finish", id: "step-1", reason: "stop", cost: 0.005, tokens: { input: 500, output: 100, reasoning: 0, cache: { read: 0, write: 0 } } } } }, { type: "session.status", properties: { sessionID: "session-tui-1", status: { type: "idle" } } }, @@ -671,7 +671,7 @@ describe("Trace viewer server", () => { tracer.startTrace("viewer-test", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", callID: "c1", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) tracer.logStepFinish({ diff --git a/packages/opencode/test/altimate/tracing-final-audit.test.ts b/packages/opencode/test/altimate/tracing-final-audit.test.ts index 4e2ebcdacb..7ac540b25e 100644 --- a/packages/opencode/test/altimate/tracing-final-audit.test.ts +++ b/packages/opencode/test/altimate/tracing-final-audit.test.ts @@ -187,7 +187,7 @@ describe("logToolCall — tool name edge cases", () => { tracer.startTrace("s1", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -208,7 +208,7 @@ describe("logToolCall — tool name edge cases", () => { tracer.startTrace("s1", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -229,7 +229,7 @@ describe("logToolCall — tool name edge cases", () => { tracer.startTrace("s1", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -495,7 +495,7 @@ describe("Generation span — input from previous tool results", () => { // First generation with a tool call tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: { cmd: "ls" }, output: "file1.ts\nfile2.ts", time: { start: 1000, end: 2000 } }, }) @@ -522,7 +522,7 @@ describe("Generation span — input from previous tool results", () => { tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "error", input: {}, error: "Permission denied", time: { start: 1000, end: 2000 } }, }) @@ -549,7 +549,7 @@ describe("Generation output composition", () => { tracer.startTrace("s1", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1000, end: 2000 } }, }) @@ -711,7 +711,7 @@ describe("Structural invariants", () => { tracer.startTrace("s1", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", callID: "c1", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1000, end: 2000 } }, }) tracer.logStepFinish(ZERO_STEP) @@ -753,7 +753,7 @@ describe("Structural invariants", () => { tracer.startTrace("s1", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", callID: "c1", + tool: "terminal", callID: "c1", state: { status: "completed", input: {}, output: "ok", time: { start: 1000, end: 2000 } }, }) tracer.logToolCall({ diff --git a/packages/opencode/test/altimate/tracing-integration.test.ts b/packages/opencode/test/altimate/tracing-integration.test.ts index a945aaa34c..bd9bb6b19a 100644 --- a/packages/opencode/test/altimate/tracing-integration.test.ts +++ b/packages/opencode/test/altimate/tracing-integration.test.ts @@ -107,7 +107,7 @@ describe("Write → Read round-trip", () => { [DE.SQL.VALIDATION_VALID]: true, }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c2", state: { status: "error", @@ -541,7 +541,7 @@ describe("DE attributes on different span types in same trace", () => { // dbt tool tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c2", state: { status: "completed", input: { cmd: "dbt test" }, output: "4 passed", time: { start: 2000, end: 5000 } }, }) @@ -617,7 +617,7 @@ describe("Complete pipeline edge cases", () => { }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", input: { cmd: "ls" }, output: "ok", time: { start: 1000, end: 2000 } }, }) diff --git a/packages/opencode/test/altimate/tracing-rename-race.test.ts b/packages/opencode/test/altimate/tracing-rename-race.test.ts index cf8c8c67a7..91e0ef5d8e 100644 --- a/packages/opencode/test/altimate/tracing-rename-race.test.ts +++ b/packages/opencode/test/altimate/tracing-rename-race.test.ts @@ -69,7 +69,7 @@ describe("trace corruption — flushSync vs in-flight rename race", () => { await new Promise((r) => setTimeout(r, 300)) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c-pre-crash", state: { status: "completed", input: {}, output: "ok", time: { start: 1, end: 2 } }, }) diff --git a/packages/opencode/test/altimate/tracing-thorough.test.ts b/packages/opencode/test/altimate/tracing-thorough.test.ts index d528746e46..4830a70ac1 100644 --- a/packages/opencode/test/altimate/tracing-thorough.test.ts +++ b/packages/opencode/test/altimate/tracing-thorough.test.ts @@ -113,7 +113,7 @@ describe("logToolCall — state.time null/undefined", () => { tracer.startTrace("s1", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -136,7 +136,7 @@ describe("logToolCall — state.time null/undefined", () => { tracer.startTrace("s1", { prompt: "test" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -156,7 +156,7 @@ describe("logToolCall — state.time null/undefined", () => { tracer.logStepStart({ id: "1" }) // The try/catch should handle this tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: null as any, }) diff --git a/packages/opencode/test/altimate/tracing.test.ts b/packages/opencode/test/altimate/tracing.test.ts index acd76ebe0f..15aaa900bf 100644 --- a/packages/opencode/test/altimate/tracing.test.ts +++ b/packages/opencode/test/altimate/tracing.test.ts @@ -313,7 +313,7 @@ describe("Recap — graceful degradation", () => { tracer.startTrace("s-empty-input", { prompt: "x" }) tracer.logStepStart({ id: "1" }) tracer.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -989,7 +989,7 @@ describe("Loop detection", () => { // Call same tool 5 times with identical input for (let i = 0; i < 5; i++) { recap.logToolCall({ - tool: "bash", + tool: "terminal", callID: `c-${i}`, state: { status: "completed", @@ -1018,7 +1018,7 @@ describe("Loop detection", () => { recap.startTrace("s-no-loop-diff-tools", { prompt: "test" }) recap.logStepStart({ id: "1" }) - const tools = ["bash", "read", "edit", "glob", "grep"] + const tools = ["terminal", "read", "edit", "glob", "grep"] for (let i = 0; i < 5; i++) { recap.logToolCall({ tool: tools[i]!, @@ -1048,7 +1048,7 @@ describe("Loop detection", () => { for (let i = 0; i < 5; i++) { recap.logToolCall({ - tool: "bash", + tool: "terminal", callID: `c-${i}`, state: { status: "completed", @@ -1076,7 +1076,7 @@ describe("Loop detection", () => { // Call same tool 5 times with empty input for (let i = 0; i < 5; i++) { recap.logToolCall({ - tool: "bash", + tool: "terminal", callID: `c-${i}`, state: { status: "completed", @@ -1120,7 +1120,7 @@ describe("Loop detection", () => { // Only 2 repeats of the same tool (below threshold of 3) for (let i = 0; i < 2; i++) { recap.logToolCall({ - tool: "bash", + tool: "terminal", callID: `c-bash-${i}`, state: { status: "completed", @@ -1165,7 +1165,7 @@ describe("Loop detection", () => { // of the surviving slice, well within the last-10 detection window for (let i = 0; i < 3; i++) { recap.logToolCall({ - tool: "bash", + tool: "terminal", callID: `c-loop-${i}`, state: { status: "completed", @@ -1196,7 +1196,7 @@ describe("Loop detection", () => { // Interleave two different loops: bash(ls) and read(file.ts) for (let i = 0; i < 4; i++) { recap.logToolCall({ - tool: "bash", + tool: "terminal", callID: `c-bash-${i}`, state: { status: "completed", @@ -1268,7 +1268,7 @@ describe("Post-session narrative", () => { // Call bash 3 times, read 2 times, edit 1 time for (let i = 0; i < 3; i++) { recap.logToolCall({ - tool: "bash", + tool: "terminal", callID: `c-bash-${i}`, state: { status: "completed", @@ -1346,7 +1346,7 @@ describe("Post-session narrative", () => { // Trigger a loop: same tool+input 5 times for (let i = 0; i < 5; i++) { recap.logToolCall({ - tool: "bash", + tool: "terminal", callID: `c-${i}`, state: { status: "completed", @@ -1373,7 +1373,7 @@ describe("Post-session narrative", () => { recap.logStepStart({ id: "1" }) recap.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c1", state: { status: "completed", @@ -1383,7 +1383,7 @@ describe("Post-session narrative", () => { }, }) recap.logToolCall({ - tool: "bash", + tool: "terminal", callID: "c2", state: { status: "completed", @@ -1397,9 +1397,9 @@ describe("Post-session narrative", () => { const filePath = await recap.endTrace() const traceFile: TraceFile = JSON.parse(await fs.readFile(filePath!, "utf-8")) - const bashTool = traceFile.summary.topTools!.find((t) => t.name === "bash")! - expect(bashTool.count).toBe(2) - expect(bashTool.totalDuration).toBe(4000) // 2500 + 1500 + const terminalTool = traceFile.summary.topTools!.find((t) => t.name === "terminal")! + expect(terminalTool.count).toBe(2) + expect(terminalTool.totalDuration).toBe(4000) // 2500 + 1500 }) }) @@ -1527,7 +1527,7 @@ describe("viewer adversarial tests", () => { const traceFile = makeTrace({ spans: [ { spanId: "s0", parentSpanId: null, name: "session", kind: "session", startTime: 0, status: "ok" }, - { spanId: "s1", parentSpanId: "s0", name: "bash", kind: "tool", startTime: 1, endTime: 2, status: "ok", + { spanId: "s1", parentSpanId: "s0", name: "terminal", kind: "tool", startTime: 1, endTime: 2, status: "ok", tool: { callId: "c1", durationMs: 1 }, input: { command: '' }, output: 'Completed successfully' }, @@ -1628,11 +1628,11 @@ describe("viewer adversarial tests", () => { const traceFile = makeTrace({ spans: [ { spanId: "s0", parentSpanId: null, name: "session", kind: "session", startTime: 0, status: "ok" }, - { spanId: "s1", parentSpanId: "s0", name: "bash", kind: "tool", startTime: 1, endTime: 2, status: "ok", + { spanId: "s1", parentSpanId: "s0", name: "terminal", kind: "tool", startTime: 1, endTime: 2, status: "ok", tool: { callId: "c1", durationMs: 1 }, input: { command: "pytest tests/" }, output: "5 tests passed, 0 failed" }, - { spanId: "s2", parentSpanId: "s0", name: "bash", kind: "tool", startTime: 3, endTime: 4, status: "ok", + { spanId: "s2", parentSpanId: "s0", name: "terminal", kind: "tool", startTime: 3, endTime: 4, status: "ok", tool: { callId: "c2", durationMs: 1 }, input: { command: "pip install pandas" }, output: "Successfully installed pandas-2.0.0" }, diff --git a/packages/opencode/test/branding/build-integrity.test.ts b/packages/opencode/test/branding/build-integrity.test.ts index 132dbf61ad..1d05b6b3b8 100644 --- a/packages/opencode/test/branding/build-integrity.test.ts +++ b/packages/opencode/test/branding/build-integrity.test.ts @@ -325,7 +325,7 @@ describe("Bundle Completeness", () => { }) test("bash.ts prepends ALTIMATE_BIN_DIR to PATH", () => { - const bashTs = readFileSync(join(repoRoot, "packages/opencode/src/tool/bash.ts"), "utf-8") + const bashTs = readFileSync(join(repoRoot, "packages/opencode/src/tool/terminal.ts"), "utf-8") expect(bashTs).toContain("ALTIMATE_BIN_DIR") expect(bashTs).toContain("PATH") }) diff --git a/packages/opencode/test/cli/run/entry.body.test.ts b/packages/opencode/test/cli/run/entry.body.test.ts index 17659113c5..28a6332d41 100644 --- a/packages/opencode/test/cli/run/entry.body.test.ts +++ b/packages/opencode/test/cli/run/entry.body.test.ts @@ -289,7 +289,7 @@ describe("run entry body", () => { text: "partial output", phase: "progress", source: "tool", - tool: "bash", + tool: "terminal", partID: "tool-2", }), ) @@ -305,7 +305,7 @@ describe("run entry body", () => { text: "partial output", phase: "progress", source: "tool", - tool: "bash", + tool: "terminal", }), body, ), @@ -317,7 +317,7 @@ describe("run entry body", () => { text: "output", phase: "progress", source: "tool", - tool: "bash", + tool: "terminal", toolState: "completed", }), ), @@ -328,7 +328,7 @@ describe("run entry body", () => { expect( entryBody( toolCommit({ - tool: "bash", + tool: "terminal", phase: "progress", toolState: "completed", text: ["/tmp/demo", "git status", "On branch demo", "nothing to commit, working tree clean", ""].join("\n"), @@ -359,7 +359,7 @@ describe("run entry body", () => { expect( entryBody( toolCommit({ - tool: "bash", + tool: "terminal", phase: "start", toolState: "running", text: "running shell", @@ -386,7 +386,7 @@ describe("run entry body", () => { text: "running shell", phase: "start", source: "tool", - tool: "bash", + tool: "terminal", partID: "shell:call-1", toolState: "running", shell: { @@ -407,7 +407,7 @@ describe("run entry body", () => { text: "/tmp/demo\n", phase: "progress", source: "tool", - tool: "bash", + tool: "terminal", partID: "shell:call-1", toolState: "completed", shell: { diff --git a/packages/opencode/test/cli/run/footer.view.test.tsx b/packages/opencode/test/cli/run/footer.view.test.tsx index 2e9fd8ef17..70e49bcec2 100644 --- a/packages/opencode/test/cli/run/footer.view.test.tsx +++ b/packages/opencode/test/cli/run/footer.view.test.tsx @@ -320,7 +320,7 @@ test("run entry content updates when live commit text changes", async () => { source: "tool", messageID: "msg-1", partID: "part-1", - tool: "bash", + tool: "terminal", }) const app = await testRender( @@ -346,7 +346,7 @@ test("run entry content updates when live commit text changes", async () => { source: "tool", messageID: "msg-1", partID: "part-1", - tool: "bash", + tool: "terminal", }) await app.renderOnce() diff --git a/packages/opencode/test/cli/run/permission.shared.test.ts b/packages/opencode/test/cli/run/permission.shared.test.ts index 58ec1574da..61fd3f0510 100644 --- a/packages/opencode/test/cli/run/permission.shared.test.ts +++ b/packages/opencode/test/cli/run/permission.shared.test.ts @@ -80,7 +80,7 @@ describe("run permission shared", () => { expect( permissionInfo( req({ - permission: "bash", + permission: "terminal", metadata: { input: { command: "git status --short", @@ -132,8 +132,8 @@ describe("run permission shared", () => { test("formats always-allow copy for wildcard and explicit patterns", () => { // altimate_change — fork branding: copy says "Altimate Code", not "OpenCode". - expect(permissionAlwaysLines(req({ permission: "bash", always: ["*"] }))).toEqual([ - "This will allow bash until Altimate Code is restarted.", + expect(permissionAlwaysLines(req({ permission: "terminal", always: ["*"] }))).toEqual([ + "This will allow terminal until Altimate Code is restarted.", ]) expect(permissionAlwaysLines(req({ always: ["src/**/*.ts", "src/**/*.tsx"] }))).toEqual([ diff --git a/packages/opencode/test/cli/run/scrollback.surface.test.ts b/packages/opencode/test/cli/run/scrollback.surface.test.ts index f1500ec44a..ad5f4fd73a 100644 --- a/packages/opencode/test/cli/run/scrollback.surface.test.ts +++ b/packages/opencode/test/cli/run/scrollback.surface.test.ts @@ -573,9 +573,9 @@ test("coalesces same-line tool progress into one snapshot", async () => { const out = await setup() try { - await out.scrollback.append(toolCommit({ tool: "bash", phase: "progress", text: "abc" })) - await out.scrollback.append(toolCommit({ tool: "bash", phase: "progress", text: "def" })) - await out.scrollback.append(toolCommit({ tool: "bash", phase: "final", text: "", toolState: "completed" })) + await out.scrollback.append(toolCommit({ tool: "terminal", phase: "progress", text: "abc" })) + await out.scrollback.append(toolCommit({ tool: "terminal", phase: "progress", text: "def" })) + await out.scrollback.append(toolCommit({ tool: "terminal", phase: "final", text: "", toolState: "completed" })) const commits = claim(out.renderer) try { @@ -607,7 +607,7 @@ test("renders completed bash output with one blank line after the command and be take() await out.scrollback.append( toolCommit({ - tool: "bash", + tool: "terminal", phase: "start", toolState: "running", state: { @@ -624,7 +624,7 @@ test("renders completed bash output with one blank line after the command and be take() await out.scrollback.append( toolCommit({ - tool: "bash", + tool: "terminal", phase: "progress", toolState: "completed", text: ["/tmp/demo", "git status", "On branch demo", "nothing to commit, working tree clean", ""].join("\n"), @@ -669,7 +669,7 @@ test("inserts a spacer before the next tool after completed multiline bash outpu await out.scrollback.append( toolCommit({ - tool: "bash", + tool: "terminal", phase: "start", toolState: "running", state: { @@ -686,7 +686,7 @@ test("inserts a spacer before the next tool after completed multiline bash outpu take() await out.scrollback.append( toolCommit({ - tool: "bash", + tool: "terminal", phase: "progress", toolState: "completed", text: ["/tmp/demo", "pwd; ls -la", "/tmp/demo", "total 4", "", ""].join("\n"), @@ -747,7 +747,7 @@ test("does not double-space before completed bash output when inline tool header await out.scrollback.append( toolCommit({ - tool: "bash", + tool: "terminal", phase: "start", toolState: "running", state: { @@ -796,7 +796,7 @@ test("does not double-space before completed bash output when inline tool header take() await out.scrollback.append( toolCommit({ - tool: "bash", + tool: "terminal", phase: "progress", toolState: "completed", text: ["src/cli/cmd/run", "ls", "demo.ts", "entry.body.ts", "", ""].join("\n"), diff --git a/packages/opencode/test/cli/run/session-data.test.ts b/packages/opencode/test/cli/run/session-data.test.ts index 705e0250de..a5486ffd9c 100644 --- a/packages/opencode/test/cli/run/session-data.test.ts +++ b/packages/opencode/test/cli/run/session-data.test.ts @@ -240,7 +240,7 @@ describe("run session data", () => { properties: { id: "perm-1", sessionID: "session-1", - permission: "bash", + permission: "terminal", patterns: ["src/**/*.ts"], metadata: {}, always: [], @@ -257,7 +257,7 @@ describe("run session data", () => { id: "tool-1", messageID: "msg-1", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "running", input: { @@ -290,7 +290,7 @@ describe("run session data", () => { tool({ id: "tool-1", messageID: "msg-1", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { @@ -343,7 +343,7 @@ describe("run session data", () => { kind: "tool", phase: "start", partID: "shell:call-1", - tool: "bash", + tool: "terminal", shell: { callID: "call-1", command: "pwd", @@ -367,7 +367,7 @@ describe("run session data", () => { kind: "tool", phase: "progress", partID: "shell:call-1", - tool: "bash", + tool: "terminal", text: "/tmp/demo\n", toolState: "completed", shell: { @@ -396,7 +396,7 @@ describe("run session data", () => { id: "tool-1", messageID: "msg-1", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "running", input: { @@ -425,7 +425,7 @@ describe("run session data", () => { id: "tool-1", messageID: "msg-1", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { @@ -451,7 +451,7 @@ describe("run session data", () => { id: "tool-1", messageID: "msg-1", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "running", input: { @@ -480,7 +480,7 @@ describe("run session data", () => { id: "tool-1", messageID: "msg-1", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { diff --git a/packages/opencode/test/cli/run/session-replay.test.ts b/packages/opencode/test/cli/run/session-replay.test.ts index 18ae82d4da..8d08967e81 100644 --- a/packages/opencode/test/cli/run/session-replay.test.ts +++ b/packages/opencode/test/cli/run/session-replay.test.ts @@ -169,7 +169,7 @@ function runningToolMessage(id: string): SessionMessages[number] { messageID: id, type: "tool", callID: `${id}-call`, - tool: "bash", + tool: "terminal", state: { status: "running", input: { @@ -228,7 +228,7 @@ function shellAssistantMessage(id: string, parentID: string): SessionMessages[nu messageID: id, type: "tool", callID: `${id}-call`, - tool: "bash", + tool: "terminal", state: { status: "completed", input: { @@ -387,7 +387,7 @@ describe("run session replay", () => { expect.objectContaining({ kind: "tool", text: "account.ts\n", - tool: "bash", + tool: "terminal", toolState: "completed", }), ) diff --git a/packages/opencode/test/cli/run/stream.transport.test.ts b/packages/opencode/test/cli/run/stream.transport.test.ts index 5bb578447f..d30407b160 100644 --- a/packages/opencode/test/cli/run/stream.transport.test.ts +++ b/packages/opencode/test/cli/run/stream.transport.test.ts @@ -710,7 +710,7 @@ describe("run stream transport", () => { messageID: "msg-1", id: "bash-1", callID: "call-1", - tool: "bash", + tool: "terminal", body: { command: "pwd", }, diff --git a/packages/opencode/test/cli/run/subagent-data.test.ts b/packages/opencode/test/cli/run/subagent-data.test.ts index 4dcbd09608..2d31e55e9d 100644 --- a/packages/opencode/test/cli/run/subagent-data.test.ts +++ b/packages/opencode/test/cli/run/subagent-data.test.ts @@ -346,7 +346,7 @@ describe("run subagent data", () => { sessionID: "child-1", type: "tool", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "running", input: { @@ -362,7 +362,7 @@ describe("run subagent data", () => { properties: { id: "perm-1", sessionID: "child-1", - permission: "bash", + permission: "terminal", patterns: ["git status --short"], metadata: {}, always: [], diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index e438545369..212ad95abc 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -872,7 +872,7 @@ Ordered permissions`, ) const config = yield* Config.use.get() - expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"]) + expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["terminal", "*", "edit"]) }), ) @@ -1421,7 +1421,7 @@ test("config parser preserves permission order while rejecting unknown top-level "test", ) - expect(Object.keys(config.permission!)).toEqual(["bash", "*", "edit"]) + expect(Object.keys(config.permission!)).toEqual(["terminal", "*", "edit"]) try { ConfigParse.schema(ConfigV1.Info, { invalid_field: true }, "test") throw new Error("expected config parse to fail") diff --git a/packages/opencode/test/file/security-e2e.test.ts b/packages/opencode/test/file/security-e2e.test.ts index 120316c8b8..113c8549e9 100644 --- a/packages/opencode/test/file/security-e2e.test.ts +++ b/packages/opencode/test/file/security-e2e.test.ts @@ -570,26 +570,26 @@ describe("E2E: bash deny defaults", () => { }) // Database DDL is blocked entirely (deny) — both upper and lowercase - expect(PermissionNext.evaluate("bash", "DROP DATABASE production", defaults).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "DROP SCHEMA public", defaults).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "TRUNCATE users", defaults).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "drop database production", defaults).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "drop schema public", defaults).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "truncate users", defaults).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "DROP DATABASE production", defaults).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "DROP SCHEMA public", defaults).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "TRUNCATE users", defaults).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "drop database production", defaults).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "drop schema public", defaults).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "truncate users", defaults).action).toBe("deny") // Destructive file/git commands are prompted (ask), not blocked // This is intentional — rm -rf ./build, git push --force after rebase, etc. are legitimate - expect(PermissionNext.evaluate("bash", "rm -rf ./build", defaults).action).toBe("ask") - expect(PermissionNext.evaluate("bash", "git push --force origin main", defaults).action).toBe("ask") - expect(PermissionNext.evaluate("bash", "git reset --hard HEAD~5", defaults).action).toBe("ask") - expect(PermissionNext.evaluate("bash", "git clean -f", defaults).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "rm -rf ./build", defaults).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "git push --force origin main", defaults).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "git reset --hard HEAD~5", defaults).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "git clean -f", defaults).action).toBe("ask") // Regular commands also prompt (ask) - expect(PermissionNext.evaluate("bash", "ls -la", defaults).action).toBe("ask") - expect(PermissionNext.evaluate("bash", "git status", defaults).action).toBe("ask") - expect(PermissionNext.evaluate("bash", "dbt run", defaults).action).toBe("ask") - expect(PermissionNext.evaluate("bash", "npm install", defaults).action).toBe("ask") - expect(PermissionNext.evaluate("bash", "git push origin main", defaults).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "ls -la", defaults).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "git status", defaults).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "dbt run", defaults).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "npm install", defaults).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "git push origin main", defaults).action).toBe("ask") }) test("user config can override defaults via merge (last-match-wins)", () => { @@ -608,9 +608,9 @@ describe("E2E: bash deny defaults", () => { const merged = PermissionNext.merge(defaults, userOverride) // Specific user override allows dropping a test database (last-match-wins) - expect(PermissionNext.evaluate("bash", "DROP DATABASE test_db", merged).action).toBe("allow") + expect(PermissionNext.evaluate("terminal", "DROP DATABASE test_db", merged).action).toBe("allow") // Other DROP DATABASE commands still denied (deny from defaults, no user override matches) - expect(PermissionNext.evaluate("bash", "DROP DATABASE production", merged).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "DROP DATABASE production", merged).action).toBe("deny") }) }) diff --git a/packages/opencode/test/permission-task.test.ts b/packages/opencode/test/permission-task.test.ts index e5d92c5815..62d300b8af 100644 --- a/packages/opencode/test/permission-task.test.ts +++ b/packages/opencode/test/permission-task.test.ts @@ -228,12 +228,12 @@ describe("permission.task with real config files", () => { expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") // Verify other tool permissions - expect(Permission.evaluate("bash", "*", ruleset).action).toBe("allow") + expect(Permission.evaluate("terminal", "*", ruleset).action).toBe("allow") expect(Permission.evaluate("edit", "*", ruleset).action).toBe("ask") // Verify disabled tools - const disabled = Permission.disabled(["bash", "edit", "task"], ruleset) - expect(disabled.has("bash")).toBe(false) + const disabled = Permission.disabled(["terminal", "edit", "task"], ruleset) + expect(disabled.has("terminal")).toBe(false) expect(disabled.has("edit")).toBe(false) // task is NOT disabled because disabled() uses findLast, and the last rule // matching "task" permission is {pattern: "general", action: "allow"}, not pattern: "*" diff --git a/packages/opencode/test/permission-yolo.test.ts b/packages/opencode/test/permission-yolo.test.ts index cd8b9de2b3..9bfa6bc4f6 100644 --- a/packages/opencode/test/permission-yolo.test.ts +++ b/packages/opencode/test/permission-yolo.test.ts @@ -110,64 +110,64 @@ describe("yolo mode: deny rules cannot be bypassed", () => { test("evaluate still returns deny regardless of any external state", () => { const denyRules: PermissionNext.Ruleset = [ - { permission: "bash", pattern: "DROP DATABASE *", action: "deny" }, - { permission: "bash", pattern: "DROP SCHEMA *", action: "deny" }, - { permission: "bash", pattern: "TRUNCATE *", action: "deny" }, + { permission: "terminal", pattern: "DROP DATABASE *", action: "deny" }, + { permission: "terminal", pattern: "DROP SCHEMA *", action: "deny" }, + { permission: "terminal", pattern: "TRUNCATE *", action: "deny" }, ] - expect(PermissionNext.evaluate("bash", "DROP DATABASE production", denyRules).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "DROP SCHEMA public", denyRules).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "TRUNCATE users", denyRules).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "DROP DATABASE production", denyRules).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "DROP SCHEMA public", denyRules).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "TRUNCATE users", denyRules).action).toBe("deny") }) test("deny rule wins over earlier allow-all wildcard (last-match-wins)", () => { const rules: PermissionNext.Ruleset = [ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "DROP DATABASE *", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "DROP DATABASE *", action: "deny" }, ] - expect(PermissionNext.evaluate("bash", "DROP DATABASE prod", rules).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "ls -la", rules).action).toBe("allow") + expect(PermissionNext.evaluate("terminal", "DROP DATABASE prod", rules).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "ls -la", rules).action).toBe("allow") }) test("deny rule wins even with complex overlapping patterns", () => { const rules: PermissionNext.Ruleset = [ { permission: "*", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "rm -rf *", action: "ask" }, - { permission: "bash", pattern: "DROP *", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "rm -rf *", action: "ask" }, + { permission: "terminal", pattern: "DROP *", action: "deny" }, ] - expect(PermissionNext.evaluate("bash", "DROP TABLE users", rules).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "rm -rf /tmp", rules).action).toBe("ask") - expect(PermissionNext.evaluate("bash", "echo hello", rules).action).toBe("allow") + expect(PermissionNext.evaluate("terminal", "DROP TABLE users", rules).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "rm -rf /tmp", rules).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "echo hello", rules).action).toBe("allow") }) test("case-sensitive deny patterns match exactly", () => { const rules: PermissionNext.Ruleset = [ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "DROP DATABASE *", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "DROP DATABASE *", action: "deny" }, ] // Uppercase matches - expect(PermissionNext.evaluate("bash", "DROP DATABASE prod", rules).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "DROP DATABASE prod", rules).action).toBe("deny") // Lowercase does NOT match (patterns are case-sensitive) - expect(PermissionNext.evaluate("bash", "drop database prod", rules).action).toBe("allow") + expect(PermissionNext.evaluate("terminal", "drop database prod", rules).action).toBe("allow") }) test("default agent rules include both cases of dangerous commands", () => { // Verify the default agent rules cover both cases (from agent.ts) - const defaultBashRules: PermissionNext.Ruleset = [ - { permission: "bash", pattern: "*", action: "ask" }, - { permission: "bash", pattern: "DROP DATABASE *", action: "deny" }, - { permission: "bash", pattern: "DROP SCHEMA *", action: "deny" }, - { permission: "bash", pattern: "TRUNCATE *", action: "deny" }, - { permission: "bash", pattern: "drop database *", action: "deny" }, - { permission: "bash", pattern: "drop schema *", action: "deny" }, - { permission: "bash", pattern: "truncate *", action: "deny" }, + const defaultTerminalRules: PermissionNext.Ruleset = [ + { permission: "terminal", pattern: "*", action: "ask" }, + { permission: "terminal", pattern: "DROP DATABASE *", action: "deny" }, + { permission: "terminal", pattern: "DROP SCHEMA *", action: "deny" }, + { permission: "terminal", pattern: "TRUNCATE *", action: "deny" }, + { permission: "terminal", pattern: "drop database *", action: "deny" }, + { permission: "terminal", pattern: "drop schema *", action: "deny" }, + { permission: "terminal", pattern: "truncate *", action: "deny" }, ] // Both cases denied - expect(PermissionNext.evaluate("bash", "DROP DATABASE prod", defaultBashRules).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "drop database prod", defaultBashRules).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "DROP DATABASE prod", defaultTerminalRules).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "drop database prod", defaultTerminalRules).action).toBe("deny") // Normal commands still ask - expect(PermissionNext.evaluate("bash", "dbt run", defaultBashRules).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "dbt run", defaultTerminalRules).action).toBe("ask") }) }) @@ -176,28 +176,28 @@ describe("yolo mode: permission evaluation is unchanged", () => { // These tests verify evaluate() behavior is completely unaffected. test("evaluate returns ask for unmatched permissions (yolo doesn't change this)", () => { - expect(PermissionNext.evaluate("bash", "dbt run", []).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "dbt run", []).action).toBe("ask") expect(PermissionNext.evaluate("edit", "src/main.ts", []).action).toBe("ask") expect(PermissionNext.evaluate("write", "output.sql", []).action).toBe("ask") }) test("evaluate returns allow for explicitly allowed permissions", () => { const rules: PermissionNext.Ruleset = [{ permission: "*", pattern: "*", action: "allow" }] - expect(PermissionNext.evaluate("bash", "dbt run", rules).action).toBe("allow") + expect(PermissionNext.evaluate("terminal", "dbt run", rules).action).toBe("allow") expect(PermissionNext.evaluate("edit", "any-file.ts", rules).action).toBe("allow") }) test("disabled() is unaffected by yolo mode (it checks ruleset, not env)", () => { - const rules: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "deny" }] - const disabled = PermissionNext.disabled(["bash", "read", "edit"], rules) - expect(disabled.has("bash")).toBe(true) + const rules: PermissionNext.Ruleset = [{ permission: "terminal", pattern: "*", action: "deny" }] + const disabled = PermissionNext.disabled(["terminal", "read", "edit"], rules) + expect(disabled.has("terminal")).toBe(true) expect(disabled.has("read")).toBe(false) expect(disabled.has("edit")).toBe(false) }) test("fromConfig correctly converts nested permission config", () => { const config = { - bash: { + terminal: { "*": "ask" as const, "dbt *": "allow" as const, "DROP *": "deny" as const, @@ -206,9 +206,9 @@ describe("yolo mode: permission evaluation is unchanged", () => { } const ruleset = PermissionNext.fromConfig(config) - expect(PermissionNext.evaluate("bash", "dbt run", ruleset).action).toBe("allow") - expect(PermissionNext.evaluate("bash", "DROP TABLE x", ruleset).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "ls", ruleset).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "dbt run", ruleset).action).toBe("allow") + expect(PermissionNext.evaluate("terminal", "DROP TABLE x", ruleset).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "ls", ruleset).action).toBe("ask") expect(PermissionNext.evaluate("read", "any-file", ruleset).action).toBe("allow") }) }) @@ -216,38 +216,38 @@ describe("yolo mode: permission evaluation is unchanged", () => { describe("yolo mode: edge cases and adversarial scenarios", () => { test("empty patterns array doesn't crash evaluate", () => { const rules: PermissionNext.Ruleset = [] - expect(PermissionNext.evaluate("bash", "", rules).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "", rules).action).toBe("ask") expect(PermissionNext.evaluate("", "", rules).action).toBe("ask") }) test("permission with special characters in pattern", () => { const rules: PermissionNext.Ruleset = [ - { permission: "bash", pattern: "rm -rf /Users/*/Documents/*", action: "deny" }, + { permission: "terminal", pattern: "rm -rf /Users/*/Documents/*", action: "deny" }, ] - expect(PermissionNext.evaluate("bash", "rm -rf /Users/john/Documents/important", rules).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "rm -rf /tmp/safe", rules).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "rm -rf /Users/john/Documents/important", rules).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "rm -rf /tmp/safe", rules).action).toBe("ask") }) test("multiple rulesets are merged correctly (last wins)", () => { const defaults: PermissionNext.Ruleset = [ - { permission: "bash", pattern: "*", action: "ask" }, - { permission: "bash", pattern: "DROP *", action: "deny" }, + { permission: "terminal", pattern: "*", action: "ask" }, + { permission: "terminal", pattern: "DROP *", action: "deny" }, ] - const userOverride: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }] + const userOverride: PermissionNext.Ruleset = [{ permission: "terminal", pattern: "*", action: "allow" }] // User allow-all comes AFTER default deny, so it wins for everything - expect(PermissionNext.evaluate("bash", "DROP TABLE x", defaults, userOverride).action).toBe("allow") + expect(PermissionNext.evaluate("terminal", "DROP TABLE x", defaults, userOverride).action).toBe("allow") // But if user doesn't override, deny still works - expect(PermissionNext.evaluate("bash", "DROP TABLE x", defaults).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "DROP TABLE x", defaults).action).toBe("deny") }) test("DeniedError contains relevant ruleset info", () => { const rules: PermissionNext.Ruleset = [ - { permission: "bash", pattern: "DROP *", action: "deny" }, + { permission: "terminal", pattern: "DROP *", action: "deny" }, { permission: "read", pattern: "*.env", action: "ask" }, ] try { // Simulate what ask() does when it encounters deny - const result = PermissionNext.evaluate("bash", "DROP TABLE users", rules) + const result = PermissionNext.evaluate("terminal", "DROP TABLE users", rules) if (result.action === "deny") { throw new PermissionNext.DeniedError(rules.filter((r) => r.permission === "bash")) } @@ -288,13 +288,13 @@ describe("yolo mode E2E: permission ask/reply flow", () => { await expect( PermissionNext.ask({ sessionID: SessionID.make("ses_yolo_deny_test"), - permission: "bash", + permission: "terminal", patterns: ["DROP DATABASE production"], metadata: {}, always: [], ruleset: [ - { permission: "bash", pattern: "*", action: "ask" }, - { permission: "bash", pattern: "DROP DATABASE *", action: "deny" }, + { permission: "terminal", pattern: "*", action: "ask" }, + { permission: "terminal", pattern: "DROP DATABASE *", action: "deny" }, ], }), ).rejects.toBeInstanceOf(PermissionNext.DeniedError) @@ -309,11 +309,11 @@ describe("yolo mode E2E: permission ask/reply flow", () => { fn: async () => { const result = await PermissionNext.ask({ sessionID: SessionID.make("ses_yolo_allow_test"), - permission: "bash", + permission: "terminal", patterns: ["dbt run"], metadata: {}, always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "allow" }], + ruleset: [{ permission: "terminal", pattern: "*", action: "allow" }], }) expect(result).toBeUndefined() }, @@ -328,7 +328,7 @@ describe("yolo mode E2E: permission ask/reply flow", () => { const askPromise = PermissionNext.ask({ id: PermissionID.make("per_yolo_e2e"), sessionID: SessionID.make("ses_yolo_ask_test"), - permission: "bash", + permission: "terminal", patterns: ["echo hello"], metadata: {}, always: [], @@ -356,7 +356,7 @@ describe("yolo mode E2E: permission ask/reply flow", () => { const ask1 = PermissionNext.ask({ id: PermissionID.make("per_yolo_multi_1"), sessionID: SessionID.make("ses_yolo_multi_test"), - permission: "bash", + permission: "terminal", patterns: ["dbt run"], metadata: {}, always: [], @@ -401,14 +401,14 @@ describe("yolo mode E2E: permission ask/reply flow", () => { directory: tmp.path, fn: async () => { const rules: PermissionNext.Ruleset = [ - { permission: "bash", pattern: "*", action: "ask" }, - { permission: "bash", pattern: "DROP *", action: "deny" }, + { permission: "terminal", pattern: "*", action: "ask" }, + { permission: "terminal", pattern: "DROP *", action: "deny" }, ] await expect( PermissionNext.ask({ sessionID: SessionID.make("ses_yolo_mixed"), - permission: "bash", + permission: "terminal", patterns: ["DROP TABLE users"], metadata: {}, always: [], @@ -419,7 +419,7 @@ describe("yolo mode E2E: permission ask/reply flow", () => { const askPromise = PermissionNext.ask({ id: PermissionID.make("per_yolo_mixed"), sessionID: SessionID.make("ses_yolo_mixed"), - permission: "bash", + permission: "terminal", patterns: ["dbt run"], metadata: {}, always: [], @@ -441,7 +441,7 @@ describe("yolo mode E2E: permission ask/reply flow", () => { git: true, config: { permission: { - bash: { + terminal: { "*": "ask", "dbt *": "allow", "DROP *": "deny", @@ -456,14 +456,14 @@ describe("yolo mode E2E: permission ask/reply flow", () => { const config = await Config.get() const ruleset = PermissionNext.fromConfig(config.permission ?? {}) - expect(PermissionNext.evaluate("bash", "dbt run", ruleset).action).toBe("allow") - expect(PermissionNext.evaluate("bash", "DROP TABLE x", ruleset).action).toBe("deny") - expect(PermissionNext.evaluate("bash", "git status", ruleset).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "dbt run", ruleset).action).toBe("allow") + expect(PermissionNext.evaluate("terminal", "DROP TABLE x", ruleset).action).toBe("deny") + expect(PermissionNext.evaluate("terminal", "git status", ruleset).action).toBe("ask") await expect( PermissionNext.ask({ sessionID: SessionID.make("ses_yolo_config_test"), - permission: "bash", + permission: "terminal", patterns: ["DROP TABLE users"], metadata: {}, always: [], diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index b046399909..80e5fe56a8 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -81,14 +81,14 @@ const list = () => test("fromConfig - string value becomes wildcard rule", () => { const result = Permission.fromConfig({ bash: "allow" }) - expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }]) + expect(result).toEqual([{ permission: "terminal", pattern: "*", action: "allow" }]) }) test("fromConfig - object value converts to rules array", () => { const result = Permission.fromConfig({ bash: { "*": "allow", rm: "deny" } }) expect(result).toEqual([ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "rm", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "rm", action: "deny" }, ]) }) @@ -99,8 +99,8 @@ test("fromConfig - mixed string and object values", () => { webfetch: "ask", }) expect(result).toEqual([ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "rm", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "rm", action: "deny" }, { permission: "edit", pattern: "*", action: "allow" }, { permission: "webfetch", pattern: "*", action: "ask" }, ]) @@ -139,17 +139,17 @@ test("fromConfig - preserves top-level config key order", () => { const wildcardFirst = Permission.fromConfig({ "*": "deny", bash: "allow" }) const specificFirst = Permission.fromConfig({ bash: "allow", "*": "deny" }) - expect(wildcardFirst.map((r) => r.permission)).toEqual(["*", "bash"]) - expect(specificFirst.map((r) => r.permission)).toEqual(["bash", "*"]) + expect(wildcardFirst.map((r) => r.permission)).toEqual(["*", "terminal"]) + expect(specificFirst.map((r) => r.permission)).toEqual(["terminal", "*"]) - expect(Permission.evaluate("bash", "ls", wildcardFirst).action).toBe("allow") - expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("deny") + expect(Permission.evaluate("terminal", "ls", wildcardFirst).action).toBe("allow") + expect(Permission.evaluate("terminal", "ls", specificFirst).action).toBe("deny") }) test("fromConfig - wildcard acts as fallback when it appears before specifics", () => { const ruleset = Permission.fromConfig({ "*": "ask", bash: "allow" }) expect(Permission.evaluate("edit", "foo.ts", ruleset).action).toBe("ask") - expect(Permission.evaluate("bash", "ls", ruleset).action).toBe("allow") + expect(Permission.evaluate("terminal", "ls", ruleset).action).toBe("allow") }) test("fromConfig - top-level ordering is not sorted by wildcard specificity", () => { @@ -159,19 +159,19 @@ test("fromConfig - top-level ordering is not sorted by wildcard specificity", () edit: "deny", "mcp_*": "allow", }) - expect(ruleset.map((r) => r.permission)).toEqual(["bash", "*", "edit", "mcp_*"]) + expect(ruleset.map((r) => r.permission)).toEqual(["terminal", "*", "edit", "mcp_*"]) }) test("fromConfig - sub-pattern insertion order inside a tool key is preserved", () => { const ruleset = Permission.fromConfig({ bash: { "*": "deny", "git *": "allow" } }) expect(ruleset.map((r) => r.pattern)).toEqual(["*", "git *"]) - expect(Permission.evaluate("bash", "rm foo", ruleset).action).toBe("deny") - expect(Permission.evaluate("bash", "git status", ruleset).action).toBe("allow") + expect(Permission.evaluate("terminal", "rm foo", ruleset).action).toBe("deny") + expect(Permission.evaluate("terminal", "git status", ruleset).action).toBe("allow") }) test("fromConfig - documented fallback-first example", () => { const ruleset = Permission.fromConfig({ "*": "ask", bash: "allow", edit: "deny" }) - expect(Permission.evaluate("bash", "ls", ruleset).action).toBe("allow") + expect(Permission.evaluate("terminal", "ls", ruleset).action).toBe("allow") expect(Permission.evaluate("edit", "foo.ts", ruleset).action).toBe("deny") expect(Permission.evaluate("read", "foo.ts", ruleset).action).toBe("ask") }) @@ -197,53 +197,53 @@ test("evaluate - matches expanded $HOME pattern", () => { test("merge - simple concatenation", () => { const result = Permission.merge( - [{ permission: "bash", pattern: "*", action: "allow" }], - [{ permission: "bash", pattern: "*", action: "deny" }], + [{ permission: "terminal", pattern: "*", action: "allow" }], + [{ permission: "terminal", pattern: "*", action: "deny" }], ) expect(result).toEqual([ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "*", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "*", action: "deny" }, ]) }) test("merge - adds new permission", () => { const result = Permission.merge( - [{ permission: "bash", pattern: "*", action: "allow" }], + [{ permission: "terminal", pattern: "*", action: "allow" }], [{ permission: "edit", pattern: "*", action: "deny" }], ) expect(result).toEqual([ - { permission: "bash", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "*", action: "allow" }, { permission: "edit", pattern: "*", action: "deny" }, ]) }) test("merge - concatenates rules for same permission", () => { const result = Permission.merge( - [{ permission: "bash", pattern: "foo", action: "ask" }], - [{ permission: "bash", pattern: "*", action: "deny" }], + [{ permission: "terminal", pattern: "foo", action: "ask" }], + [{ permission: "terminal", pattern: "*", action: "deny" }], ) expect(result).toEqual([ - { permission: "bash", pattern: "foo", action: "ask" }, - { permission: "bash", pattern: "*", action: "deny" }, + { permission: "terminal", pattern: "foo", action: "ask" }, + { permission: "terminal", pattern: "*", action: "deny" }, ]) }) test("merge - multiple rulesets", () => { const result = Permission.merge( - [{ permission: "bash", pattern: "*", action: "allow" }], - [{ permission: "bash", pattern: "rm", action: "ask" }], + [{ permission: "terminal", pattern: "*", action: "allow" }], + [{ permission: "terminal", pattern: "rm", action: "ask" }], [{ permission: "edit", pattern: "*", action: "allow" }], ) expect(result).toEqual([ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "rm", action: "ask" }, + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "rm", action: "ask" }, { permission: "edit", pattern: "*", action: "allow" }, ]) }) test("merge - empty ruleset does nothing", () => { - const result = Permission.merge([{ permission: "bash", pattern: "*", action: "allow" }], []) - expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }]) + const result = Permission.merge([{ permission: "terminal", pattern: "*", action: "allow" }], []) + expect(result).toEqual([{ permission: "terminal", pattern: "*", action: "allow" }]) }) test("merge - preserves rule order", () => { @@ -263,45 +263,45 @@ test("merge - preserves rule order", () => { test("merge - config permission overrides default ask", () => { const defaults: PermissionV1.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }] - const config: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }] + const config: PermissionV1.Ruleset = [{ permission: "terminal", pattern: "*", action: "allow" }] const merged = Permission.merge(defaults, config) - expect(Permission.evaluate("bash", "ls", merged).action).toBe("allow") + expect(Permission.evaluate("terminal", "ls", merged).action).toBe("allow") expect(Permission.evaluate("edit", "foo.ts", merged).action).toBe("ask") }) test("merge - config ask overrides default allow", () => { - const defaults: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }] - const config: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }] + const defaults: PermissionV1.Ruleset = [{ permission: "terminal", pattern: "*", action: "allow" }] + const config: PermissionV1.Ruleset = [{ permission: "terminal", pattern: "*", action: "ask" }] const merged = Permission.merge(defaults, config) - expect(Permission.evaluate("bash", "ls", merged).action).toBe("ask") + expect(Permission.evaluate("terminal", "ls", merged).action).toBe("ask") }) // evaluate tests test("evaluate - exact pattern match", () => { - const result = Permission.evaluate("bash", "rm", [{ permission: "bash", pattern: "rm", action: "deny" }]) + const result = Permission.evaluate("terminal", "rm", [{ permission: "terminal", pattern: "rm", action: "deny" }]) expect(result.action).toBe("deny") }) test("evaluate - wildcard pattern match", () => { - const result = Permission.evaluate("bash", "rm", [{ permission: "bash", pattern: "*", action: "allow" }]) + const result = Permission.evaluate("terminal", "rm", [{ permission: "terminal", pattern: "*", action: "allow" }]) expect(result.action).toBe("allow") }) test("evaluate - last matching rule wins", () => { - const result = Permission.evaluate("bash", "rm", [ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "rm", action: "deny" }, + const result = Permission.evaluate("terminal", "rm", [ + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "rm", action: "deny" }, ]) expect(result.action).toBe("deny") }) test("evaluate - last matching rule wins (wildcard after specific)", () => { - const result = Permission.evaluate("bash", "rm", [ - { permission: "bash", pattern: "rm", action: "deny" }, - { permission: "bash", pattern: "*", action: "allow" }, + const result = Permission.evaluate("terminal", "rm", [ + { permission: "terminal", pattern: "rm", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, ]) expect(result.action).toBe("allow") }) @@ -329,13 +329,13 @@ test("evaluate - order matters for specificity", () => { test("evaluate - unknown permission returns ask", () => { const result = Permission.evaluate("unknown_tool", "anything", [ - { permission: "bash", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "*", action: "allow" }, ]) expect(result.action).toBe("ask") }) test("evaluate - empty ruleset returns ask", () => { - const result = Permission.evaluate("bash", "rm", []) + const result = Permission.evaluate("terminal", "rm", []) expect(result.action).toBe("ask") }) @@ -345,7 +345,7 @@ test("evaluate - no matching pattern returns ask", () => { }) test("evaluate - empty rules array returns ask", () => { - const result = Permission.evaluate("bash", "rm", []) + const result = Permission.evaluate("terminal", "rm", []) expect(result.action).toBe("ask") }) @@ -368,17 +368,17 @@ test("evaluate - non-matching patterns are skipped", () => { }) test("evaluate - exact match at end wins over earlier wildcard", () => { - const result = Permission.evaluate("bash", "/bin/rm", [ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "/bin/rm", action: "deny" }, + const result = Permission.evaluate("terminal", "/bin/rm", [ + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "/bin/rm", action: "deny" }, ]) expect(result.action).toBe("deny") }) test("evaluate - wildcard at end overrides earlier exact match", () => { - const result = Permission.evaluate("bash", "/bin/rm", [ - { permission: "bash", pattern: "/bin/rm", action: "deny" }, - { permission: "bash", pattern: "*", action: "allow" }, + const result = Permission.evaluate("terminal", "/bin/rm", [ + { permission: "terminal", pattern: "/bin/rm", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, ]) expect(result.action).toBe("allow") }) @@ -386,12 +386,12 @@ test("evaluate - wildcard at end overrides earlier exact match", () => { // wildcard permission tests test("evaluate - wildcard permission matches any permission", () => { - const result = Permission.evaluate("bash", "rm", [{ permission: "*", pattern: "*", action: "deny" }]) + const result = Permission.evaluate("terminal", "rm", [{ permission: "*", pattern: "*", action: "deny" }]) expect(result.action).toBe("deny") }) test("evaluate - wildcard permission with specific pattern", () => { - const result = Permission.evaluate("bash", "rm", [{ permission: "*", pattern: "rm", action: "deny" }]) + const result = Permission.evaluate("terminal", "rm", [{ permission: "*", pattern: "rm", action: "deny" }]) expect(result.action).toBe("deny") }) @@ -403,9 +403,9 @@ test("evaluate - glob permission pattern", () => { }) test("evaluate - specific permission and wildcard permission combined", () => { - const result = Permission.evaluate("bash", "rm", [ + const result = Permission.evaluate("terminal", "rm", [ { permission: "*", pattern: "*", action: "deny" }, - { permission: "bash", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "*", action: "allow" }, ]) expect(result.action).toBe("allow") }) @@ -430,49 +430,49 @@ test("evaluate - multiple matching permission patterns combine rules", () => { test("evaluate - wildcard permission fallback for unknown tool", () => { const result = Permission.evaluate("unknown_tool", "anything", [ { permission: "*", pattern: "*", action: "ask" }, - { permission: "bash", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "*", action: "allow" }, ]) expect(result.action).toBe("ask") }) test("evaluate - later wildcard permission can override earlier specific permission", () => { - const result = Permission.evaluate("bash", "rm", [ - { permission: "bash", pattern: "*", action: "allow" }, + const result = Permission.evaluate("terminal", "rm", [ + { permission: "terminal", pattern: "*", action: "allow" }, { permission: "*", pattern: "*", action: "deny" }, ]) expect(result.action).toBe("deny") }) test("evaluate - merges multiple rulesets", () => { - const config: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }] - const approved: PermissionV1.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }] - const result = Permission.evaluate("bash", "rm", config, approved) + const config: PermissionV1.Ruleset = [{ permission: "terminal", pattern: "*", action: "allow" }] + const approved: PermissionV1.Ruleset = [{ permission: "terminal", pattern: "rm", action: "deny" }] + const result = Permission.evaluate("terminal", "rm", config, approved) expect(result.action).toBe("deny") }) // disabled tests test("disabled - returns empty set when all tools allowed", () => { - const result = Permission.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "allow" }]) + const result = Permission.disabled(["terminal", "edit", "read"], [{ permission: "*", pattern: "*", action: "allow" }]) expect(result.size).toBe(0) }) test("disabled - disables tool when denied", () => { const result = Permission.disabled( - ["bash", "edit", "read"], + ["terminal", "edit", "read"], [ { permission: "*", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "*", action: "deny" }, + { permission: "terminal", pattern: "*", action: "deny" }, ], ) - expect(result.has("bash")).toBe(true) + expect(result.has("terminal")).toBe(true) expect(result.has("edit")).toBe(false) expect(result.has("read")).toBe(false) }) test("disabled - disables edit/write/apply_patch when edit denied", () => { const result = Permission.disabled( - ["edit", "write", "apply_patch", "bash"], + ["edit", "write", "apply_patch", "terminal"], [ { permission: "*", pattern: "*", action: "allow" }, { permission: "edit", pattern: "*", action: "deny" }, @@ -481,77 +481,77 @@ test("disabled - disables edit/write/apply_patch when edit denied", () => { expect(result.has("edit")).toBe(true) expect(result.has("write")).toBe(true) expect(result.has("apply_patch")).toBe(true) - expect(result.has("bash")).toBe(false) + expect(result.has("terminal")).toBe(false) }) test("disabled - does not disable when partially denied", () => { const result = Permission.disabled( - ["bash"], + ["terminal"], [ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "rm *", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "rm *", action: "deny" }, ], ) - expect(result.has("bash")).toBe(false) + expect(result.has("terminal")).toBe(false) }) test("disabled - does not disable when action is ask", () => { - const result = Permission.disabled(["bash", "edit"], [{ permission: "*", pattern: "*", action: "ask" }]) + const result = Permission.disabled(["terminal", "edit"], [{ permission: "*", pattern: "*", action: "ask" }]) expect(result.size).toBe(0) }) test("disabled - does not disable when specific allow after wildcard deny", () => { const result = Permission.disabled( - ["bash"], + ["terminal"], [ - { permission: "bash", pattern: "*", action: "deny" }, - { permission: "bash", pattern: "echo *", action: "allow" }, + { permission: "terminal", pattern: "*", action: "deny" }, + { permission: "terminal", pattern: "echo *", action: "allow" }, ], ) - expect(result.has("bash")).toBe(false) + expect(result.has("terminal")).toBe(false) }) test("disabled - does not disable when wildcard allow after deny", () => { const result = Permission.disabled( - ["bash"], + ["terminal"], [ - { permission: "bash", pattern: "rm *", action: "deny" }, - { permission: "bash", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "rm *", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, ], ) - expect(result.has("bash")).toBe(false) + expect(result.has("terminal")).toBe(false) }) test("disabled - disables multiple tools", () => { const result = Permission.disabled( - ["bash", "edit", "webfetch"], + ["terminal", "edit", "webfetch"], [ - { permission: "bash", pattern: "*", action: "deny" }, + { permission: "terminal", pattern: "*", action: "deny" }, { permission: "edit", pattern: "*", action: "deny" }, { permission: "webfetch", pattern: "*", action: "deny" }, ], ) - expect(result.has("bash")).toBe(true) + expect(result.has("terminal")).toBe(true) expect(result.has("edit")).toBe(true) expect(result.has("webfetch")).toBe(true) }) test("disabled - wildcard permission denies all tools", () => { - const result = Permission.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "deny" }]) - expect(result.has("bash")).toBe(true) + const result = Permission.disabled(["terminal", "edit", "read"], [{ permission: "*", pattern: "*", action: "deny" }]) + expect(result.has("terminal")).toBe(true) expect(result.has("edit")).toBe(true) expect(result.has("read")).toBe(true) }) test("disabled - specific allow overrides wildcard deny", () => { const result = Permission.disabled( - ["bash", "edit", "read"], + ["terminal", "edit", "read"], [ { permission: "*", pattern: "*", action: "deny" }, - { permission: "bash", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "*", action: "allow" }, ], ) - expect(result.has("bash")).toBe(false) + expect(result.has("terminal")).toBe(false) expect(result.has("edit")).toBe(true) expect(result.has("read")).toBe(true) }) @@ -564,11 +564,11 @@ it.instance( Effect.gen(function* () { const result = yield* ask({ sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "allow" }], + ruleset: [{ permission: "terminal", pattern: "*", action: "allow" }], }) expect(result).toBeUndefined() }), @@ -581,7 +581,7 @@ it.instance( Effect.gen(function* () { const ctx = yield* requireInstance const now = Date.now() - const stored: PermissionV1.Rule[] = [{ permission: "bash", pattern: "echo *", action: "allow" }] + const stored: PermissionV1.Rule[] = [{ permission: "terminal", pattern: "echo *", action: "allow" }] LegacyDatabase.use((db) => db.run(sql` INSERT INTO permission (id, project_id, action, resource, time_created, time_updated, data) @@ -591,7 +591,7 @@ it.instance( const result = yield* ask({ sessionID: SessionID.make("session_persisted"), - permission: "bash", + permission: "terminal", patterns: ["echo hello"], metadata: {}, always: [], @@ -610,11 +610,11 @@ it.instance( const err = yield* fail( ask({ sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["rm -rf /"], metadata: {}, always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "deny" }], + ruleset: [{ permission: "terminal", pattern: "*", action: "deny" }], }), ) expect(err).toBeInstanceOf(PermissionV1.DeniedError) @@ -628,11 +628,11 @@ it.instance( Effect.gen(function* () { const fiber = yield* ask({ sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], + ruleset: [{ permission: "terminal", pattern: "*", action: "ask" }], }).pipe(Effect.forkScoped) expect(yield* waitForPending(1)).toHaveLength(1) @@ -648,7 +648,7 @@ it.instance( Effect.gen(function* () { const fiber = yield* ask({ sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: { cmd: "ls" }, always: ["ls"], @@ -663,7 +663,7 @@ it.instance( expect(items).toHaveLength(1) expect(items[0]).toMatchObject({ sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: { cmd: "ls" }, always: ["ls"], @@ -694,7 +694,7 @@ it.instance( const fiber = yield* ask({ sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: { cmd: "ls" }, always: ["ls"], @@ -715,7 +715,7 @@ it.instance( ), ).toMatchObject({ sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], }) @@ -734,7 +734,7 @@ it.instance( const fiber = yield* ask({ id: PermissionV1.ID.make("per_test1"), sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -755,7 +755,7 @@ it.instance( const fiber = yield* ask({ id: PermissionV1.ID.make("per_test2"), sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -779,7 +779,7 @@ it.instance( const fiber = yield* ask({ id: PermissionV1.ID.make("per_test2b"), sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -811,7 +811,7 @@ it.instance( const fiber = yield* ask({ id: PermissionV1.ID.make("per_test3"), sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: ["ls"], @@ -824,7 +824,7 @@ it.instance( const result = yield* ask({ sessionID: SessionID.make("session_test2"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -842,7 +842,7 @@ it.instance( const a = yield* ask({ id: PermissionV1.ID.make("per_test4a"), sessionID: SessionID.make("session_same"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -878,7 +878,7 @@ it.instance( const a = yield* ask({ id: PermissionV1.ID.make("per_test5a"), sessionID: SessionID.make("session_same"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: ["ls"], @@ -888,7 +888,7 @@ it.instance( const b = yield* ask({ id: PermissionV1.ID.make("per_test5b"), sessionID: SessionID.make("session_same"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -912,7 +912,7 @@ it.instance( const a = yield* ask({ id: PermissionV1.ID.make("per_test6a"), sessionID: SessionID.make("session_a"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: ["ls"], @@ -922,7 +922,7 @@ it.instance( const b = yield* ask({ id: PermissionV1.ID.make("per_test6b"), sessionID: SessionID.make("session_b"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -955,7 +955,7 @@ it.instance( const fiber = yield* ask({ id: PermissionV1.ID.make("per_test7"), sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -1006,7 +1006,7 @@ it.live("permission requests stay isolated by directory", () => ask({ id: PermissionV1.ID.make("per_dir_a"), sessionID: SessionID.make("session_dir_a"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -1021,7 +1021,7 @@ it.live("permission requests stay isolated by directory", () => ask({ id: PermissionV1.ID.make("per_dir_b"), sessionID: SessionID.make("session_dir_b"), - permission: "bash", + permission: "terminal", patterns: ["pwd"], metadata: {}, always: [], @@ -1055,7 +1055,7 @@ it.instance( const fiber = yield* ask({ id: PermissionV1.ID.make("per_dispose"), sessionID: SessionID.make("session_dispose"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -1082,7 +1082,7 @@ it.instance( const fiber = yield* ask({ id: PermissionV1.ID.make("per_reload"), sessionID: SessionID.make("session_reload"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], @@ -1120,13 +1120,13 @@ it.instance( const err = yield* fail( ask({ sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["echo hello", "rm -rf /"], metadata: {}, always: [], ruleset: [ - { permission: "bash", pattern: "*", action: "allow" }, - { permission: "bash", pattern: "rm *", action: "deny" }, + { permission: "terminal", pattern: "*", action: "allow" }, + { permission: "terminal", pattern: "rm *", action: "deny" }, ], }), ) @@ -1141,11 +1141,11 @@ it.instance( Effect.gen(function* () { const result = yield* ask({ sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["echo hello", "ls -la", "pwd"], metadata: {}, always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "allow" }], + ruleset: [{ permission: "terminal", pattern: "*", action: "allow" }], }) expect(result).toBeUndefined() }), @@ -1159,13 +1159,13 @@ it.instance( const err = yield* fail( ask({ sessionID: SessionID.make("session_test"), - permission: "bash", + permission: "terminal", patterns: ["echo hello", "rm -rf /"], metadata: {}, always: [], ruleset: [ - { permission: "bash", pattern: "echo *", action: "ask" }, - { permission: "bash", pattern: "rm *", action: "deny" }, + { permission: "terminal", pattern: "echo *", action: "ask" }, + { permission: "terminal", pattern: "rm *", action: "deny" }, ], }), ) @@ -1186,11 +1186,11 @@ it.instance( const fiber = yield* ask({ id: PermissionV1.ID.make("per_reload"), sessionID: SessionID.make("session_reload"), - permission: "bash", + permission: "terminal", patterns: ["ls"], metadata: {}, always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], + ruleset: [{ permission: "terminal", pattern: "*", action: "ask" }], }).pipe(Effect.forkScoped) const pending = yield* waitForPending(1) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 001aab94f3..220ca20edb 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1580,7 +1580,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { { type: "tool-call", toolCallId: "test", - toolName: "bash", + toolName: "terminal", input: { command: "echo hello" }, }, ], @@ -1631,7 +1631,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { { type: "tool-call", toolCallId: "test", - toolName: "bash", + toolName: "terminal", input: { command: "echo hello" }, }, ]) @@ -1707,7 +1707,7 @@ describe("ProviderTransform.message - Devstral detection", () => { const msgs = [ { role: "assistant", - content: [{ type: "tool-call", toolCallId: "call-id!!", toolName: "bash", input: { command: "pwd" } }], + content: [{ type: "tool-call", toolCallId: "call-id!!", toolName: "terminal", input: { command: "pwd" } }], }, { role: "tool", @@ -1715,7 +1715,7 @@ describe("ProviderTransform.message - Devstral detection", () => { { type: "tool-result", toolCallId: "call-id!!", - toolName: "bash", + toolName: "terminal", output: { type: "text", value: "ok" }, }, ], @@ -2086,7 +2086,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => role: "assistant", content: [ { type: "text", text: "" }, - { type: "tool-call", toolCallId: "123", toolName: "bash", input: { command: "ls" } }, + { type: "tool-call", toolCallId: "123", toolName: "terminal", input: { command: "ls" } }, ], }, ] as any[] @@ -2098,7 +2098,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => expect(result[0].content[0]).toEqual({ type: "tool-call", toolCallId: "123", - toolName: "bash", + toolName: "terminal", input: { command: "ls" }, }) }) diff --git a/packages/opencode/test/release-validation/question-937-codex.test.ts b/packages/opencode/test/release-validation/question-937-codex.test.ts index 19c31354c1..376a323648 100644 --- a/packages/opencode/test/release-validation/question-937-codex.test.ts +++ b/packages/opencode/test/release-validation/question-937-codex.test.ts @@ -353,7 +353,7 @@ describe("release validation PR #937 source-level env plumbing", () => { }) test("bash tool strips ALTIMATE_NON_INTERACTIVE from child process env but keeps auto-answer env untouched", async () => { - const source = await Bun.file(new URL("../../src/tool/bash.ts", import.meta.url)).text() + const source = await Bun.file(new URL("../../src/tool/terminal.ts", import.meta.url)).text() expect(source).toContain('const mergedEnv: Record = { ...process.env, ...shellEnv.env }') expect(source).toContain('delete mergedEnv["ALTIMATE_NON_INTERACTIVE"]') diff --git a/packages/opencode/test/release-validation/question-937.test.ts b/packages/opencode/test/release-validation/question-937.test.ts index e0f0ec5b00..86ceaf6a92 100644 --- a/packages/opencode/test/release-validation/question-937.test.ts +++ b/packages/opencode/test/release-validation/question-937.test.ts @@ -2,16 +2,16 @@ // // Touched source: // packages/opencode/src/tool/question.ts (isNonInteractive / autoAnswer + mode-aware output) -// packages/opencode/src/tool/bash.ts (strip ALTIMATE_NON_INTERACTIVE from child env) +// packages/opencode/src/tool/terminal.ts (strip ALTIMATE_NON_INTERACTIVE from child env) // packages/opencode/src/cli/cmd/run.ts (set ALTIMATE_NON_INTERACTIVE; null-safe stdin read) // // Style/imports follow packages/opencode/test/tool/question.test.ts and -// packages/opencode/test/tool/bash.test.ts. +// packages/opencode/test/tool/terminal.test.ts. import { describe, expect, test, spyOn, beforeAll, beforeEach, afterEach } from "bun:test" import { QuestionTool } from "../../src/tool/question" import * as QuestionModule from "../../src/question" -import { BashTool } from "../../src/tool/bash" +import { TerminalTool } from "../../src/tool/terminal" import { Instance } from "../../src/project/instance" import { SessionID, MessageID } from "../../src/session/schema" // altimate_change start — upstream v1.17.9 made tools Effect definitions (no static .init()); @@ -219,7 +219,7 @@ describe("tool.question non-interactive autoAnswer mapping", () => { // --------------------------------------------------------------------------- // Gap #7 — bash tool strips ALTIMATE_NON_INTERACTIVE from child env. -// Mirrors packages/opencode/test/tool/bash.test.ts (Instance.provide + execute). +// Mirrors packages/opencode/test/tool/terminal.test.ts (Instance.provide + execute). // --------------------------------------------------------------------------- describe("tool.bash strips ALTIMATE_NON_INTERACTIVE from child env", () => { let prev: string | undefined @@ -239,7 +239,7 @@ describe("tool.bash strips ALTIMATE_NON_INTERACTIVE from child env", () => { await Instance.provide({ directory: projectRoot, fn: async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) // printenv exits non-zero when the var is unset, so `|| echo MISSING` // proves the delete reached spawn's env. const result = await bash.execute( diff --git a/packages/opencode/test/release-validation/session-transcript-941.test.ts b/packages/opencode/test/release-validation/session-transcript-941.test.ts index 614764e0d1..56bbf06261 100644 --- a/packages/opencode/test/release-validation/session-transcript-941.test.ts +++ b/packages/opencode/test/release-validation/session-transcript-941.test.ts @@ -125,7 +125,7 @@ async function addAssistantMessageWithTool(sessionID: SessionID, parentID: Messa messageID: id, type: "tool", callID: "call_completed", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { a: 1 }, diff --git a/packages/opencode/test/session/compaction-mask.test.ts b/packages/opencode/test/session/compaction-mask.test.ts index feddf5a598..868d648b82 100644 --- a/packages/opencode/test/session/compaction-mask.test.ts +++ b/packages/opencode/test/session/compaction-mask.test.ts @@ -48,7 +48,7 @@ function makePendingPart(overrides?: { tool?: string }): MessageV2.ToolPart { describe("SessionCompaction.createObservationMask", () => { test("includes tool name, args, line count, byte size, and fingerprint for completed part", () => { const part = makeCompletedPart({ - tool: "bash", + tool: "terminal", input: { command: "git status" }, output: "On branch main\nnothing to commit, working tree clean\n", }) @@ -76,7 +76,7 @@ describe("SessionCompaction.createObservationMask", () => { }) test("shows empty args for pending status (falls through to {} path)", () => { - const part = makePendingPart({ tool: "bash" }) + const part = makePendingPart({ tool: "terminal" }) const mask = SessionCompaction.createObservationMask(part) // Pending status → output is "" (since only completed reads output) @@ -118,7 +118,7 @@ describe("SessionCompaction.createObservationMask", () => { circular.self = circular const part = makeCompletedPart({ - tool: "bash", + tool: "terminal", input: circular, output: "result", }) @@ -157,7 +157,7 @@ describe("SessionCompaction.createObservationMask", () => { test("fingerprint is capped at 80 characters", () => { const longFirstLine = "z".repeat(200) - const part = makeCompletedPart({ tool: "bash", output: longFirstLine }) + const part = makeCompletedPart({ tool: "terminal", output: longFirstLine }) const mask = SessionCompaction.createObservationMask(part) // The fingerprint should contain the first 80 chars, not all 200 diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 145c525c79..a6a931689f 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -802,7 +802,7 @@ describe("session.compaction.prune", () => { sessionID: info.id, type: "tool", callID: crypto.randomUUID(), - tool: "bash", + tool: "terminal", state: { status: "completed", input: {}, diff --git a/packages/opencode/test/session/llm-native.test.ts b/packages/opencode/test/session/llm-native.test.ts index 54fd097ec1..57cd3a555c 100644 --- a/packages/opencode/test/session/llm-native.test.ts +++ b/packages/opencode/test/session/llm-native.test.ts @@ -170,7 +170,7 @@ describe("session.llm-native.request", () => { { type: "tool-call", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", input: { command: "ls" }, providerOptions: { openai: { itemId: "item-1" } }, }, @@ -182,7 +182,7 @@ describe("session.llm-native.request", () => { { type: "tool-result", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", output: { type: "text", value: "ok" }, providerOptions: { openai: { outputId: "output-1" } }, }, @@ -243,7 +243,7 @@ describe("session.llm-native.request", () => { expect(request.toolChoice).toMatchObject({ type: "required" }) expect(request.tools).toMatchObject([ { - name: "bash", + name: "terminal", description: "Run a shell command", inputSchema: { type: "object", @@ -270,7 +270,7 @@ describe("session.llm-native.request", () => { { type: "tool-call", id: "call-1", - name: "bash", + name: "terminal", input: { command: "ls" }, providerMetadata: { openai: { itemId: "item-1" } }, }, @@ -282,7 +282,7 @@ describe("session.llm-native.request", () => { { type: "tool-result", id: "call-1", - name: "bash", + name: "terminal", result: { type: "text", value: "ok" }, providerMetadata: { openai: { outputId: "output-1" } }, }, diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 148529ad64..41861a8ea6 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -32,54 +32,54 @@ describe("session.llm.toolNamesFromMessages", () => { { role: "assistant", content: [ - { type: "tool-call", toolCallId: "call-1", toolName: "bash" }, + { type: "tool-call", toolCallId: "call-1", toolName: "terminal" }, { type: "tool-call", toolCallId: "call-2", toolName: "read" }, ], }, ] as ModelMessage[] - expect(LLM.toolNamesFromMessages(messages)).toEqual(new Set(["bash", "read"])) + expect(LLM.toolNamesFromMessages(messages)).toEqual(new Set(["terminal", "read"])) }) test("deduplicates tool names across messages", () => { const messages = [ { role: "assistant", - content: [{ type: "tool-call", toolCallId: "call-1", toolName: "bash" }], + content: [{ type: "tool-call", toolCallId: "call-1", toolName: "terminal" }], }, { role: "assistant", - content: [{ type: "tool-call", toolCallId: "call-2", toolName: "bash" }], + content: [{ type: "tool-call", toolCallId: "call-2", toolName: "terminal" }], }, ] as ModelMessage[] - expect(LLM.toolNamesFromMessages(messages)).toEqual(new Set(["bash"])) + expect(LLM.toolNamesFromMessages(messages)).toEqual(new Set(["terminal"])) }) test("extracts tool names from tool-result blocks", () => { const messages = [ { role: "tool", - content: [{ type: "tool-result", toolCallId: "call-1", toolName: "bash" }], + content: [{ type: "tool-result", toolCallId: "call-1", toolName: "terminal" }], }, ] as ModelMessage[] - expect(LLM.toolNamesFromMessages(messages)).toEqual(new Set(["bash"])) + expect(LLM.toolNamesFromMessages(messages)).toEqual(new Set(["terminal"])) }) test("extracts from both tool-call and tool-result blocks", () => { const messages = [ { role: "assistant", - content: [{ type: "tool-call", toolCallId: "call-1", toolName: "bash" }], + content: [{ type: "tool-call", toolCallId: "call-1", toolName: "terminal" }], }, { role: "tool", - content: [{ type: "tool-result", toolCallId: "call-1", toolName: "bash" }], + content: [{ type: "tool-result", toolCallId: "call-1", toolName: "terminal" }], }, { role: "tool", content: [{ type: "tool-result", toolCallId: "call-2", toolName: "read" }], }, ] as ModelMessage[] - expect(LLM.toolNamesFromMessages(messages)).toEqual(new Set(["bash", "read"])) + expect(LLM.toolNamesFromMessages(messages)).toEqual(new Set(["terminal", "read"])) }) }) diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 52ff59767e..a4c5b5ce74 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -354,7 +354,7 @@ describe("session.message-v2.toModelMessage", () => { ...basePart(assistantID, "a2"), type: "tool", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { cmd: "ls" }, @@ -390,7 +390,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-call", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", input: { cmd: "ls" }, providerExecuted: undefined, providerOptions: { openai: { tool: "meta" } }, @@ -403,7 +403,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-result", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", output: { type: "content", value: [ @@ -637,7 +637,7 @@ describe("session.message-v2.toModelMessage", () => { ...basePart(assistantID, "a3"), type: "tool", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { cmd: "ls" }, @@ -665,7 +665,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-call", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", input: { cmd: "ls" }, providerExecuted: undefined, }, @@ -677,7 +677,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-result", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", output: { type: "text", value: "ok" }, }, ], @@ -707,7 +707,7 @@ describe("session.message-v2.toModelMessage", () => { ...basePart(assistantID, "a1"), type: "tool", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { cmd: "ls" }, @@ -732,7 +732,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-call", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", input: { cmd: "ls" }, providerExecuted: undefined, }, @@ -744,7 +744,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-result", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", output: { type: "text", value: "[Old tool result content cleared]" }, }, ], @@ -774,7 +774,7 @@ describe("session.message-v2.toModelMessage", () => { ...basePart(assistantID, "a1"), type: "tool", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { cmd: "ls" }, @@ -799,7 +799,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-call", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", input: { cmd: "ls" }, providerExecuted: undefined, }, @@ -811,7 +811,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-result", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", output: { type: "text", value: "abcd\n[Tool output truncated for compaction: omitted 6 chars]", @@ -844,7 +844,7 @@ describe("session.message-v2.toModelMessage", () => { ...basePart(assistantID, "a1"), type: "tool", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "error", input: { cmd: "ls" }, @@ -869,7 +869,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-call", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", input: { cmd: "ls" }, providerExecuted: undefined, providerOptions: { openai: { tool: "meta" } }, @@ -882,7 +882,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-result", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", output: { type: "error-text", value: "nope" }, providerOptions: { openai: { tool: "meta" } }, }, @@ -923,7 +923,7 @@ describe("session.message-v2.toModelMessage", () => { ...basePart(assistantID, "a1"), type: "tool", callID: "call-1", - tool: "bash", + tool: "terminal", state: { status: "error", input: { command: "for i in {1..20}; do print -- $RANDOM; sleep 1; done" }, @@ -947,7 +947,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-call", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", input: { command: "for i in {1..20}; do print -- $RANDOM; sleep 1; done" }, providerExecuted: undefined, }, @@ -959,7 +959,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-result", toolCallId: "call-1", - toolName: "bash", + toolName: "terminal", output: { type: "text", value: output }, }, ], @@ -1193,7 +1193,7 @@ describe("session.message-v2.toModelMessage", () => { ...basePart(assistantID, "a1"), type: "tool", callID: "call-pending", - tool: "bash", + tool: "terminal", state: { status: "pending", input: { cmd: "ls" }, @@ -1228,7 +1228,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-call", toolCallId: "call-pending", - toolName: "bash", + toolName: "terminal", input: { cmd: "ls" }, providerExecuted: undefined, }, @@ -1247,7 +1247,7 @@ describe("session.message-v2.toModelMessage", () => { { type: "tool-result", toolCallId: "call-pending", - toolName: "bash", + toolName: "terminal", output: { type: "error-text", value: "[Tool execution was interrupted]" }, }, { diff --git a/packages/opencode/test/session/observation-mask.test.ts b/packages/opencode/test/session/observation-mask.test.ts index 2c47a0fa9f..52bca84dd1 100644 --- a/packages/opencode/test/session/observation-mask.test.ts +++ b/packages/opencode/test/session/observation-mask.test.ts @@ -15,7 +15,7 @@ function makeToolPart(overrides: { messageID: "msg_1" as any, type: "tool" as const, callID: "call_1", - tool: overrides.tool ?? "bash", + tool: overrides.tool ?? "terminal", } if (status === "completed") { @@ -82,7 +82,7 @@ describe("SessionCompaction.createObservationMask", () => { test("handles empty output gracefully", () => { const part = makeToolPart({ - tool: "bash", + tool: "terminal", input: { command: "echo hello" }, output: "", }) @@ -139,10 +139,10 @@ describe("SessionCompaction.createObservationMask", () => { }) test("uses empty input for pending tool parts", () => { - const part = makeToolPart({ tool: "bash", status: "pending" }) + const part = makeToolPart({ tool: "terminal", status: "pending" }) const mask = SessionCompaction.createObservationMask(part) - expect(mask).toContain("bash()") + expect(mask).toContain("terminal()") expect(mask).toContain("returned 1 lines") expect(mask).toContain("0 B") }) @@ -162,13 +162,13 @@ describe("SessionCompaction.createObservationMask", () => { test("uses input from error tool parts", () => { const part = makeToolPart({ - tool: "bash", + tool: "terminal", input: { command: "rm -rf /" }, status: "error", }) const mask = SessionCompaction.createObservationMask(part) - expect(mask).toContain("bash(command:") + expect(mask).toContain("terminal(command:") expect(mask).toContain("0 B") }) }) diff --git a/packages/opencode/test/session/processor.test.ts b/packages/opencode/test/session/processor.test.ts index d6d460b992..94e75a2dfa 100644 --- a/packages/opencode/test/session/processor.test.ts +++ b/packages/opencode/test/session/processor.test.ts @@ -136,7 +136,7 @@ describe("tool call telemetry", () => { let counter = 0 let prevTool: string | null = null - for (const tool of ["read", "edit", "bash"]) { + for (const tool of ["read", "edit", "terminal"]) { const event = simulateToolResult({ tool, isMcpTool: false, @@ -161,7 +161,7 @@ describe("tool call telemetry", () => { let counter = 0 let prevTool: string | null = null - for (const tool of ["read", "edit", "bash"]) { + for (const tool of ["read", "edit", "terminal"]) { const event = simulateToolResult({ tool, isMcpTool: false, @@ -184,7 +184,7 @@ describe("tool call telemetry", () => { test("error tool calls include error_message truncated to 500 chars", () => { const longError = "x".repeat(1000) const event = simulateToolError({ - tool: "bash", + tool: "terminal", isMcpTool: false, sessionID: "sess-1", messageID: "msg-1", @@ -201,7 +201,7 @@ describe("tool call telemetry", () => { test("error tool calls with short messages preserve full message", () => { const event = simulateToolError({ - tool: "bash", + tool: "terminal", isMcpTool: false, sessionID: "sess-1", messageID: "msg-1", @@ -250,7 +250,7 @@ describe("tool call telemetry", () => { // --------------------------------------------------------------------------- describe("tool categorization", () => { test("file tools are categorized correctly", () => { - for (const tool of ["read", "write", "edit", "glob", "grep", "bash"]) { + for (const tool of ["read", "write", "edit", "glob", "grep", "terminal"]) { expect(Telemetry.categorizeToolName(tool, "standard")).toBe("file") } }) @@ -443,7 +443,7 @@ describe("doom loop detection telemetry", () => { type: "doom_loop_detected", timestamp: Date.now(), session_id: "sess-1", - tool_name: "bash", + tool_name: "terminal", repeat_count: DOOM_LOOP_THRESHOLD, } diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 92fe3f8136..7e037d3b3e 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -339,7 +339,7 @@ const waitForBusy = (sessionID: SessionID, duration: Duration.Input = "2 seconds duration, ) -const hasBash = Effect.sync(() => Bun.which("bash") !== null) +const hasTerminal = Effect.sync(() => Bun.which("bash") !== null) const deferredAsPromise = (deferred: Deferred.Deferred): PromiseLike => ({ then: (onfulfilled, onrejected) => { @@ -896,7 +896,7 @@ noLLMServer.instance("prompt tools replace previous prompt tool rules", () => const reloaded = yield* sessions.get(session.id) expect(reloaded.permission).toEqual([{ permission: "read", pattern: "*", action: "allow" }]) - expect(Permission.evaluate("bash", "anything", reloaded.permission ?? []).action).toBe("ask") + expect(Permission.evaluate("terminal", "anything", reloaded.permission ?? []).action).toBe("ask") }), ) @@ -1482,7 +1482,7 @@ unixNoLLMServer( () => withSh(() => Effect.gen(function* () { - if (!(yield* hasBash)) return + if (!(yield* hasTerminal)) return const { prompt, chat } = yield* boot() const result = yield* prompt.shell({ @@ -1681,7 +1681,7 @@ it.instance( () => withSh(() => Effect.gen(function* () { - if (!(yield* hasBash)) return + if (!(yield* hasTerminal)) return const { llm } = yield* useServerConfig((url) => ({ ...providerCfg(url), shell: "bash", @@ -1804,7 +1804,7 @@ it.instance.todo( parts: [{ type: "text", text: "run bash" }], }) - yield* llm.tool("bash", { + yield* llm.tool("terminal", { command: 'i=0; while [ "$i" -lt 4000 ]; do printf "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx %05d\\n" "$i"; i=$((i + 1)); done; printf truncation-ready; sleep 30', description: "Print many lines", diff --git a/packages/opencode/test/session/regression.test.ts b/packages/opencode/test/session/regression.test.ts index 85fd87bb56..2115db12b0 100644 --- a/packages/opencode/test/session/regression.test.ts +++ b/packages/opencode/test/session/regression.test.ts @@ -39,7 +39,7 @@ describe("toolCallCount accumulation", () => { { type: "tool", tool: "read" }, { type: "text", text: "hello" }, { type: "tool", tool: "edit" }, - { type: "tool", tool: "bash" }, + { type: "tool", tool: "terminal" }, ] toolCallCount += step1Parts.filter((p) => p.type === "tool").length expect(toolCallCount).toBe(3) diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index 4e71a6a36c..7e1992b426 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -73,7 +73,7 @@ const tool = Effect.fn("test.tool")(function* (sessionID: SessionID, messageID: messageID, sessionID, type: "tool" as const, - tool: "bash", + tool: "terminal", callID: "call-1", state: { status: "completed" as const, diff --git a/packages/opencode/test/session/schema-decoding.test.ts b/packages/opencode/test/session/schema-decoding.test.ts index 55d43ba37d..2c4550a0f5 100644 --- a/packages/opencode/test/session/schema-decoding.test.ts +++ b/packages/opencode/test/session/schema-decoding.test.ts @@ -165,7 +165,7 @@ describe("Session input schemas", () => { parentID: sessionID, title: "child", metadata: { source: "test" }, - permission: [{ action: "ask" as const, pattern: "*", permission: "bash" }], + permission: [{ action: "ask" as const, pattern: "*", permission: "terminal" }], workspaceID, } expect(decode(populated)).toEqual(populated) diff --git a/packages/opencode/test/skill/release-v0.6.0-adversarial.test.ts b/packages/opencode/test/skill/release-v0.6.0-adversarial.test.ts index 9f29dc2f37..32018370f8 100644 --- a/packages/opencode/test/skill/release-v0.6.0-adversarial.test.ts +++ b/packages/opencode/test/skill/release-v0.6.0-adversarial.test.ts @@ -193,7 +193,7 @@ describe("toolNamesFromMessages — validation guards", () => { } test("accepts standard tool names", () => { - expect(LLM.toolNamesFromMessages(msg("bash"))).toEqual(new Set(["bash"])) + expect(LLM.toolNamesFromMessages(msg("bash"))).toEqual(new Set(["terminal"])) expect(LLM.toolNamesFromMessages(msg("data_diff"))).toEqual(new Set(["data_diff"])) expect(LLM.toolNamesFromMessages(msg("sql-execute"))).toEqual(new Set(["sql-execute"])) expect(LLM.toolNamesFromMessages(msg("Tool123"))).toEqual(new Set(["Tool123"])) @@ -267,7 +267,7 @@ describe("toolNamesFromMessages — validation guards", () => { ...msg("bash"), ...msg("read"), ] - expect(LLM.toolNamesFromMessages(messages)).toEqual(new Set(["bash", "read"])) + expect(LLM.toolNamesFromMessages(messages)).toEqual(new Set(["terminal", "read"])) }) test("returns empty set for messages with text-only content", () => { diff --git a/packages/opencode/test/skill/release-v0.8.0-adversarial.test.ts b/packages/opencode/test/skill/release-v0.8.0-adversarial.test.ts index a964423553..ede7b4c8c7 100644 --- a/packages/opencode/test/skill/release-v0.8.0-adversarial.test.ts +++ b/packages/opencode/test/skill/release-v0.8.0-adversarial.test.ts @@ -16,7 +16,7 @@ import { Agent } from "../../src/agent/agent" import { PermissionNext } from "../../src/permission/next" function bashAction(agent: Agent.Info, command: string) { - return PermissionNext.evaluate("bash", command, agent.permission).action + return PermissionNext.evaluate("terminal", command, agent.permission).action } // The reviewer agent advertises read-only; these pin that NO bash command is @@ -35,7 +35,7 @@ test("reviewer agent: bash requires approval (base) — never auto-allowed", asy // #978: "ask" instead of "deny" so `gh pr view ` is possible with // explicit user approval. The P0 was auto-run via a bypassable allowlist; // "ask" never auto-runs. - expect(PermissionNext.evaluate("bash", "*", reviewer!.permission).action).toBe("ask") + expect(PermissionNext.evaluate("terminal", "*", reviewer!.permission).action).toBe("ask") }, }) }) diff --git a/packages/opencode/test/skill/release-v0.8.6-adversarial.test.ts b/packages/opencode/test/skill/release-v0.8.6-adversarial.test.ts index 6902260b18..7fb06f71c3 100644 --- a/packages/opencode/test/skill/release-v0.8.6-adversarial.test.ts +++ b/packages/opencode/test/skill/release-v0.8.6-adversarial.test.ts @@ -77,7 +77,7 @@ function toolEvent(sessionID: string, callID: string) { part: { sessionID, type: "tool", - tool: "bash", + tool: "terminal", callID, state: { status: "completed", input: { command: "ls" }, output: "ok", time: { start: now - 5, end: now } }, }, diff --git a/packages/opencode/test/telemetry/telemetry.test.ts b/packages/opencode/test/telemetry/telemetry.test.ts index a9490888e8..5bb3ec5eca 100644 --- a/packages/opencode/test/telemetry/telemetry.test.ts +++ b/packages/opencode/test/telemetry/telemetry.test.ts @@ -44,7 +44,7 @@ describe("telemetry.categorizeToolName", () => { }) test("returns 'file' for file operation tools", () => { - for (const tool of ["read", "write", "edit", "glob", "grep", "bash"]) { + for (const tool of ["read", "write", "edit", "glob", "grep", "terminal"]) { expect(Telemetry.categorizeToolName(tool, "standard")).toBe("file") } }) @@ -2265,7 +2265,7 @@ describe("telemetry.tool_chain_outcome", () => { type: "tool_chain_outcome", timestamp: Date.now(), session_id: "s1", - chain: JSON.stringify(["read", "edit", "bash"]), + chain: JSON.stringify(["read", "edit", "terminal"]), chain_length: 3, had_errors: false, error_recovery_count: 0, diff --git a/packages/opencode/test/tool/parameters.test.ts b/packages/opencode/test/tool/parameters.test.ts index 0eedcdd588..8b3b9a4319 100644 --- a/packages/opencode/test/tool/parameters.test.ts +++ b/packages/opencode/test/tool/parameters.test.ts @@ -74,7 +74,7 @@ const toJsonSchema = ToolJsonSchema.fromSchema describe("tool parameters", () => { describe("JSON Schema (wire shape)", () => { test("apply_patch", () => expect(toJsonSchema(ApplyPatch)).toMatchSnapshot()) - test("bash", () => expect(toJsonSchema(Shell)).toMatchSnapshot()) + test("terminal", () => expect(toJsonSchema(Shell)).toMatchSnapshot()) test("edit", () => expect(toJsonSchema(Edit)).toMatchSnapshot()) test("glob", () => expect(toToolJsonSchema(GlobDef)).toMatchSnapshot()) test("grep", () => expect(toJsonSchema(Grep)).toMatchSnapshot()) diff --git a/packages/opencode/test/tool/retrieval.test.ts b/packages/opencode/test/tool/retrieval.test.ts index 09e7835cca..27744948ad 100644 --- a/packages/opencode/test/tool/retrieval.test.ts +++ b/packages/opencode/test/tool/retrieval.test.ts @@ -11,7 +11,7 @@ const TOOLS = [ describe("Retrieval.select", () => { test("always keeps core tools", () => { const sel = Retrieval.select("run the dbt models", TOOLS, { topk: 12 }) - expect(sel.has("bash")).toBe(true) + expect(sel.has("terminal")).toBe(true) expect(sel.has("read")).toBe(true) }) diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index 2ea1d14589..bafe8d6b39 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -42,7 +42,7 @@ const initShell = Effect.fn("ShellToolTest.init")(function* () { return yield* info.init() }) -const initBash = initShell +const initTerminal = initShell const run = Effect.fn("ShellToolTest.run")(function* ( args: Tool.InferParameters, @@ -196,7 +196,7 @@ describe("tool.shell", () => { yield* runIn( tmp, Effect.gen(function* () { - const bash = yield* initBash() + const bash = yield* initTerminal() const fallback = Shell.name(Shell.acceptable("fish")) expect(fallback).not.toBe("fish") expect(bash.description).toContain(fallback) diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 66eafda0c6..db0d01b6fd 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -458,7 +458,7 @@ describe("tool.task", () => { action: "deny", }, { - permission: "bash", + permission: "terminal", pattern: "*", action: "deny", }, @@ -482,7 +482,7 @@ describe("tool.task", () => { }, }, experimental: { - primary_tools: ["bash", "read"], + primary_tools: ["terminal", "read"], }, }, }, diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/terminal.test.ts similarity index 93% rename from packages/opencode/test/tool/bash.test.ts rename to packages/opencode/test/tool/terminal.test.ts index 982ed0a412..216018efb3 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/terminal.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import fs from "fs/promises" import os from "os" import path from "path" -import { BashTool } from "../../src/tool/bash" +import { TerminalTool } from "../../src/tool/terminal" import { Instance } from "../../src/project/instance" import { Filesystem } from "../../src/util/filesystem" import { tmpdir, provideTestInstance } from "../fixture/fixture" @@ -27,7 +27,7 @@ function toEffectValue(value: any): Effect.Effect { if (value && typeof value.then === "function") return Effect.promise(() => value) return Effect.void } -async function initToolWithTruncate(tool: typeof BashTool) { +async function initToolWithTruncate(tool: typeof TerminalTool) { const def = await truncateRuntime.runPromise( tool.pipe(Effect.flatMap((info) => info.init())) as Effect.Effect, ) @@ -63,7 +63,7 @@ const projectRoot = path.join(__dirname, "../..") // Load the instance through the Effect InstanceStore (DB-safe — the legacy Instance.provide path // re-runs project migrations through src/storage/db.ts which conflicts with the core Database // migrator on the shared test DB → "table project already exists"), then restore the legacy -// Instance ALS so BashTool (a legacy tool reading Instance.directory) resolves. +// Instance ALS so TerminalTool (a legacy tool reading Instance.directory) resolves. async function provideInstance(directory: string, fn: () => Promise): Promise { return provideTestInstance({ directory, fn: (instanceCtx) => Instance.restore(instanceCtx, fn) }) } @@ -71,7 +71,7 @@ async function provideInstance(directory: string, fn: () => Promise): Prom describe("tool.bash", () => { test("basic", async () => { await provideInstance(projectRoot, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const result = await bash.execute( { command: "echo 'test'", @@ -89,7 +89,7 @@ describe("tool.bash permissions", () => { test("asks for bash permission with correct pattern", async () => { await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const requests: Array> = [] const testCtx = { ...ctx, @@ -113,7 +113,7 @@ describe("tool.bash permissions", () => { test("asks for bash permission with multiple commands", async () => { await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const requests: Array> = [] const testCtx = { ...ctx, @@ -138,7 +138,7 @@ describe("tool.bash permissions", () => { test("asks for external_directory permission when cd to parent", async () => { await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const requests: Array> = [] const testCtx = { ...ctx, @@ -161,7 +161,7 @@ describe("tool.bash permissions", () => { test("asks for external_directory permission when workdir is outside project", async () => { await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const requests: Array> = [] const testCtx = { ...ctx, @@ -191,7 +191,7 @@ describe("tool.bash permissions", () => { }) await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const requests: Array> = [] const testCtx = { ...ctx, @@ -218,7 +218,7 @@ describe("tool.bash permissions", () => { test("does not ask for external_directory permission when rm inside project", async () => { await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const requests: Array> = [] const testCtx = { ...ctx, @@ -245,7 +245,7 @@ describe("tool.bash permissions", () => { test("includes always patterns for auto-approval", async () => { await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const requests: Array> = [] const testCtx = { ...ctx, @@ -269,7 +269,7 @@ describe("tool.bash permissions", () => { test("does not ask for bash permission when command is cd only", async () => { await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const requests: Array> = [] const testCtx = { ...ctx, @@ -292,7 +292,7 @@ describe("tool.bash permissions", () => { test("matches redirects in permission pattern", async () => { await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const requests: Array> = [] const testCtx = { ...ctx, @@ -310,7 +310,7 @@ describe("tool.bash permissions", () => { test("always pattern has space before wildcard to not include different commands", async () => { await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const requests: Array> = [] const testCtx = { ...ctx, @@ -330,7 +330,7 @@ describe("tool.bash permissions", () => { describe("tool.bash truncation", () => { test("truncates output exceeding line limit", async () => { await provideInstance(projectRoot, async () => { - const bash = await initToolWithTruncate(BashTool) + const bash = await initToolWithTruncate(TerminalTool) const lineCount = Truncate.MAX_LINES + 500 const result = await bash.execute( { @@ -347,7 +347,7 @@ describe("tool.bash truncation", () => { test("truncates output exceeding byte limit", async () => { await provideInstance(projectRoot, async () => { - const bash = await initToolWithTruncate(BashTool) + const bash = await initToolWithTruncate(TerminalTool) const byteCount = Truncate.MAX_BYTES + 10000 const result = await bash.execute( { @@ -364,7 +364,7 @@ describe("tool.bash truncation", () => { test("does not truncate small output", async () => { await provideInstance(projectRoot, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const result = await bash.execute( { command: "echo hello", @@ -380,7 +380,7 @@ describe("tool.bash truncation", () => { test("full output is saved to file when truncated", async () => { await provideInstance(projectRoot, async () => { - const bash = await initToolWithTruncate(BashTool) + const bash = await initToolWithTruncate(TerminalTool) const lineCount = Truncate.MAX_LINES + 100 const result = await bash.execute( { @@ -416,7 +416,7 @@ describe("tool.bash PATH injection", () => { ) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const result = await bash.execute( { command: "my-custom-tool", @@ -440,7 +440,7 @@ describe("tool.bash PATH injection", () => { ) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const result = await bash.execute( { command: "my-altimate-tool", @@ -459,7 +459,7 @@ describe("tool.bash PATH injection", () => { await fs.mkdir(toolsDir, { recursive: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const result = await bash.execute( { command: 'echo "$PATH"', @@ -478,7 +478,7 @@ describe("tool.bash PATH injection", () => { test("ALTIMATE_BIN_DIR is on PATH with higher priority than system dirs", async () => { await using tmp = await tmpdir({ git: true }) await provideInstance(tmp.path, async () => { - const bash = await initTool(BashTool) + const bash = await initTool(TerminalTool) const result = await bash.execute( { command: 'echo "$PATH"', diff --git a/packages/opencode/test/tui-journeys/README.md b/packages/opencode/test/tui-journeys/README.md index ebaedd841f..de352ea676 100644 --- a/packages/opencode/test/tui-journeys/README.md +++ b/packages/opencode/test/tui-journeys/README.md @@ -41,7 +41,7 @@ state are never read. Use `withJourney("name", async (tui, ctx) => { ... })` from `harness.ts`. - Script model output with `await tui.ctx.llm.text("...")` or - `await tui.ctx.llm.tool("bash", { ... })`. + `await tui.ctx.llm.tool("terminal", { ... })`. - Drive the terminal with `tui.type("text")` and `tui.send("C-p")`. - Assert visible state with `tui.snapshot()` or OSC/hyperlink state with `tui.snapshotAnsi()`. diff --git a/packages/opencode/test/tui-journeys/journeys.test.ts b/packages/opencode/test/tui-journeys/journeys.test.ts index 2eb23609d8..b4a0e8b350 100644 --- a/packages/opencode/test/tui-journeys/journeys.test.ts +++ b/packages/opencode/test/tui-journeys/journeys.test.ts @@ -218,7 +218,7 @@ maybeDescribe("real-binary TUI journeys", () => { await booted(tui) await selectAgent(tui, "reviewer") - await tui.ctx.llm.tool("bash", { + await tui.ctx.llm.tool("terminal", { command: "gh pr view 1", description: "View pull request details", }) diff --git a/packages/opencode/test/upstream/adversarial/upi-tool-api.test.ts b/packages/opencode/test/upstream/adversarial/upi-tool-api.test.ts index a3e23f42b3..49719a1d72 100644 --- a/packages/opencode/test/upstream/adversarial/upi-tool-api.test.ts +++ b/packages/opencode/test/upstream/adversarial/upi-tool-api.test.ts @@ -150,7 +150,7 @@ describe("UPI-12 and UPI-13 tool context, decode, and central truncation", () => async execute(args: { name: string }, ctx) { await new Promise((resolve) => setTimeout(resolve, 0)) ctx.metadata({ title: args.name, metadata: { stage: "metadata" } }) - await ctx.ask({ permission: "bash", patterns: ["echo *"], always: ["echo *"], metadata: {} }) + await ctx.ask({ permission: "terminal", patterns: ["echo *"], always: ["echo *"], metadata: {} }) return { title: "done", metadata: { ok: true }, output: "ok" } }, }), diff --git a/packages/opencode/test/upstream/altimate-features.test.ts b/packages/opencode/test/upstream/altimate-features.test.ts index eb67869dc4..4df9c32abc 100644 --- a/packages/opencode/test/upstream/altimate-features.test.ts +++ b/packages/opencode/test/upstream/altimate-features.test.ts @@ -61,13 +61,13 @@ function evaluate(permission: string, pattern: string, ruleset: Rule[]): Rule { // in agent.ts (after user config) so they always win. We mirror those rules // here so we can verify the WIRING stays intact even when the agent-level // import chain is broken by an in-progress bridge merge. -const SAFETY_BASH: Rule[] = [ - { permission: "bash", pattern: "DROP DATABASE *", action: "deny" }, - { permission: "bash", pattern: "DROP SCHEMA *", action: "deny" }, - { permission: "bash", pattern: "TRUNCATE *", action: "deny" }, - { permission: "bash", pattern: "drop database *", action: "deny" }, - { permission: "bash", pattern: "drop schema *", action: "deny" }, - { permission: "bash", pattern: "truncate *", action: "deny" }, +const SAFETY_TERMINAL: Rule[] = [ + { permission: "terminal", pattern: "DROP DATABASE *", action: "deny" }, + { permission: "terminal", pattern: "DROP SCHEMA *", action: "deny" }, + { permission: "terminal", pattern: "TRUNCATE *", action: "deny" }, + { permission: "terminal", pattern: "drop database *", action: "deny" }, + { permission: "terminal", pattern: "drop schema *", action: "deny" }, + { permission: "terminal", pattern: "truncate *", action: "deny" }, ] const SAFETY_SQL: Rule[] = [ { permission: "sql_execute_write", pattern: "DROP DATABASE *", action: "deny" }, @@ -284,7 +284,7 @@ describe("altimate features: agent safety denial wiring", () => { }) test("Wildcard-driven evaluate denies 'DROP DATABASE foo' on the safety-bash ruleset", () => { - const result = evaluate("bash", "DROP DATABASE foo", SAFETY_BASH) + const result = evaluate("terminal", "DROP DATABASE foo", SAFETY_TERMINAL) expect(result.action).toBe("deny") }) @@ -295,11 +295,11 @@ describe("altimate features: agent safety denial wiring", () => { test("Wildcard-driven evaluate: even after a wildcard 'allow', the safety deny wins via last-match", () => { // Mimic what agent.ts does: user config first, then safety denials appended last. - const userAllow: Rule[] = [{ permission: "bash", pattern: "*", action: "allow" }] - const merged = [...userAllow, ...SAFETY_BASH] - expect(evaluate("bash", "DROP DATABASE prod", merged).action).toBe("deny") + const userAllow: Rule[] = [{ permission: "terminal", pattern: "*", action: "allow" }] + const merged = [...userAllow, ...SAFETY_TERMINAL] + expect(evaluate("terminal", "DROP DATABASE prod", merged).action).toBe("deny") // Non-destructive bash still allowed - expect(evaluate("bash", "ls -la", merged).action).toBe("allow") + expect(evaluate("terminal", "ls -la", merged).action).toBe("allow") }) }) diff --git a/packages/opencode/test/upstream/v140-merge-fuzz.test.ts b/packages/opencode/test/upstream/v140-merge-fuzz.test.ts index d9ddf7c668..7274aad2a6 100644 --- a/packages/opencode/test/upstream/v140-merge-fuzz.test.ts +++ b/packages/opencode/test/upstream/v140-merge-fuzz.test.ts @@ -201,7 +201,7 @@ describe("v1.4.0 failure injection — synthetic provider errors flow through di test("aborted with non-string reason normalized to 'non_string_reason' (not [object Object])", () => { const out = Telemetry.deriveAgentOutcomeReason({ outcome: "aborted", - lastToolName: "bash", + lastToolName: "terminal", lastMessageError: null, abortReason: "non_string_reason", lastErrorClass: "", diff --git a/packages/opencode/test/upstream/v140-permission-deadlock.test.ts b/packages/opencode/test/upstream/v140-permission-deadlock.test.ts index 6baf8ed045..b6e5c8be1e 100644 --- a/packages/opencode/test/upstream/v140-permission-deadlock.test.ts +++ b/packages/opencode/test/upstream/v140-permission-deadlock.test.ts @@ -43,15 +43,15 @@ test("PDF deadlock: bash ask + 'Allow once' reply via route handler logic resolv await Instance.provide({ directory: tmp.path, fn: async () => { - // Step 1: tool/bash.ts calls ctx.ask, which hits PermissionNext.ask + // Step 1: tool/terminal.ts calls ctx.ask, which hits PermissionNext.ask // (this is exactly what session/processor.ts does at line 193/221). const askPromise = PermissionNext.ask({ sessionID: SessionID.make("session_pdf_test"), - permission: "bash", + permission: "terminal", patterns: ["which duckdb"], metadata: { command: "which duckdb" }, always: ["which *"], - ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], + ruleset: [{ permission: "terminal", pattern: "*", action: "ask" }], }) // Step 2: request lands in PermissionNext's pending Map and emits @@ -89,11 +89,11 @@ test("PDF deadlock: 'Allow always' reply also resolves the ask Promise", async ( fn: async () => { const askPromise = PermissionNext.ask({ sessionID: SessionID.make("session_pdf_test_always"), - permission: "bash", + permission: "terminal", patterns: ["pwd && ls -la"], metadata: { command: "pwd && ls -la" }, always: ["pwd *", "ls *"], - ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], + ruleset: [{ permission: "terminal", pattern: "*", action: "ask" }], }) const list = await waitForPending(1) @@ -112,11 +112,11 @@ test("PDF deadlock: 'Allow always' reply also resolves the ask Promise", async ( // Subsequent ask for matching pattern auto-resolves (allow rule was added). const followup = PermissionNext.ask({ sessionID: SessionID.make("session_pdf_test_always"), - permission: "bash", + permission: "terminal", patterns: ["pwd"], metadata: { command: "pwd" }, always: ["pwd *"], - ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], + ruleset: [{ permission: "terminal", pattern: "*", action: "ask" }], }) // Allow rule lets this resolve immediately without showing a new prompt. @@ -137,11 +137,11 @@ test("PDF deadlock: 'Reject' reply rejects the ask Promise (not deadlock)", asyn fn: async () => { const askPromise = PermissionNext.ask({ sessionID: SessionID.make("session_pdf_test_reject"), - permission: "bash", + permission: "terminal", patterns: ["rm -rf /"], metadata: { command: "rm -rf /" }, always: [], - ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], + ruleset: [{ permission: "terminal", pattern: "*", action: "ask" }], }) const list = await waitForPending(1) diff --git a/packages/opencode/test/v2/session-message-updater.test.ts b/packages/opencode/test/v2/session-message-updater.test.ts index 668a353f67..53495797fb 100644 --- a/packages/opencode/test/v2/session-message-updater.test.ts +++ b/packages/opencode/test/v2/session-message-updater.test.ts @@ -150,7 +150,7 @@ test.skip("tool completion stores completed timestamp", () => { assistantMessageID, timestamp: DateTime.makeUnsafe(2), callID, - name: "bash", + name: "terminal", }, } satisfies SessionEvent.Event), ) @@ -164,7 +164,7 @@ test.skip("tool completion stores completed timestamp", () => { assistantMessageID, timestamp: DateTime.makeUnsafe(3), callID, - tool: "bash", + tool: "terminal", input: { command: "pwd" }, provider: { executed: true, metadata: { fake: { source: "provider" } } }, }, diff --git a/packages/tui/src/feature-plugins/home/tips-view.tsx b/packages/tui/src/feature-plugins/home/tips-view.tsx index db7de0bad9..76a506fd57 100644 --- a/packages/tui/src/feature-plugins/home/tips-view.tsx +++ b/packages/tui/src/feature-plugins/home/tips-view.tsx @@ -286,7 +286,7 @@ const TIPS: Tip[] = [ "Use {highlight}instructions{/highlight} in config to load additional rules files", "Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative)", "Configure {highlight}steps{/highlight} to limit agentic iterations per request", - 'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools', + 'Set {highlight}"tools": {"terminal": false}{/highlight} to disable specific tools', 'Set {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server', "Override global tool settings per agent configuration", 'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions', diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 0521a6248c..afc2e93746 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1807,7 +1807,7 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess return ( - + @@ -2649,8 +2649,7 @@ function numberValue(value: unknown) { return typeof value === "number" && Number.isFinite(value) ? value : undefined } -const toolDisplays = new Set([ - "bash", +const toolDisplays = new Set(["terminal", "glob", "read", "grep", diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 92894620ac..a3ddf75fc0 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -304,7 +304,7 @@ test("settles pending tools when a live failure arrives", async () => { assistantMessageID: "msg_explicit_assistant_9", timestamp: 2, callID: "call-1", - name: "bash", + name: "terminal", }, }) emitEvent(events, { @@ -315,7 +315,7 @@ test("settles pending tools when a live failure arrives", async () => { timestamp: 2, assistantMessageID: "msg_explicit_assistant_9", callID: "call-1", - tool: "bash", + tool: "terminal", input: {}, provider: { executed: false, metadata: { fake: { call: true } } }, }, diff --git a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx index 00e4ef5c98..545ff3e644 100644 --- a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +++ b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx @@ -225,7 +225,7 @@ async function renderFrame(component: () => JSX.Element, options: { width: numbe describe("TUI inline tool wrapping", () => { test("falls back for unknown tool names", () => { - expect(toolDisplay("bash")).toBe("bash") + expect(toolDisplay("terminal")).toBe("terminal") expect(toolDisplay("plugin_tool")).toBe("generic") }) diff --git a/packages/tui/test/util/transcript.test.ts b/packages/tui/test/util/transcript.test.ts index 02d6ef0bb9..5a480e5066 100644 --- a/packages/tui/test/util/transcript.test.ts +++ b/packages/tui/test/util/transcript.test.ts @@ -167,7 +167,7 @@ describe("transcript", () => { messageID: "msg_123", type: "tool", callID: "call_1", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { command: "ls" }, @@ -192,7 +192,7 @@ describe("transcript", () => { messageID: "msg_123", type: "tool", callID: "call_1", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { command: "echo '```hello```'" }, @@ -217,7 +217,7 @@ describe("transcript", () => { messageID: "msg_123", type: "tool", callID: "call_1", - tool: "bash", + tool: "terminal", state: { status: "completed", input: { command: "ls" }, @@ -240,7 +240,7 @@ describe("transcript", () => { messageID: "msg_123", type: "tool", callID: "call_1", - tool: "bash", + tool: "terminal", state: { status: "error", input: { command: "invalid" },