fix(interactive-bash): prohibit tmux kill-server
This commit is contained in:
@@ -11,6 +11,10 @@ export const BLOCKED_TMUX_SUBCOMMANDS = [
|
||||
"pipep",
|
||||
]
|
||||
|
||||
export const PROHIBITED_TMUX_SUBCOMMANDS = [
|
||||
"kill-server",
|
||||
]
|
||||
|
||||
export const INTERACTIVE_BASH_DESCRIPTION = `WARNING: This is TMUX ONLY. Pass tmux subcommands directly (without 'tmux' prefix).
|
||||
|
||||
Examples: new-session -d -s omo-dev, send-keys -t omo-dev "vim" Enter
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||
import { interactive_bash } from "./tools"
|
||||
|
||||
const mockContext = {
|
||||
sessionID: "test-session",
|
||||
messageID: "test-message",
|
||||
agent: "test-agent",
|
||||
directory: "/project",
|
||||
worktree: "/project",
|
||||
abort: new AbortController().signal,
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
} satisfies ToolContext
|
||||
|
||||
describe("interactive_bash", () => {
|
||||
test("#given kill-server command #when executed #then returns a strong prohibition without running tmux", async () => {
|
||||
// given
|
||||
const args = { tmux_command: "kill-server" }
|
||||
|
||||
// when
|
||||
const output = await interactive_bash.execute(args, mockContext)
|
||||
|
||||
// then
|
||||
expect(output).toContain("Error: 'kill-server' is prohibited in interactive_bash.")
|
||||
expect(output).toContain("NEVER EVER run tmux kill-server from interactive_bash.")
|
||||
expect(output).toContain("Do not retry kill-server with Bash or any other tool.")
|
||||
})
|
||||
|
||||
test("#given kill-server after tmux global options #when executed #then still prohibits it", async () => {
|
||||
// given
|
||||
const args = { tmux_command: "-L omo-socket kill-server" }
|
||||
|
||||
// when
|
||||
const output = await interactive_bash.execute(args, mockContext)
|
||||
|
||||
// then
|
||||
expect(output).toContain("Error: 'kill-server' is prohibited in interactive_bash.")
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,16 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
||||
import { isCmuxCompatEnvironment } from "../../shared/tmux/cmux-detect"
|
||||
import { BLOCKED_TMUX_SUBCOMMANDS, DEFAULT_TIMEOUT_MS, INTERACTIVE_BASH_DESCRIPTION } from "./constants"
|
||||
import {
|
||||
BLOCKED_TMUX_SUBCOMMANDS,
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
INTERACTIVE_BASH_DESCRIPTION,
|
||||
PROHIBITED_TMUX_SUBCOMMANDS,
|
||||
} from "./constants"
|
||||
import { getCachedTmuxPath } from "./tmux-path-resolver"
|
||||
|
||||
const GLOBAL_TMUX_OPTIONS_WITH_ARGS = new Set(["-L", "-S", "-f", "-c", "-T"])
|
||||
|
||||
function resolveTmuxExecutable(tmuxPath: string): string[] {
|
||||
if (!isCmuxCompatEnvironment()) {
|
||||
return [tmuxPath]
|
||||
@@ -59,6 +66,84 @@ export function tokenizeCommand(cmd: string): string[] {
|
||||
return tokens
|
||||
}
|
||||
|
||||
function findSubcommandIndex(parts: string[]): number {
|
||||
let index = 0
|
||||
while (index < parts.length) {
|
||||
const part = parts[index] ?? ""
|
||||
|
||||
if (part === "--") {
|
||||
return index + 1 < parts.length ? index + 1 : -1
|
||||
}
|
||||
|
||||
if (GLOBAL_TMUX_OPTIONS_WITH_ARGS.has(part)) {
|
||||
index += 2
|
||||
continue
|
||||
}
|
||||
|
||||
if (part.startsWith("-")) {
|
||||
index++
|
||||
continue
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
function getTargetSessionName(parts: string[]): string {
|
||||
const sessionIdx = parts.findIndex(p => p === "-t" || p.startsWith("-t"))
|
||||
if (sessionIdx === -1) {
|
||||
return "omo-session"
|
||||
}
|
||||
|
||||
const sessionToken = parts[sessionIdx] ?? ""
|
||||
const nextToken = parts[sessionIdx + 1]
|
||||
if (sessionToken === "-t" && nextToken) {
|
||||
return nextToken
|
||||
}
|
||||
|
||||
if (sessionToken.startsWith("-t")) {
|
||||
return sessionToken.slice(2)
|
||||
}
|
||||
|
||||
return "omo-session"
|
||||
}
|
||||
|
||||
function buildBlockedTmuxCommandMessage(command: string, parts: string[]): string {
|
||||
const sessionName = getTargetSessionName(parts)
|
||||
|
||||
return `Error: '${command}' is blocked in interactive_bash.
|
||||
|
||||
**USE BASH TOOL INSTEAD:**
|
||||
|
||||
\`\`\`bash
|
||||
# Capture terminal output
|
||||
tmux capture-pane -p -t ${sessionName}
|
||||
|
||||
# Or capture with history (last 1000 lines)
|
||||
tmux capture-pane -p -t ${sessionName} -S -1000
|
||||
\`\`\`
|
||||
|
||||
The Bash tool can execute these commands directly. Do NOT retry with interactive_bash.`
|
||||
}
|
||||
|
||||
function buildProhibitedTmuxCommandMessage(command: string): string {
|
||||
return `Error: '${command}' is prohibited in interactive_bash.
|
||||
|
||||
NEVER EVER run tmux kill-server from interactive_bash.
|
||||
|
||||
It terminates the entire tmux server, destroying every tmux session and pane that the user, Codex, or other agents may be using.
|
||||
|
||||
Use scoped cleanup only:
|
||||
|
||||
\`\`\`bash
|
||||
tmux kill-session -t <session-name>
|
||||
\`\`\`
|
||||
|
||||
If you created an omo-* session, kill only that exact session. Do not retry kill-server with Bash or any other tool.`
|
||||
}
|
||||
|
||||
export const interactive_bash: ToolDefinition = tool({
|
||||
description: INTERACTIVE_BASH_DESCRIPTION,
|
||||
args: {
|
||||
@@ -74,31 +159,16 @@ export const interactive_bash: ToolDefinition = tool({
|
||||
return "Error: Empty tmux command"
|
||||
}
|
||||
|
||||
const subcommand = parts[0].toLowerCase()
|
||||
const subcommandIndex = findSubcommandIndex(parts)
|
||||
const rawSubcommand = subcommandIndex === -1 ? "" : parts[subcommandIndex]
|
||||
const subcommand = rawSubcommand.toLowerCase()
|
||||
|
||||
if (PROHIBITED_TMUX_SUBCOMMANDS.includes(subcommand)) {
|
||||
return buildProhibitedTmuxCommandMessage(rawSubcommand)
|
||||
}
|
||||
|
||||
if (BLOCKED_TMUX_SUBCOMMANDS.includes(subcommand)) {
|
||||
const sessionIdx = parts.findIndex(p => p === "-t" || p.startsWith("-t"))
|
||||
let sessionName = "omo-session"
|
||||
if (sessionIdx !== -1) {
|
||||
if (parts[sessionIdx] === "-t" && parts[sessionIdx + 1]) {
|
||||
sessionName = parts[sessionIdx + 1]
|
||||
} else if (parts[sessionIdx].startsWith("-t")) {
|
||||
sessionName = parts[sessionIdx].slice(2)
|
||||
}
|
||||
}
|
||||
|
||||
return `Error: '${parts[0]}' is blocked in interactive_bash.
|
||||
|
||||
**USE BASH TOOL INSTEAD:**
|
||||
|
||||
\`\`\`bash
|
||||
# Capture terminal output
|
||||
tmux capture-pane -p -t ${sessionName}
|
||||
|
||||
# Or capture with history (last 1000 lines)
|
||||
tmux capture-pane -p -t ${sessionName} -S -1000
|
||||
\`\`\`
|
||||
|
||||
The Bash tool can execute these commands directly. Do NOT retry with interactive_bash.`
|
||||
return buildBlockedTmuxCommandMessage(rawSubcommand, parts)
|
||||
}
|
||||
|
||||
const proc = spawnWithWindowsHide([...resolveTmuxExecutable(tmuxPath), ...parts], {
|
||||
|
||||
Reference in New Issue
Block a user