mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-19 08:44:22 +01:00
Add agent-level permissions with whitelist/blacklist support (#1862)
This commit is contained in:
50
.github/workflows/duplicate-issues.yml
vendored
50
.github/workflows/duplicate-issues.yml
vendored
@@ -0,0 +1,50 @@
|
|||||||
|
name: Duplicate Issue Detection
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-duplicates:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Install opencode
|
||||||
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
- name: Check for duplicate issues
|
||||||
|
env:
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
opencode run --agent github -m anthropic/claude-sonnet-4-20250514 "A new issue has been created: '${{ github.event.issue.title }}'
|
||||||
|
|
||||||
|
Issue body:
|
||||||
|
${{ github.event.issue.body }}
|
||||||
|
|
||||||
|
Please search through existing issues in this repository to find any potential duplicates of this new issue. Consider:
|
||||||
|
1. Similar titles or descriptions
|
||||||
|
2. Same error messages or symptoms
|
||||||
|
3. Related functionality or components
|
||||||
|
4. Similar feature requests
|
||||||
|
|
||||||
|
If you find any potential duplicates, please comment on the new issue with:
|
||||||
|
- A brief explanation of why it might be a duplicate
|
||||||
|
- Links to the potentially duplicate issues
|
||||||
|
- A suggestion to check those issues first
|
||||||
|
|
||||||
|
Use this format for the comment:
|
||||||
|
'👋 This issue might be a duplicate of existing issues. Please check:
|
||||||
|
- #[issue_number]: [brief description of similarity]
|
||||||
|
|
||||||
|
If none of these address your specific case, please let us know how this issue differs.'
|
||||||
|
|
||||||
|
If no clear duplicates are found, do not comment."
|
||||||
|
|||||||
49
.github/workflows/guidelines-check.yml
vendored
49
.github/workflows/guidelines-check.yml
vendored
@@ -0,0 +1,49 @@
|
|||||||
|
name: Guidelines Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-guidelines:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Install opencode
|
||||||
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
- name: Check PR guidelines compliance
|
||||||
|
env:
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
opencode run --agent github -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
|
||||||
|
|
||||||
|
PR description:
|
||||||
|
${{ github.event.pull_request.body }}
|
||||||
|
|
||||||
|
Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository.
|
||||||
|
|
||||||
|
For each violation you find, create a file comment using the gh CLI. Use this exact format for each violation:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
gh pr review ${{ github.event.pull_request.number }} --comment-body 'This violates the AGENTS.md guideline: [specific rule]. Consider: [suggestion]' --file 'path/to/file.ts' --line [line_number]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
When possible, also submit code change suggestions using:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
gh pr review ${{ github.event.pull_request.number }} --comment-body 'Suggested fix for AGENTS.md guideline violation:' --file 'path/to/file.ts' --line [line_number] --body '```suggestion
|
||||||
|
[corrected code here]
|
||||||
|
```'
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."
|
||||||
|
|||||||
13
.opencode/agent/github.md
Normal file
13
.opencode/agent/github.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
permission:
|
||||||
|
bash:
|
||||||
|
"*": "deny"
|
||||||
|
"gh*": "allow"
|
||||||
|
mode: subagent
|
||||||
|
---
|
||||||
|
|
||||||
|
You are running in github actions, typically to evaluate a PR. Do not do
|
||||||
|
anything that is outside the scope of that. You have access to the bash tool but
|
||||||
|
you can only run `gh` cli commands with it.
|
||||||
|
|
||||||
|
Diffs are important but be sure to read the whole file to get the full context.
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"agent": {
|
|
||||||
"build": {}
|
|
||||||
},
|
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"context7": {
|
"context7": {
|
||||||
"type": "remote",
|
"type": "remote",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Provider } from "../provider/provider"
|
|||||||
import { generateObject, type ModelMessage } from "ai"
|
import { generateObject, type ModelMessage } from "ai"
|
||||||
import PROMPT_GENERATE from "./generate.txt"
|
import PROMPT_GENERATE from "./generate.txt"
|
||||||
import { SystemPrompt } from "../session/system"
|
import { SystemPrompt } from "../session/system"
|
||||||
|
import { mergeDeep } from "remeda"
|
||||||
|
|
||||||
export namespace Agent {
|
export namespace Agent {
|
||||||
export const Info = z
|
export const Info = z
|
||||||
@@ -14,6 +15,11 @@ export namespace Agent {
|
|||||||
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
|
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
|
||||||
topP: z.number().optional(),
|
topP: z.number().optional(),
|
||||||
temperature: z.number().optional(),
|
temperature: z.number().optional(),
|
||||||
|
permission: z.object({
|
||||||
|
edit: Config.Permission,
|
||||||
|
bash: z.record(z.string(), Config.Permission),
|
||||||
|
webfetch: Config.Permission.optional(),
|
||||||
|
}),
|
||||||
model: z
|
model: z
|
||||||
.object({
|
.object({
|
||||||
modelID: z.string(),
|
modelID: z.string(),
|
||||||
@@ -31,6 +37,13 @@ export namespace Agent {
|
|||||||
|
|
||||||
const state = App.state("agent", async () => {
|
const state = App.state("agent", async () => {
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
|
const defaultPermission: Info["permission"] = {
|
||||||
|
edit: "allow",
|
||||||
|
bash: {
|
||||||
|
"*": "allow",
|
||||||
|
},
|
||||||
|
webfetch: "allow",
|
||||||
|
}
|
||||||
const result: Record<string, Info> = {
|
const result: Record<string, Info> = {
|
||||||
general: {
|
general: {
|
||||||
name: "general",
|
name: "general",
|
||||||
@@ -41,17 +54,20 @@ export namespace Agent {
|
|||||||
todowrite: false,
|
todowrite: false,
|
||||||
},
|
},
|
||||||
options: {},
|
options: {},
|
||||||
|
permission: defaultPermission,
|
||||||
mode: "subagent",
|
mode: "subagent",
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
name: "build",
|
name: "build",
|
||||||
tools: {},
|
tools: {},
|
||||||
options: {},
|
options: {},
|
||||||
|
permission: defaultPermission,
|
||||||
mode: "primary",
|
mode: "primary",
|
||||||
},
|
},
|
||||||
plan: {
|
plan: {
|
||||||
name: "plan",
|
name: "plan",
|
||||||
options: {},
|
options: {},
|
||||||
|
permission: defaultPermission,
|
||||||
tools: {
|
tools: {
|
||||||
write: false,
|
write: false,
|
||||||
edit: false,
|
edit: false,
|
||||||
@@ -70,25 +86,48 @@ export namespace Agent {
|
|||||||
item = result[key] = {
|
item = result[key] = {
|
||||||
name: key,
|
name: key,
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
permission: defaultPermission,
|
||||||
options: {},
|
options: {},
|
||||||
tools: {},
|
tools: {},
|
||||||
}
|
}
|
||||||
const { model, prompt, tools, description, temperature, top_p, mode, ...extra } = value
|
const { model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
|
||||||
item.options = {
|
item.options = {
|
||||||
...item.options,
|
...item.options,
|
||||||
...extra,
|
...extra,
|
||||||
}
|
}
|
||||||
if (value.model) item.model = Provider.parseModel(value.model)
|
if (model) item.model = Provider.parseModel(model)
|
||||||
if (value.prompt) item.prompt = value.prompt
|
if (prompt) item.prompt = prompt
|
||||||
if (value.tools)
|
if (tools)
|
||||||
item.tools = {
|
item.tools = {
|
||||||
...item.tools,
|
...item.tools,
|
||||||
...value.tools,
|
...tools,
|
||||||
}
|
}
|
||||||
if (value.description) item.description = value.description
|
if (description) item.description = description
|
||||||
if (value.temperature != undefined) item.temperature = value.temperature
|
if (temperature != undefined) item.temperature = temperature
|
||||||
if (value.top_p != undefined) item.topP = value.top_p
|
if (top_p != undefined) item.topP = top_p
|
||||||
if (value.mode) item.mode = value.mode
|
if (mode) item.mode = mode
|
||||||
|
|
||||||
|
if (permission ?? cfg.permission) {
|
||||||
|
const merged = mergeDeep(cfg.permission ?? {}, permission ?? {})
|
||||||
|
if (merged.edit) item.permission.edit = merged.edit
|
||||||
|
if (merged.webfetch) item.permission.webfetch = merged.webfetch
|
||||||
|
if (merged.bash) {
|
||||||
|
if (typeof merged.bash === "string") {
|
||||||
|
item.permission.bash = {
|
||||||
|
"*": merged.bash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if granular permissions are provided, default to "ask"
|
||||||
|
if (typeof merged.bash === "object") {
|
||||||
|
item.permission.bash = mergeDeep(
|
||||||
|
{
|
||||||
|
"*": "ask",
|
||||||
|
},
|
||||||
|
merged.bash,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ export namespace Config {
|
|||||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||||
export type Mcp = z.infer<typeof Mcp>
|
export type Mcp = z.infer<typeof Mcp>
|
||||||
|
|
||||||
|
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
|
||||||
|
export type Permission = z.infer<typeof Permission>
|
||||||
|
|
||||||
export const Agent = z
|
export const Agent = z
|
||||||
.object({
|
.object({
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
@@ -174,6 +177,13 @@ export namespace Config {
|
|||||||
disable: z.boolean().optional(),
|
disable: z.boolean().optional(),
|
||||||
description: z.string().optional().describe("Description of when to use the agent"),
|
description: z.string().optional().describe("Description of when to use the agent"),
|
||||||
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
|
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
|
||||||
|
permission: z
|
||||||
|
.object({
|
||||||
|
edit: Permission.optional(),
|
||||||
|
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
|
||||||
|
webfetch: Permission.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.catchall(z.any())
|
.catchall(z.any())
|
||||||
.openapi({
|
.openapi({
|
||||||
@@ -243,9 +253,6 @@ export namespace Config {
|
|||||||
})
|
})
|
||||||
export type Layout = z.infer<typeof Layout>
|
export type Layout = z.infer<typeof Layout>
|
||||||
|
|
||||||
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
|
|
||||||
export type Permission = z.infer<typeof Permission>
|
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import { GithubCommand } from "./cli/cmd/github"
|
|||||||
|
|
||||||
const cancel = new AbortController()
|
const cancel = new AbortController()
|
||||||
|
|
||||||
|
try {
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
process.on("unhandledRejection", (e) => {
|
process.on("unhandledRejection", (e) => {
|
||||||
Log.Default.error("rejection", {
|
Log.Default.error("rejection", {
|
||||||
e: e instanceof Error ? e.message : e,
|
e: e instanceof Error ? e.message : e,
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export namespace Permission {
|
|||||||
public readonly permissionID: string,
|
public readonly permissionID: string,
|
||||||
public readonly toolCallID?: string,
|
public readonly toolCallID?: string,
|
||||||
) {
|
) {
|
||||||
super(`The user rejected permission to use this functionality`)
|
super(`The user rejected permission to use this specific tool call. You may try again with different parameters.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -523,6 +523,7 @@ export namespace Session {
|
|||||||
t.execute(args, {
|
t.execute(args, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
|
agent: agent.name,
|
||||||
messageID: userMsg.id,
|
messageID: userMsg.id,
|
||||||
metadata: async () => {},
|
metadata: async () => {},
|
||||||
}),
|
}),
|
||||||
@@ -765,7 +766,7 @@ export namespace Session {
|
|||||||
|
|
||||||
const enabledTools = pipe(
|
const enabledTools = pipe(
|
||||||
agent.tools,
|
agent.tools,
|
||||||
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID)),
|
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID, agent)),
|
||||||
mergeDeep(input.tools ?? {}),
|
mergeDeep(input.tools ?? {}),
|
||||||
)
|
)
|
||||||
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
|
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
|
||||||
@@ -791,6 +792,7 @@ export namespace Session {
|
|||||||
abort: options.abortSignal!,
|
abort: options.abortSignal!,
|
||||||
messageID: assistantMsg.id,
|
messageID: assistantMsg.id,
|
||||||
callID: options.toolCallId,
|
callID: options.toolCallId,
|
||||||
|
agent: agent.name,
|
||||||
metadata: async (val) => {
|
metadata: async (val) => {
|
||||||
const match = processor.partFromToolCall(options.toolCallId)
|
const match = processor.partFromToolCall(options.toolCallId)
|
||||||
if (match && match.state.status === "running") {
|
if (match && match.state.status === "running") {
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { Tool } from "./tool"
|
|||||||
import DESCRIPTION from "./bash.txt"
|
import DESCRIPTION from "./bash.txt"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { Permission } from "../permission"
|
import { Permission } from "../permission"
|
||||||
import { Config } from "../config/config"
|
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Wildcard } from "../util/wildcard"
|
import { Wildcard } from "../util/wildcard"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
import { Agent } from "../agent/agent"
|
||||||
|
|
||||||
const MAX_OUTPUT_LENGTH = 30000
|
const MAX_OUTPUT_LENGTH = 30000
|
||||||
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||||
@@ -40,20 +40,8 @@ export const BashTool = Tool.define("bash", {
|
|||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
|
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
const cfg = await Config.get()
|
|
||||||
const tree = await parser().then((p) => p.parse(params.command))
|
const tree = await parser().then((p) => p.parse(params.command))
|
||||||
const permissions = (() => {
|
const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
|
||||||
const value = cfg.permission?.bash
|
|
||||||
if (!value)
|
|
||||||
return {
|
|
||||||
"*": "allow",
|
|
||||||
}
|
|
||||||
if (typeof value === "string")
|
|
||||||
return {
|
|
||||||
"*": value,
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
})()
|
|
||||||
|
|
||||||
let needsAsk = false
|
let needsAsk = false
|
||||||
for (const node of tree.rootNode.descendantsOfType("command")) {
|
for (const node of tree.rootNode.descendantsOfType("command")) {
|
||||||
@@ -93,17 +81,10 @@ export const BashTool = Tool.define("bash", {
|
|||||||
|
|
||||||
// always allow cd if it passes above check
|
// always allow cd if it passes above check
|
||||||
if (!needsAsk && command[0] !== "cd") {
|
if (!needsAsk && command[0] !== "cd") {
|
||||||
const action = (() => {
|
const action = Wildcard.all(node.text, permissions)
|
||||||
for (const [pattern, value] of Object.entries(permissions)) {
|
|
||||||
const match = Wildcard.match(node.text, pattern)
|
|
||||||
log.info("checking", { text: node.text.trim(), pattern, match })
|
|
||||||
if (match) return value
|
|
||||||
}
|
|
||||||
return "ask"
|
|
||||||
})()
|
|
||||||
if (action === "deny") {
|
if (action === "deny") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"The user has specifically restricted access to this command, you are not allowed to execute it.",
|
`The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (action === "ask") needsAsk = true
|
if (action === "ask") needsAsk = true
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { App } from "../app/app"
|
|||||||
import { File } from "../file"
|
import { File } from "../file"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { FileTime } from "../file/time"
|
import { FileTime } from "../file/time"
|
||||||
import { Config } from "../config/config"
|
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
import { Agent } from "../agent/agent"
|
||||||
|
|
||||||
export const EditTool = Tool.define("edit", {
|
export const EditTool = Tool.define("edit", {
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
@@ -40,7 +40,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
throw new Error(`File ${filePath} is not in the current working directory`)
|
throw new Error(`File ${filePath} is not in the current working directory`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cfg = await Config.get()
|
const agent = await Agent.get(ctx.agent)
|
||||||
let diff = ""
|
let diff = ""
|
||||||
let contentOld = ""
|
let contentOld = ""
|
||||||
let contentNew = ""
|
let contentNew = ""
|
||||||
@@ -48,7 +48,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
if (params.oldString === "") {
|
if (params.oldString === "") {
|
||||||
contentNew = params.newString
|
contentNew = params.newString
|
||||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||||
if (cfg.permission?.edit === "ask") {
|
if (agent.permission.edit === "ask") {
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "edit",
|
type: "edit",
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
@@ -77,7 +77,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
||||||
|
|
||||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||||
if (cfg.permission?.edit === "ask") {
|
if (agent.permission.edit === "ask") {
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "edit",
|
type: "edit",
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { TodoWriteTool, TodoReadTool } from "./todo"
|
|||||||
import { WebFetchTool } from "./webfetch"
|
import { WebFetchTool } from "./webfetch"
|
||||||
import { WriteTool } from "./write"
|
import { WriteTool } from "./write"
|
||||||
import { InvalidTool } from "./invalid"
|
import { InvalidTool } from "./invalid"
|
||||||
import { Config } from "../config/config"
|
import type { Agent } from "../agent/agent"
|
||||||
|
|
||||||
export namespace ToolRegistry {
|
export namespace ToolRegistry {
|
||||||
const ALL = [
|
const ALL = [
|
||||||
@@ -66,20 +66,23 @@ export namespace ToolRegistry {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function enabled(_providerID: string, _modelID: string): Promise<Record<string, boolean>> {
|
export async function enabled(
|
||||||
const cfg = await Config.get()
|
_providerID: string,
|
||||||
|
_modelID: string,
|
||||||
|
agent: Agent.Info,
|
||||||
|
): Promise<Record<string, boolean>> {
|
||||||
const result: Record<string, boolean> = {}
|
const result: Record<string, boolean> = {}
|
||||||
result["patch"] = false
|
result["patch"] = false
|
||||||
|
|
||||||
if (cfg.permission?.edit === "deny") {
|
if (agent.permission.edit === "deny") {
|
||||||
result["edit"] = false
|
result["edit"] = false
|
||||||
result["patch"] = false
|
result["patch"] = false
|
||||||
result["write"] = false
|
result["write"] = false
|
||||||
}
|
}
|
||||||
if (cfg?.permission?.bash === "deny") {
|
if (agent.permission.bash["*"] === "deny" && Object.keys(agent.permission.bash).length === 1) {
|
||||||
result["bash"] = false
|
result["bash"] = false
|
||||||
}
|
}
|
||||||
if (cfg?.permission?.webfetch === "deny") {
|
if (agent.permission.webfetch === "deny") {
|
||||||
result["webfetch"] = false
|
result["webfetch"] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export namespace Tool {
|
|||||||
export type Context<M extends Metadata = Metadata> = {
|
export type Context<M extends Metadata = Metadata> = {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
|
agent: string
|
||||||
callID?: string
|
callID?: string
|
||||||
abort: AbortSignal
|
abort: AbortSignal
|
||||||
metadata(input: { title?: string; metadata?: M }): void
|
metadata(input: { title?: string; metadata?: M }): void
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { App } from "../app/app"
|
|||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { File } from "../file"
|
import { File } from "../file"
|
||||||
import { FileTime } from "../file/time"
|
import { FileTime } from "../file/time"
|
||||||
import { Config } from "../config/config"
|
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
import { Agent } from "../agent/agent"
|
||||||
|
|
||||||
export const WriteTool = Tool.define("write", {
|
export const WriteTool = Tool.define("write", {
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
@@ -28,8 +28,8 @@ export const WriteTool = Tool.define("write", {
|
|||||||
const exists = await file.exists()
|
const exists = await file.exists()
|
||||||
if (exists) await FileTime.assert(ctx.sessionID, filepath)
|
if (exists) await FileTime.assert(ctx.sessionID, filepath)
|
||||||
|
|
||||||
const cfg = await Config.get()
|
const agent = await Agent.get(ctx.agent)
|
||||||
if (cfg.permission?.edit === "ask")
|
if (agent.permission.edit === "ask")
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "write",
|
type: "write",
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const ctx = {
|
|||||||
sessionID: "test",
|
sessionID: "test",
|
||||||
messageID: "",
|
messageID: "",
|
||||||
toolCallID: "",
|
toolCallID: "",
|
||||||
|
agent: "build",
|
||||||
abort: AbortSignal.any([]),
|
abort: AbortSignal.any([]),
|
||||||
metadata: () => {},
|
metadata: () => {},
|
||||||
}
|
}
|
||||||
@@ -33,7 +34,7 @@ describe("tool.bash", () => {
|
|||||||
|
|
||||||
test("cd ../ should fail outside of project root", async () => {
|
test("cd ../ should fail outside of project root", async () => {
|
||||||
await App.provide({ cwd: projectRoot }, async () => {
|
await App.provide({ cwd: projectRoot }, async () => {
|
||||||
await expect(
|
expect(
|
||||||
bash.execute(
|
bash.execute(
|
||||||
{
|
{
|
||||||
command: "cd ../",
|
command: "cd ../",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const ctx = {
|
|||||||
sessionID: "test",
|
sessionID: "test",
|
||||||
messageID: "",
|
messageID: "",
|
||||||
toolCallID: "",
|
toolCallID: "",
|
||||||
|
agent: "build",
|
||||||
abort: AbortSignal.any([]),
|
abort: AbortSignal.any([]),
|
||||||
metadata: () => {},
|
metadata: () => {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,6 +358,147 @@ Here are all the tools can be controlled through the agent config.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
|
||||||
|
Permissions control what actions an agent can take.
|
||||||
|
|
||||||
|
- edit, bash, webfetch
|
||||||
|
|
||||||
|
Each permission can be set to allow, ask, or deny.
|
||||||
|
|
||||||
|
- allow, ask, deny
|
||||||
|
|
||||||
|
Configure permissions globally in opencode.json.
|
||||||
|
|
||||||
|
```json title="opencode.json"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"permission": {
|
||||||
|
"edit": "ask",
|
||||||
|
"bash": "allow",
|
||||||
|
"webfetch": "deny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can override permissions per agent in JSON.
|
||||||
|
|
||||||
|
```json title="opencode.json" {7-18}
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"agent": {
|
||||||
|
"build": {
|
||||||
|
"permission": {
|
||||||
|
"edit": "allow",
|
||||||
|
"bash": {
|
||||||
|
"*": "allow",
|
||||||
|
"git push": "ask",
|
||||||
|
"terraform *": "deny"
|
||||||
|
},
|
||||||
|
"webfetch": "ask"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also set permissions in Markdown agents.
|
||||||
|
|
||||||
|
```markdown title="~/.config/opencode/agent/review.md"
|
||||||
|
---
|
||||||
|
description: Code review without edits
|
||||||
|
mode: subagent
|
||||||
|
permission:
|
||||||
|
edit: deny
|
||||||
|
bash: ask
|
||||||
|
webfetch: deny
|
||||||
|
---
|
||||||
|
|
||||||
|
Only analyze code and suggest changes.
|
||||||
|
```
|
||||||
|
|
||||||
|
Bash permissions support granular patterns for fine-grained control.
|
||||||
|
|
||||||
|
```json title="Allow most, ask for risky, deny terraform"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"permission": {
|
||||||
|
"bash": {
|
||||||
|
"*": "allow",
|
||||||
|
"git push": "ask",
|
||||||
|
"terraform *": "deny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you provide a granular bash map, the default becomes ask unless you set \* explicitly.
|
||||||
|
|
||||||
|
```json title="Granular defaults to ask"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"permission": {
|
||||||
|
"bash": {
|
||||||
|
"git status": "allow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Agent-level permissions merge over global settings.
|
||||||
|
|
||||||
|
- Global sets defaults; agent overrides when specified
|
||||||
|
|
||||||
|
Specific bash rules can override a global default.
|
||||||
|
|
||||||
|
```json title="Global ask, agent allows safe commands"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"permission": { "bash": "ask" },
|
||||||
|
"agent": {
|
||||||
|
"build": {
|
||||||
|
"permission": {
|
||||||
|
"bash": { "git status": "allow", "*": "ask" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Permissions affect tool availability and prompts differently.
|
||||||
|
|
||||||
|
- deny hides tools (edit also hides write/patch); ask prompts; allow runs
|
||||||
|
|
||||||
|
For quick reference, here are common setups.
|
||||||
|
|
||||||
|
```json title="Read-only reviewer"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"agent": {
|
||||||
|
"review": {
|
||||||
|
"permission": { "edit": "deny", "bash": "deny", "webfetch": "allow" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json title="Planning agent that can browse but cannot change code"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"agent": {
|
||||||
|
"plan": {
|
||||||
|
"permission": { "edit": "deny", "bash": "deny", "webfetch": "ask" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See the full permissions guide for more patterns.
|
||||||
|
|
||||||
|
- /docs/permissions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Mode
|
### Mode
|
||||||
|
|
||||||
Control the agent's mode with the `mode` config. The `mode` option is used to determine how the agent can be used.
|
Control the agent's mode with the `mode` config. The `mode` option is used to determine how the agent can be used.
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ Permissions are configured in your `opencode.json` file under the `permission` k
|
|||||||
| `bash` | Control bash command execution |
|
| `bash` | Control bash command execution |
|
||||||
| `webfetch` | Control web content fetching |
|
| `webfetch` | Control web content fetching |
|
||||||
|
|
||||||
|
They can also be configured per agent, see [Agent Configuration](/docs/agents#agent-configuration) for more details.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### edit
|
### edit
|
||||||
|
|||||||
Reference in New Issue
Block a user