Download packages/app/src/context/permission.tsx from SaylorTwift/opencode: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/context/permission.tsx
- Command line
-
hf download hf://SaylorTwift/opencode/packages/app/src/context/permission.tsx
-
curl -L -o permission.tsx https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/context/permission.tsx
16.2 kB
| import { createEffect, createMemo, createRoot, getOwner, onCleanup } from "solid-js" | |
| import { createStore, produce } from "solid-js/store" | |
| import { createSimpleContext } from "@opencode-ai/ui/context" | |
| import type { PermissionRequest } from "@opencode-ai/sdk/v2/client" | |
| import { Persist, persisted } from "@/utils/persist" | |
| import type { ServerSDK } from "@/context/server-sdk" | |
| import type { ServerSync } from "./server-sync" | |
| import { useParams, useSearchParams } from "@solidjs/router" | |
| import { decode64 } from "@/utils/base64" | |
| import { useGlobal } from "./global" | |
| import { ServerConnection, useServer } from "./server" | |
| import { type DraftTab, useTabs } from "./tabs" | |
| import { useSettings } from "./settings" | |
| import { requireServerKey } from "@/utils/session-route" | |
| import type { ServerScope } from "@/utils/server-scope" | |
| import { normalizePermissionRequest } from "./global-sync/utils" | |
| import { | |
| acceptKey, | |
| directoryAcceptKey, | |
| isDirectoryAutoAccepting, | |
| autoRespondsPermission, | |
| sessionAutoAccept, | |
| } from "./permission-auto-respond" | |
| type PermissionRespondFn = (input: { | |
| sessionID: string | |
| permissionID: string | |
| response: "once" | "always" | "reject" | |
| directory?: string | |
| }) => void | |
| function isNonAllowRule(rule: unknown) { | |
| if (!rule) return false | |
| if (typeof rule === "string") return rule !== "allow" | |
| if (typeof rule !== "object") return false | |
| if (Array.isArray(rule)) return false | |
| for (const action of Object.values(rule)) { | |
| if (action !== "allow") return true | |
| } | |
| return false | |
| } | |
| function hasPermissionPromptRules(permission: unknown) { | |
| if (!permission) return false | |
| if (typeof permission === "string") return permission !== "allow" | |
| if (typeof permission !== "object") return false | |
| if (Array.isArray(permission)) return false | |
| const config = permission as Record<string, unknown> | |
| return Object.values(config).some(isNonAllowRule) | |
| } | |
| export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({ | |
| name: "Permission", | |
| gate: false, | |
| init: () => { | |
| const params = useParams<{ serverKey?: string; dir?: string; id?: string }>() | |
| const [search] = useSearchParams<{ draftId?: string }>() | |
| const global = useGlobal() | |
| const server = useServer() | |
| const tabs = useTabs() | |
| const settings = useSettings() | |
| const owner = getOwner() | |
| const states = new Map<ServerScope, { key: ServerConnection.Key; dispose: () => void; state: PermissionState }>() | |
| const activeDraft = createMemo(() => { | |
| if (!search.draftId) return | |
| return tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId) | |
| }) | |
| const activeServer = createMemo(() => { | |
| if (params.serverKey && settings.general.newLayoutDesigns()) return requireServerKey(params.serverKey) | |
| return activeDraft()?.server ?? server.key | |
| }) | |
| const ensure = (key: ServerConnection.Key) => { | |
| const conn = global.servers.list().find((item) => ServerConnection.key(item) === key) | |
| if (!conn) throw new Error(`Permission server not found: ${key}`) | |
| const ctx = global.ensureServerCtx(conn) | |
| const existing = states.get(ctx.sdk.scope) | |
| if (existing && global.servers.list().some((item) => ServerConnection.key(item) === existing.key)) { | |
| return existing.state | |
| } | |
| if (existing) { | |
| existing.dispose() | |
| states.delete(ctx.sdk.scope) | |
| } | |
| const root = createRoot( | |
| (dispose) => ({ | |
| key, | |
| dispose, | |
| state: createServerPermissionState({ sdk: ctx.sdk, sync: ctx.sync }), | |
| }), | |
| owner ?? undefined, | |
| ) | |
| states.set(ctx.sdk.scope, root) | |
| return root.state | |
| } | |
| createEffect(() => { | |
| global.servers.list().forEach((conn) => ensure(ServerConnection.key(conn))) | |
| }) | |
| createEffect(() => { | |
| const list = global.servers.list() | |
| const keys = new Set(list.map(ServerConnection.key)) | |
| states.forEach((value, scope) => { | |
| if (keys.has(value.key)) return | |
| value.dispose() | |
| states.delete(scope) | |
| const replacement = list.find((conn) => server.scope(ServerConnection.key(conn)) === scope) | |
| if (replacement) ensure(ServerConnection.key(replacement)) | |
| }) | |
| }) | |
| onCleanup(() => states.forEach((value) => value.dispose())) | |
| let lastSelected: PermissionState | undefined | |
| const selected = () => { | |
| const key = activeServer() | |
| if (global.servers.list().some((conn) => ServerConnection.key(conn) === key)) { | |
| lastSelected = ensure(key) | |
| } | |
| if (lastSelected) return lastSelected | |
| return ensure(server.key) | |
| } | |
| const activeDirectory = createMemo(() => { | |
| const directory = decode64(params.dir) | |
| if (directory) return directory | |
| const draft = activeDraft() | |
| if (draft) return draft.directory | |
| if (!params.id) return | |
| if (!global.servers.list().some((conn) => ServerConnection.key(conn) === activeServer())) return | |
| return selected().sync.session.lineage.peek(params.id)?.session.directory | |
| }) | |
| createEffect(() => { | |
| const directory = activeDirectory() | |
| if (!directory) return | |
| selected().enableConfiguredDirectory(directory) | |
| }) | |
| const permissionsEnabled = createMemo(() => { | |
| const directory = activeDirectory() | |
| if (!directory) return false | |
| return selected().permissionsEnabled(directory) | |
| }) | |
| return { | |
| ready: () => selected().ready(), | |
| ensureServerState: (key: ServerConnection.Key) => ensure(key).api, | |
| currentServerState: () => selected().api, | |
| respond(input: Parameters<PermissionRespondFn>[0]) { | |
| selected().respond(input) | |
| }, | |
| autoResponds(permission: PermissionRequest, directory?: string) { | |
| return selected().autoResponds(permission, directory) | |
| }, | |
| isAutoAccepting(sessionID: string, directory?: string) { | |
| return selected().isAutoAccepting(sessionID, directory) | |
| }, | |
| isAutoAcceptingDirectory(directory: string) { | |
| return selected().isAutoAcceptingDirectory(directory) | |
| }, | |
| toggleAutoAccept(sessionID: string, directory: string) { | |
| selected().toggleAutoAccept(sessionID, directory) | |
| }, | |
| toggleAutoAcceptDirectory(directory: string) { | |
| selected().toggleAutoAcceptDirectory(directory) | |
| }, | |
| enableAutoAccept(sessionID: string, directory: string) { | |
| selected().enableAutoAccept(sessionID, directory) | |
| }, | |
| disableAutoAccept(sessionID: string, directory?: string) { | |
| selected().disableAutoAccept(sessionID, directory) | |
| }, | |
| permissionsEnabled, | |
| isPermissionAllowAll(directory: string) { | |
| return selected().isPermissionAllowAll(directory) | |
| }, | |
| } | |
| }, | |
| }) | |
| type PermissionState = ReturnType<typeof createServerPermissionState> | |
| type PermissionEvent = Parameters<Parameters<ServerSDK["event"]["listen"]>[0]>[0] | |
| function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }) { | |
| const [store, setStore, _, ready] = persisted( | |
| { | |
| ...Persist.serverGlobal(input.sdk.scope, "permission", ["permission.v3"]), | |
| migrate(value) { | |
| if (!value || typeof value !== "object" || Array.isArray(value)) return value | |
| const data = value as Record<string, unknown> | |
| if (data.autoAccept) return value | |
| return { | |
| ...data, | |
| autoAccept: | |
| typeof data.autoAcceptEdits === "object" && data.autoAcceptEdits && !Array.isArray(data.autoAcceptEdits) | |
| ? data.autoAcceptEdits | |
| : {}, | |
| } | |
| }, | |
| }, | |
| createStore({ | |
| autoAccept: {} as Record<string, boolean>, | |
| }), | |
| ) | |
| function enableConfiguredDirectory(directory: string) { | |
| if (input.sdk.protocolKind() !== "v1") return | |
| if (meta.disposed || !ready()) return | |
| const [childStore] = input.sync.child(directory) | |
| if (childStore.config.permission !== "allow") return | |
| const key = directoryAcceptKey(directory) | |
| if (store.autoAccept[key] !== undefined) return | |
| setStore( | |
| produce((draft) => { | |
| draft.autoAccept[key] = true | |
| }), | |
| ) | |
| } | |
| const MAX_RESPONDED = 1000 | |
| const RESPONDED_TTL_MS = 60 * 60 * 1000 | |
| const responded = new Map<string, number>() | |
| const enableVersion = new Map<string, number>() | |
| const meta = { disposed: false } | |
| function pruneResponded(now: number) { | |
| for (const [id, ts] of responded) { | |
| if (now - ts < RESPONDED_TTL_MS) break | |
| responded.delete(id) | |
| } | |
| for (const id of responded.keys()) { | |
| if (responded.size <= MAX_RESPONDED) break | |
| responded.delete(id) | |
| } | |
| } | |
| const respond: PermissionRespondFn = (request) => { | |
| if (meta.disposed) return | |
| input.sdk.api.permission | |
| .reply({ | |
| sessionID: request.sessionID, | |
| requestID: request.permissionID, | |
| reply: request.response, | |
| location: request.directory ? { directory: request.directory } : undefined, | |
| }) | |
| .catch(() => { | |
| responded.delete(request.permissionID) | |
| }) | |
| } | |
| const list = async (directory: string) => { | |
| if ((await input.sdk.protocol) === "v1") { | |
| return (await input.sdk.client.permission.list({ directory })).data ?? [] | |
| } | |
| return input.sdk.api.permission.request | |
| .list({ location: { directory } }) | |
| .then((result) => result.data.map(normalizePermissionRequest)) | |
| } | |
| function respondOnce(permission: PermissionRequest, directory?: string) { | |
| const now = Date.now() | |
| const hit = responded.has(permission.id) | |
| responded.delete(permission.id) | |
| responded.set(permission.id, now) | |
| pruneResponded(now) | |
| if (hit) return | |
| respond({ | |
| sessionID: permission.sessionID, | |
| permissionID: permission.id, | |
| response: "once", | |
| directory, | |
| }) | |
| } | |
| function sessions(directory?: string) { | |
| const info = Object.values(input.sync.session.data.info).filter((session) => !!session) | |
| if (!directory) return info | |
| return [...info, ...input.sync.child(directory, { bootstrap: false })[0].session] | |
| } | |
| function isAutoAccepting(sessionID: string, directory?: string) { | |
| return autoRespondsPermission(store.autoAccept, sessions(directory), { sessionID }, directory) | |
| } | |
| function isAutoAcceptingDirectory(directory: string) { | |
| return isDirectoryAutoAccepting(store.autoAccept, directory) | |
| } | |
| function shouldAutoRespond(permission: PermissionRequest, directory?: string) { | |
| return autoRespondsPermission(store.autoAccept, sessions(directory), permission, directory) | |
| } | |
| function isPending(permission: PermissionRequest) { | |
| const pending = input.sync.session.data.permission[permission.sessionID] | |
| return pending === undefined || pending.some((item) => item.id === permission.id) | |
| } | |
| async function shouldAutoRespondResolved(permission: PermissionRequest, directory?: string) { | |
| const override = sessionAutoAccept(store.autoAccept, sessions(directory), permission, directory) | |
| if (override !== undefined) return override | |
| if (input.sync.session.lineage.peek(permission.sessionID)) return shouldAutoRespond(permission, directory) | |
| const lineage = await input.sync.session.lineage.resolve(permission.sessionID).catch(() => undefined) | |
| if (meta.disposed || !lineage) return false | |
| return shouldAutoRespond(permission, directory) | |
| } | |
| async function respondPending( | |
| permission: PermissionRequest, | |
| directory?: string, | |
| current: () => boolean = () => true, | |
| ) { | |
| if (!current() || !isPending(permission)) return | |
| if (!(await shouldAutoRespondResolved(permission, directory))) return | |
| if (meta.disposed || !current() || !isPending(permission)) return | |
| respondOnce(permission, directory) | |
| } | |
| function bumpEnableVersion(sessionID: string, directory?: string) { | |
| const key = acceptKey(sessionID, directory) | |
| const next = (enableVersion.get(key) ?? 0) + 1 | |
| enableVersion.set(key, next) | |
| return next | |
| } | |
| const handlePermission = (e: PermissionEvent) => { | |
| const event = e.details | |
| if (event?.type !== "permission.asked") return | |
| void respondPending(event.properties, e.name) | |
| } | |
| const unsubscribe = input.sdk.event.listen((event) => { | |
| if (ready()) { | |
| handlePermission(event) | |
| return | |
| } | |
| void ready.promise?.then(() => { | |
| if (meta.disposed) return | |
| handlePermission(event) | |
| }) | |
| }) | |
| onCleanup(() => { | |
| meta.disposed = true | |
| unsubscribe() | |
| }) | |
| function enableDirectory(directory: string) { | |
| if (meta.disposed) return | |
| const key = directoryAcceptKey(directory) | |
| setStore( | |
| produce((draft) => { | |
| draft.autoAccept[key] = true | |
| }), | |
| ) | |
| list(directory) | |
| .then((permissions) => { | |
| if (meta.disposed) return | |
| if (!isAutoAcceptingDirectory(directory)) return | |
| for (const permission of permissions) { | |
| void respondPending(permission, directory, () => isAutoAcceptingDirectory(directory)) | |
| } | |
| }) | |
| .catch(() => undefined) | |
| } | |
| function disableDirectory(directory: string) { | |
| if (meta.disposed) return | |
| const key = directoryAcceptKey(directory) | |
| setStore( | |
| produce((draft) => { | |
| draft.autoAccept[key] = false | |
| }), | |
| ) | |
| } | |
| function enable(sessionID: string, directory: string) { | |
| if (meta.disposed) return | |
| const key = acceptKey(sessionID, directory) | |
| const version = bumpEnableVersion(sessionID, directory) | |
| setStore( | |
| produce((draft) => { | |
| draft.autoAccept[key] = true | |
| delete draft.autoAccept[sessionID] | |
| }), | |
| ) | |
| list(directory) | |
| .then((permissions) => { | |
| if (meta.disposed) return | |
| if (enableVersion.get(key) !== version) return | |
| if (!isAutoAccepting(sessionID, directory)) return | |
| for (const permission of permissions) { | |
| void respondPending( | |
| permission, | |
| directory, | |
| () => enableVersion.get(key) === version && isAutoAccepting(sessionID, directory), | |
| ) | |
| } | |
| }) | |
| .catch(() => undefined) | |
| } | |
| function disable(sessionID: string, directory?: string) { | |
| if (meta.disposed) return | |
| bumpEnableVersion(sessionID, directory) | |
| const key = directory ? acceptKey(sessionID, directory) : sessionID | |
| setStore( | |
| produce((draft) => { | |
| draft.autoAccept[key] = false | |
| if (!directory) return | |
| delete draft.autoAccept[sessionID] | |
| }), | |
| ) | |
| } | |
| const api = { | |
| ready: () => !meta.disposed && ready(), | |
| respond, | |
| autoResponds(permission: PermissionRequest, directory?: string) { | |
| if (meta.disposed) return false | |
| return shouldAutoRespond(permission, directory) | |
| }, | |
| isAutoAccepting(sessionID: string, directory?: string) { | |
| if (meta.disposed) return false | |
| return isAutoAccepting(sessionID, directory) | |
| }, | |
| isAutoAcceptingDirectory(directory: string) { | |
| if (meta.disposed) return false | |
| return isAutoAcceptingDirectory(directory) | |
| }, | |
| toggleAutoAccept(sessionID: string, directory: string) { | |
| if (meta.disposed) return | |
| if (isAutoAccepting(sessionID, directory)) { | |
| disable(sessionID, directory) | |
| return | |
| } | |
| enable(sessionID, directory) | |
| }, | |
| toggleAutoAcceptDirectory(directory: string) { | |
| if (meta.disposed) return | |
| if (isAutoAcceptingDirectory(directory)) { | |
| disableDirectory(directory) | |
| return | |
| } | |
| enableDirectory(directory) | |
| }, | |
| enableAutoAccept(sessionID: string, directory: string) { | |
| if (meta.disposed) return | |
| if (isAutoAccepting(sessionID, directory)) return | |
| enable(sessionID, directory) | |
| }, | |
| disableAutoAccept(sessionID: string, directory?: string) { | |
| if (meta.disposed) return | |
| disable(sessionID, directory) | |
| }, | |
| isPermissionAllowAll(directory: string) { | |
| if (meta.disposed) return false | |
| const [childStore] = input.sync.child(directory) | |
| return childStore.config.permission === "allow" | |
| }, | |
| } | |
| return { | |
| ...api, | |
| api, | |
| sync: input.sync, | |
| enableConfiguredDirectory, | |
| permissionsEnabled(directory: string) { | |
| if (meta.disposed) return false | |
| const [childStore] = input.sync.child(directory) | |
| return hasPermissionPromptRules(childStore.config.permission) | |
| }, | |
| } | |
| } | |