fix(interactive-bash): prohibit tmux kill-server
This commit is contained in:
@@ -11,6 +11,10 @@ export const BLOCKED_TMUX_SUBCOMMANDS = [
|
|||||||
"pipep",
|
"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).
|
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
|
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 { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||||
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
||||||
import { isCmuxCompatEnvironment } from "../../shared/tmux/cmux-detect"
|
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"
|
import { getCachedTmuxPath } from "./tmux-path-resolver"
|
||||||
|
|
||||||
|
const GLOBAL_TMUX_OPTIONS_WITH_ARGS = new Set(["-L", "-S", "-f", "-c", "-T"])
|
||||||
|
|
||||||
function resolveTmuxExecutable(tmuxPath: string): string[] {
|
function resolveTmuxExecutable(tmuxPath: string): string[] {
|
||||||
if (!isCmuxCompatEnvironment()) {
|
if (!isCmuxCompatEnvironment()) {
|
||||||
return [tmuxPath]
|
return [tmuxPath]
|
||||||
@@ -59,6 +66,84 @@ export function tokenizeCommand(cmd: string): string[] {
|
|||||||
return tokens
|
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({
|
export const interactive_bash: ToolDefinition = tool({
|
||||||
description: INTERACTIVE_BASH_DESCRIPTION,
|
description: INTERACTIVE_BASH_DESCRIPTION,
|
||||||
args: {
|
args: {
|
||||||
@@ -74,31 +159,16 @@ export const interactive_bash: ToolDefinition = tool({
|
|||||||
return "Error: Empty tmux command"
|
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)) {
|
if (BLOCKED_TMUX_SUBCOMMANDS.includes(subcommand)) {
|
||||||
const sessionIdx = parts.findIndex(p => p === "-t" || p.startsWith("-t"))
|
return buildBlockedTmuxCommandMessage(rawSubcommand, parts)
|
||||||
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.`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const proc = spawnWithWindowsHide([...resolveTmuxExecutable(tmuxPath), ...parts], {
|
const proc = spawnWithWindowsHide([...resolveTmuxExecutable(tmuxPath), ...parts], {
|
||||||
|
|||||||
Reference in New Issue
Block a user