Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
38376f4
PATCH /config hot-reload w/ selective cache invalidation (#15)
shuv1337 Nov 13, 2025
f471203
Merge branch 'dev' into config-hot-reload
shuv1337 Nov 13, 2025
3dbd5ed
typecheck fix
shuv1337 Nov 13, 2025
df3e955
chore: format code
actions-user Nov 13, 2025
f71cb8b
Merge branch 'dev' into config-hot-reload
shuv1337 Nov 13, 2025
6ee86eb
fix for failing test
shuv1337 Nov 13, 2025
725a878
fix for failing test
shuv1337 Nov 14, 2025
865955e
chore: format code
actions-user Nov 14, 2025
bdf7f7d
Merge branch 'dev' into config-hot-reload
shuv1337 Nov 14, 2025
bad3021
Merge branch 'integration' into config-hot-reload
shuv1337 Nov 14, 2025
f28361f
chore: format code
actions-user Nov 14, 2025
410fd58
Merge branch 'integration' into config-hot-reload
shuv1337 Nov 14, 2025
37527bd
Fix cache corruption from partial updates in file refresh (#18)
Copilot Nov 14, 2025
ce411d0
Merge branch 'integration' into config-hot-reload
shuv1337 Nov 14, 2025
66546ce
Add favorites to model selector (#23)
shuv1337 Nov 15, 2025
e3d696e
Merge branch 'integration' into config-hot-reload
shuv1337 Nov 17, 2025
03d3c80
revert desktop changes
shuv1337 Nov 17, 2025
76d48d8
revert desktop changes
shuv1337 Nov 17, 2025
4d5f6b5
proper fix for image attachments
shuv1337 Nov 20, 2025
26b6b1e
Merge branch 'dev' into fix-tui-img-attachments
shuv1337 Nov 21, 2025
f3b5b37
chore: format code
actions-user Nov 21, 2025
2c264a7
Merge branch 'dev' into add-model-favorites
shuv1337 Nov 21, 2025
a069899
Update Nix flake.lock and hashes
actions-user Nov 21, 2025
d9a0881
chore: format code
actions-user Nov 21, 2025
1aedc8a
chore: format code
actions-user Nov 21, 2025
0a1f6d8
fix syntax error
shuv1337 Nov 21, 2025
15b72aa
Merge branch 'dev' into add-model-favorites
shuv1337 Nov 21, 2025
51eaba8
Merge branch 'dev' into fix-tui-img-attachments
shuv1337 Nov 21, 2025
16aed1b
chore: format code
actions-user Nov 21, 2025
e6b665e
Merge branch 'dev' into add-model-favorites
shuv1337 Nov 21, 2025
0b96087
Merge branch 'dev' into fix-tui-img-attachments
shuv1337 Nov 21, 2025
a8832fa
Update Nix flake.lock and hashes
actions-user Nov 21, 2025
fd4d38b
Merge branch 'dev' into add-model-favorites
shuv1337 Nov 21, 2025
65b7569
Merge branch 'dev' into fix-tui-img-attachments
shuv1337 Nov 21, 2025
1296e46
Merge origin/dev into config-hot-reload
shuv1337 Nov 21, 2025
9c9d2ed
Merge branch 'dev' into fix-tui-img-attachments
rekram1-node Nov 21, 2025
8cfe611
fix: resolve type error in provider configuration loop
shuv1337 Nov 21, 2025
980c331
gen sdk
shuv1337 Nov 21, 2025
62a8131
Update Nix flake.lock and hashes
actions-user Nov 21, 2025
01133dd
Merge branch 'dev' into config-hot-reload
shuv1337 Nov 21, 2025
3f44edd
Merge branch 'sst:dev' into integration
shuv1337 Nov 21, 2025
a4cbc52
Update Nix flake.lock and hashes
actions-user Nov 21, 2025
f1ff202
Local test merge of PR #29: Config hot reload
shuv1337 Nov 21, 2025
cf53bc3
Local test merge of PR #27: Add model favorites
shuv1337 Nov 21, 2025
3538e54
Local test merge of PR #26: proper fix for image attachments
shuv1337 Nov 21, 2025
4423bc5
Local test merge of PR #30: Merge Dev into Integration
shuv1337 Nov 21, 2025
6d6be71
Local test merge of dev into integration: whitelist/blacklist provide…
shuv1337 Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 130 additions & 125 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { generateObject, type ModelMessage } from "ai"
import PROMPT_GENERATE from "./generate.txt"
import { SystemPrompt } from "../session/system"
import { Instance } from "../project/instance"
import { State } from "../project/state"
import { mergeDeep } from "remeda"

export namespace Agent {
Expand Down Expand Up @@ -39,145 +40,149 @@ export namespace Agent {
})
export type Info = z.infer<typeof Info>

const state = Instance.state(async () => {
const cfg = await Config.get()
const defaultTools = cfg.tools ?? {}
const defaultPermission: Info["permission"] = {
edit: "allow",
bash: {
"*": "allow",
},
webfetch: "allow",
doom_loop: "ask",
external_directory: "ask",
}
const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})

const planPermission = mergeAgentPermissions(
{
edit: "deny",
const state = State.register(
"agent",
() => Instance.directory,
async () => {
const cfg = await Config.get()
const defaultTools = cfg.tools ?? {}
const defaultPermission: Info["permission"] = {
edit: "allow",
bash: {
"cut*": "allow",
"diff*": "allow",
"du*": "allow",
"file *": "allow",
"find * -delete*": "ask",
"find * -exec*": "ask",
"find * -fprint*": "ask",
"find * -fls*": "ask",
"find * -fprintf*": "ask",
"find * -ok*": "ask",
"find *": "allow",
"git diff*": "allow",
"git log*": "allow",
"git show*": "allow",
"git status*": "allow",
"git branch": "allow",
"git branch -v": "allow",
"grep*": "allow",
"head*": "allow",
"less*": "allow",
"ls*": "allow",
"more*": "allow",
"pwd*": "allow",
"rg*": "allow",
"sort --output=*": "ask",
"sort -o *": "ask",
"sort*": "allow",
"stat*": "allow",
"tail*": "allow",
"tree -o *": "ask",
"tree*": "allow",
"uniq*": "allow",
"wc*": "allow",
"whereis*": "allow",
"which*": "allow",
"*": "ask",
"*": "allow",
},
webfetch: "allow",
},
cfg.permission ?? {},
)
doom_loop: "ask",
external_directory: "ask",
}
const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})

const result: Record<string, Info> = {
general: {
name: "general",
description:
"General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
tools: {
todoread: false,
todowrite: false,
...defaultTools,
const planPermission = mergeAgentPermissions(
{
edit: "deny",
bash: {
"cut*": "allow",
"diff*": "allow",
"du*": "allow",
"file *": "allow",
"find * -delete*": "ask",
"find * -exec*": "ask",
"find * -fprint*": "ask",
"find * -fls*": "ask",
"find * -fprintf*": "ask",
"find * -ok*": "ask",
"find *": "allow",
"git diff*": "allow",
"git log*": "allow",
"git show*": "allow",
"git status*": "allow",
"git branch": "allow",
"git branch -v": "allow",
"grep*": "allow",
"head*": "allow",
"less*": "allow",
"ls*": "allow",
"more*": "allow",
"pwd*": "allow",
"rg*": "allow",
"sort --output=*": "ask",
"sort -o *": "ask",
"sort*": "allow",
"stat*": "allow",
"tail*": "allow",
"tree -o *": "ask",
"tree*": "allow",
"uniq*": "allow",
"wc*": "allow",
"whereis*": "allow",
"which*": "allow",
"*": "ask",
},
webfetch: "allow",
},
options: {},
permission: agentPermission,
mode: "subagent",
builtIn: true,
},
build: {
name: "build",
tools: { ...defaultTools },
options: {},
permission: agentPermission,
mode: "primary",
builtIn: true,
},
plan: {
name: "plan",
options: {},
permission: planPermission,
tools: {
...defaultTools,
cfg.permission ?? {},
)

const result: Record<string, Info> = {
general: {
name: "general",
description:
"General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
tools: {
todoread: false,
todowrite: false,
...defaultTools,
},
options: {},
permission: agentPermission,
mode: "subagent",
builtIn: true,
},
mode: "primary",
builtIn: true,
},
}
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
if (value.disable) {
delete result[key]
continue
}
let item = result[key]
if (!item)
item = result[key] = {
name: key,
mode: "all",
build: {
name: "build",
tools: { ...defaultTools },
options: {},
permission: agentPermission,
mode: "primary",
builtIn: true,
},
plan: {
name: "plan",
options: {},
tools: {},
builtIn: false,
}
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, color, ...extra } = value
item.options = {
...item.options,
...extra,
permission: planPermission,
tools: {
...defaultTools,
},
mode: "primary",
builtIn: true,
},
}
if (model) item.model = Provider.parseModel(model)
if (prompt) item.prompt = prompt
if (tools)
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
if (value.disable) {
delete result[key]
continue
}
let item = result[key]
if (!item)
item = result[key] = {
name: key,
mode: "all",
permission: agentPermission,
options: {},
tools: {},
builtIn: false,
}
const { name, model, prompt, tools, description, temperature, top_p, mode, color, permission, ...extra } = value
item.options = {
...item.options,
...extra,
}
if (model) item.model = Provider.parseModel(model)
if (prompt) item.prompt = prompt
if (tools)
item.tools = {
...item.tools,
...tools,
}
item.tools = {
...defaultTools,
...item.tools,
...tools,
}
item.tools = {
...defaultTools,
...item.tools,
}
if (description) item.description = description
if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
if (color) item.color = color
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name
if (description) item.description = description
if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
if (color) item.color = color
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name

if (permission ?? cfg.permission) {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
if (permission ?? cfg.permission) {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
}
}
Comment on lines +140 to 182
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Permission merging can drop safe defaults and mutates config objects

Two subtle issues in the permission path:

  1. Loss of doom_loop / external_directory defaults

    • agentPermission and planPermission correctly start from defaultPermission / planPermission (with doom_loop/external_directory defaulting to "ask"), merged with cfg.permission.
    • Inside the for (const [key, value] of Object.entries(cfg.agent ?? {})) loop, the final if (permission ?? cfg.permission) branch overwrites item.permission with mergeAgentPermissions(cfg.permission ?? {}, permission ?? {}), which ignores defaultPermission.
    • For agents that have an entry in cfg.agent (including overrides of built-ins) and a global cfg.permission that doesn’t mention doom_loop / external_directory, the merged result can change those fields from "ask" to undefined. This weakens the default safety posture for those overrides.

    Consider basing this merge on the agent’s existing permission (item.permission) instead, e.g. mergeAgentPermissions(item.permission, permission ?? {}), so that per-agent overrides extend the default rather than replacing it.

  2. mergeAgentPermissions mutates its arguments

    • The normalization logic rewrites basePermission.bash and overridePermission.bash when they’re strings. If cfg.permission is passed in, this mutates the shared config object and may have surprising side effects elsewhere that read cfg.permission.
    • A shallow clone of the inputs before normalization (or working on local temporaries) would avoid this kind of in-place mutation.

Addressing both would make the permission model safer and easier to reason about.

Also applies to: 226-263

}
return result
})
return result
},
)

export async function get(agent: string) {
return state().then((x) => x[agent])
Expand Down
18 changes: 18 additions & 0 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,24 @@ function App() {
local.model.cycle(-1)
},
},
{
title: "Favorite cycle",
value: "model.cycle_favorite",
keybind: "model_cycle_favorite",
category: "Agent",
onSelect: () => {
local.model.cycleFavorite(1)
},
},
{
title: "Favorite cycle reverse",
value: "model.cycle_favorite_reverse",
keybind: "model_cycle_favorite_reverse",
category: "Agent",
onSelect: () => {
local.model.cycleFavorite(-1)
},
},
{
title: "Switch agent",
value: "agent.list",
Expand Down
Loading