This commit is contained in:
Dax Raad 2025-12-31 13:35:14 -05:00
commit c5d1a51b1f
10 changed files with 94 additions and 21 deletions

View file

@ -11,7 +11,7 @@ import PROMPT_EXPLORE from "./prompt/explore.txt"
import PROMPT_SUMMARY from "./prompt/summary.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import { PermissionNext } from "@/permission/next"
import { mergeDeep } from "remeda"
import { mergeDeep, pipe, sortBy, values } from "remeda"
export namespace Agent {
export const Info = z
@ -194,7 +194,12 @@ export namespace Agent {
}
export async function list() {
return state().then((x) => Object.values(x))
const cfg = await Config.get()
return pipe(
await state(),
values(),
sortBy([(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"]),
)
}
export async function defaultAgent() {

View file

@ -38,7 +38,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const [agentStore, setAgentStore] = createStore<{
current: string
}>({
current: agents().find((x) => x.default)?.name ?? agents()[0].name,
current: agents()[0].name,
})
const { theme } = useTheme()
const colors = createMemo(() => [

View file

@ -125,11 +125,11 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
body={
<Switch>
<Match when={props.request.always.length === 1 && props.request.always[0] === "*"}>
<TextBody title={"Are you sure you want to always allow " + props.request.permission + "?"} />
<TextBody title={"This will allow " + props.request.permission + " until OpenCode is restarted."} />
</Match>
<Match when={true}>
<box paddingLeft={1} gap={1}>
<text fg={theme.textMuted}>Applies to the following patterns</text>
<text fg={theme.textMuted}>This will allow the following patterns until OpenCode is restarted</text>
<For each={props.request.always}>
{(pattern) => (
<text fg={theme.text}>

View file

@ -189,7 +189,9 @@ export namespace PermissionNext {
action: "allow",
})
}
await Storage.write(["permission", projectID], s.approved)
// TODO: we don't save the permission ruleset to disk yet until there's
// UI to manage it
// await Storage.write(["permission", projectID], s.approved)
existing.resolve()
return
}

View file

@ -78,6 +78,7 @@ export namespace Plugin {
const hooks = await state().then((x) => x.hooks)
const config = await Config.get()
for (const hook of hooks) {
// @ts-expect-error this is because we haven't moved plugin to sdk v2
await hook.config?.(config)
}
Bus.subscribeAll(async (input) => {

View file

@ -18,6 +18,7 @@ import { Command } from "../command"
import { Snapshot } from "@/snapshot"
import type { Provider } from "@/provider/provider"
import { PermissionNext } from "@/permission/next"
export namespace Session {
const log = Log.create({ service: "session" })
@ -62,6 +63,7 @@ export namespace Session {
compacting: z.number().optional(),
archived: z.number().optional(),
}),
permission: PermissionNext.Ruleset.optional(),
revert: z
.object({
messageID: z.string(),
@ -126,6 +128,7 @@ export namespace Session {
.object({
parentID: Identifier.schema("session").optional(),
title: z.string().optional(),
permission: Info.shape.permission,
})
.optional(),
async (input) => {
@ -133,6 +136,7 @@ export namespace Session {
parentID: input?.parentID,
directory: Instance.directory,
title: input?.title,
permission: input?.permission,
})
},
)
@ -174,7 +178,13 @@ export namespace Session {
})
})
export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
export async function createNext(input: {
id?: string
title?: string
parentID?: string
directory: string
permission?: PermissionNext.Ruleset
}) {
const result: Info = {
id: Identifier.descending("session", input.id),
version: Installation.VERSION,
@ -182,6 +192,7 @@ export namespace Session {
directory: input.directory,
parentID: input.parentID,
title: input.title ?? createDefaultTitle(!!input.parentID),
permission: input.permission,
time: {
created: Date.now(),
updated: Date.now(),

View file

@ -89,7 +89,12 @@ export namespace SessionPrompt {
.optional(),
agent: z.string().optional(),
noReply: z.boolean().optional(),
tools: z.record(z.string(), z.boolean()).optional(),
tools: z
.record(z.string(), z.boolean())
.optional()
.describe(
"@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
),
system: z.string().optional(),
variant: z.string().optional(),
parts: z.array(
@ -146,6 +151,23 @@ export namespace SessionPrompt {
const message = await createUserMessage(input)
await Session.touch(input.sessionID)
// this is backwards compatibility for allowing `tools` to be specified when
// prompting
const permissions: PermissionNext.Ruleset = []
for (const [tool, enabled] of Object.entries(input.tools ?? {})) {
permissions.push({
permission: tool,
action: enabled ? "allow" : "deny",
pattern: "*",
})
}
if (permissions.length > 0) {
session.permission = permissions
await Session.update(session.id, (draft) => {
draft.permission = permissions
})
}
if (input.noReply === true) {
return message
}
@ -372,7 +394,7 @@ export namespace SessionPrompt {
await PermissionNext.ask({
...req,
sessionID: sessionID,
ruleset: taskAgent.permission,
ruleset: PermissionNext.merge(taskAgent.permission, session.permission ?? []),
})
},
}
@ -626,7 +648,7 @@ export namespace SessionPrompt {
...req,
sessionID: input.session.parentID ?? input.session.id,
tool: { messageID: input.processor.message.id, callID: options.toolCallId },
ruleset: input.agent.permission,
ruleset: PermissionNext.merge(input.agent.permission, input.session.permission ?? []),
})
},
})

View file

@ -50,6 +50,28 @@ export const TaskTool = Tool.define("task", async () => {
return await Session.create({
parentID: ctx.sessionID,
title: params.description + ` (@${agent.name} subagent)`,
permission: [
{
permission: "todowrite",
pattern: "*",
action: "deny",
},
{
permission: "todoread",
pattern: "*",
action: "deny",
},
{
permission: "task",
pattern: "*",
action: "deny",
},
...(config.experimental?.primary_tools?.map((t) => ({
pattern: "*",
action: "allow" as const,
permission: t,
})) ?? []),
],
})
})
const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
@ -112,7 +134,6 @@ export const TaskTool = Tool.define("task", async () => {
todoread: false,
task: false,
...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])),
...agent.tools,
},
parts: promptParts,
})

View file

@ -59,6 +59,7 @@ import type {
PermissionReplyResponses,
PermissionRespondErrors,
PermissionRespondResponses,
PermissionRuleset,
ProjectCurrentResponses,
ProjectListResponses,
ProjectUpdateErrors,
@ -730,6 +731,7 @@ export class Session extends HeyApiClient {
directory?: string
parentID?: string
title?: string
permission?: PermissionRuleset
},
options?: Options<never, ThrowOnError>,
) {
@ -741,6 +743,7 @@ export class Session extends HeyApiClient {
{ in: "query", key: "directory" },
{ in: "body", key: "parentID" },
{ in: "body", key: "title" },
{ in: "body", key: "permission" },
],
},
],

View file

@ -639,6 +639,16 @@ export type EventCommandExecuted = {
}
}
export type PermissionAction = "allow" | "deny" | "ask"
export type PermissionRule = {
permission: string
pattern: string
action: PermissionAction
}
export type PermissionRuleset = Array<PermissionRule>
export type Session = {
id: string
projectID: string
@ -661,6 +671,7 @@ export type Session = {
compacting?: number
archived?: number
}
permission?: PermissionRuleset
revert?: {
messageID: string
partID?: string
@ -1895,16 +1906,6 @@ export type File = {
status: "added" | "deleted" | "modified"
}
export type PermissionAction = "allow" | "deny" | "ask"
export type PermissionRule = {
permission: string
pattern: string
action: PermissionAction
}
export type PermissionRuleset = Array<PermissionRule>
export type Agent = {
name: string
description?: string
@ -2467,6 +2468,7 @@ export type SessionCreateData = {
body?: {
parentID?: string
title?: string
permission?: PermissionRuleset
}
path?: never
query?: {
@ -2982,6 +2984,9 @@ export type SessionPromptData = {
}
agent?: string
noReply?: boolean
/**
* @deprecated tools and permissions have been merged, you can set permissions on the session itself now
*/
tools?: {
[key: string]: boolean
}
@ -3166,6 +3171,9 @@ export type SessionPromptAsyncData = {
}
agent?: string
noReply?: boolean
/**
* @deprecated tools and permissions have been merged, you can set permissions on the session itself now
*/
tools?: {
[key: string]: boolean
}