mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-06 16:31:50 +00:00
edges
This commit is contained in:
parent
cffb8eb1e3
commit
26d77add77
4 changed files with 137 additions and 4 deletions
|
|
@ -22,6 +22,7 @@ import { Auth } from "@/auth"
|
|||
import { Installation } from "@/installation"
|
||||
import { InstallationVersion } from "@/installation/version"
|
||||
import { EffectBridge } from "@/effect"
|
||||
import { ShellToolID } from "@/tool/shell/id"
|
||||
import * as Option from "effect/Option"
|
||||
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||
|
||||
|
|
@ -453,7 +454,12 @@ function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission"
|
|||
Object.keys(input.tools),
|
||||
Permission.merge(input.agent.permission, input.permission ?? []),
|
||||
)
|
||||
return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
|
||||
return Record.filter(input.tools, (_, k) => {
|
||||
const userTool = input.user.tools?.[k]
|
||||
if (userTool !== undefined) return userTool !== false && !disabled.has(k)
|
||||
if (k === ShellToolID.id && input.user.tools?.[ShellToolID.legacy] === false) return false
|
||||
return !disabled.has(k)
|
||||
})
|
||||
}
|
||||
|
||||
// Check if messages contain any tool-call content
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
|||
import { fileURLToPath } from "url"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { ShellToolID } from "./shell/id"
|
||||
import { ShellKind, ShellToolID } from "./shell/id"
|
||||
|
||||
import { BashArity } from "@/permission/arity"
|
||||
import * as Truncate from "./truncate"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||
import { ShellArity } from "./shell/arity"
|
||||
|
||||
const MAX_METADATA_LENGTH = 30_000
|
||||
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||
|
|
@ -551,6 +551,7 @@ export const ShellTool = Tool.define(
|
|||
patterns: new Set<string>(),
|
||||
always: new Set<string>(),
|
||||
}
|
||||
const shellKind = ShellKind.from(Shell.name(shell))
|
||||
|
||||
for (const node of commands(root)) {
|
||||
const command = parts(node)
|
||||
|
|
@ -569,7 +570,7 @@ export const ShellTool = Tool.define(
|
|||
|
||||
if (tokens.length && (!cmd || !CWD.has(cmd))) {
|
||||
scan.patterns.add(source(node))
|
||||
scan.always.add(BashArity.prefix(tokens).join(" ") + " *")
|
||||
scan.always.add(ShellArity.prefix(tokens, shellKind).join(" ") + " *")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -561,6 +561,100 @@ describe("session.llm.stream", () => {
|
|||
})
|
||||
})
|
||||
|
||||
test("disables shell when user message uses legacy bash override", async () => {
|
||||
const server = state.server
|
||||
if (!server) {
|
||||
throw new Error("Server not initialized")
|
||||
}
|
||||
|
||||
const providerID = "alibaba"
|
||||
const modelID = "qwen-plus"
|
||||
const fixture = await loadFixture(providerID, modelID)
|
||||
const model = fixture.model
|
||||
|
||||
const request = waitRequest(
|
||||
"/chat/completions",
|
||||
new Response(createChatStream("Hello"), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
}),
|
||||
)
|
||||
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
enabled_providers: [providerID],
|
||||
provider: {
|
||||
[providerID]: {
|
||||
options: {
|
||||
apiKey: "test-key",
|
||||
baseURL: `${server.url.origin}/v1`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-legacy-bash-tools")
|
||||
const agent = {
|
||||
name: "test",
|
||||
mode: "primary",
|
||||
options: {},
|
||||
permission: [],
|
||||
} satisfies Agent.Info
|
||||
|
||||
const user = {
|
||||
id: MessageID.make("user-legacy-bash-tools"),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(providerID), modelID: resolved.id },
|
||||
tools: { bash: false },
|
||||
} satisfies MessageV2.User
|
||||
|
||||
await drain({
|
||||
user,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
system: ["You are a helpful assistant."],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
tools: {
|
||||
shell: tool({
|
||||
description: "Run a shell command",
|
||||
inputSchema: z.object({ command: z.string() }),
|
||||
execute: async () => ({ output: "" }),
|
||||
}),
|
||||
read: tool({
|
||||
description: "Read a file",
|
||||
inputSchema: z.object({ filePath: z.string() }),
|
||||
execute: async () => ({ output: "" }),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const capture = await request
|
||||
const names =
|
||||
(capture.body.tools as Array<{ function?: { name?: string } }> | undefined)?.flatMap((item) =>
|
||||
item.function?.name ? [item.function.name] : [],
|
||||
) ?? []
|
||||
|
||||
expect(names).not.toContain("shell")
|
||||
expect(names).toContain("read")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("sends responses API payload for OpenAI models", async () => {
|
||||
const server = state.server
|
||||
if (!server) {
|
||||
|
|
|
|||
|
|
@ -237,6 +237,38 @@ describe("tool.shell permissions", () => {
|
|||
)
|
||||
}
|
||||
|
||||
for (const item of ps) {
|
||||
test(
|
||||
`uses PowerShell cmdlet prefixes for always-allow prompts [${item.label}]`,
|
||||
withShell(item, async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const bash = await initShell()
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
await expect(
|
||||
Effect.runPromise(
|
||||
bash.execute(
|
||||
{
|
||||
command: "Remove-Item -Recurse tmp",
|
||||
description: "Remove a temp directory",
|
||||
},
|
||||
capture(requests, err),
|
||||
),
|
||||
),
|
||||
).rejects.toThrow(err.message)
|
||||
const bashReq = requests.find((r) => r.permission === expectedPermission)
|
||||
expect(bashReq).toBeDefined()
|
||||
expect(bashReq!.always).toContain("Remove-Item *")
|
||||
expect(bashReq!.always).not.toContain("Remove-Item -Recurse *")
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
each("asks for external_directory permission for wildcard external paths", async () => {
|
||||
await Instance.provide({
|
||||
directory: projectRoot,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue