feat(core): add location-based permission service (#30287)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Config } from "@/config/config"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import { Provider } from "@/provider/provider"
|
||||
@@ -36,7 +37,7 @@ export const Info = Schema.Struct({
|
||||
topP: Schema.optional(Schema.Finite),
|
||||
temperature: Schema.optional(Schema.Finite),
|
||||
color: Schema.optional(Schema.String),
|
||||
permission: Permission.Ruleset,
|
||||
permission: PermissionLegacy.Ruleset,
|
||||
model: Schema.optional(
|
||||
Schema.Struct({
|
||||
modelID: ProviderV2.ModelID,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import type { Permission } from "../permission"
|
||||
import type { Agent } from "./agent"
|
||||
|
||||
@@ -15,10 +16,10 @@ import type { Agent } from "./agent"
|
||||
* doesn't already permit them.
|
||||
*/
|
||||
export function deriveSubagentSessionPermission(input: {
|
||||
parentSessionPermission: Permission.Ruleset
|
||||
parentSessionPermission: PermissionLegacy.Ruleset
|
||||
parentAgent: Agent.Info | undefined
|
||||
subagent: Agent.Info
|
||||
}): Permission.Ruleset {
|
||||
}): PermissionLegacy.Ruleset {
|
||||
const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
|
||||
const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
|
||||
const parentAgentDenies =
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { EOL } from "os"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { basename } from "path"
|
||||
@@ -193,12 +194,12 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio
|
||||
abort: new AbortController().signal,
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
|
||||
ask(req: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">) {
|
||||
return Effect.sync(() => {
|
||||
for (const pattern of req.patterns) {
|
||||
const rule = Permission.evaluate(req.permission, pattern, ruleset)
|
||||
if (rule.action === "deny") {
|
||||
throw new Permission.DeniedError({ ruleset })
|
||||
throw new PermissionLegacy.DeniedError({ ruleset })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
// CLI entry point for `opencode run`.
|
||||
//
|
||||
// Handles three modes:
|
||||
@@ -367,7 +368,7 @@ export const RunCommand = effectCmd({
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const rules: Permission.Ruleset = args.interactive
|
||||
const rules: PermissionLegacy.Ruleset = args.interactive
|
||||
? []
|
||||
: [
|
||||
{
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { evaluate } from "@opencode-ai/core/permission"
|
||||
export { evaluate } from "."
|
||||
|
||||
@@ -1,142 +1,53 @@
|
||||
import { ConfigPermission } from "@/config/permission"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { MessageID, SessionID } from "@/session/schema"
|
||||
import { PermissionTable } from "@opencode-ai/core/session/sql"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { eq } from "drizzle-orm"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Wildcard } from "@opencode-ai/core/util/wildcard"
|
||||
import { Deferred, Effect, Layer, Schema, Context } from "effect"
|
||||
import { Deferred, Effect, Layer, Context } from "effect"
|
||||
import os from "os"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { PermissionID } from "./schema"
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
||||
const log = Log.create({ service: "permission" })
|
||||
|
||||
export const Action = PermissionV2.Action.annotate({ identifier: "PermissionAction" })
|
||||
export type Action = Schema.Schema.Type<typeof Action>
|
||||
|
||||
export const Rule = Schema.Struct({
|
||||
permission: Schema.String,
|
||||
pattern: Schema.String,
|
||||
action: Action,
|
||||
}).annotate({ identifier: "PermissionRule" })
|
||||
export type Rule = Schema.Schema.Type<typeof Rule>
|
||||
|
||||
export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionRuleset" })
|
||||
export type Ruleset = Schema.Schema.Type<typeof Ruleset>
|
||||
|
||||
// Pure data; nothing checks class identity. As `Schema.Struct` + type alias,
|
||||
// `Permission.ask` can trust its already-typed input and skip the inner
|
||||
// `decodeUnknownSync` that would otherwise throw uncaught on any structural
|
||||
// mismatch. Same pattern as `Question.Request` in PR #28570.
|
||||
export const Request = Schema.Struct({
|
||||
id: PermissionID,
|
||||
sessionID: SessionID,
|
||||
permission: Schema.String,
|
||||
patterns: Schema.Array(Schema.String),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown),
|
||||
always: Schema.Array(Schema.String),
|
||||
tool: Schema.optional(
|
||||
Schema.Struct({
|
||||
messageID: MessageID,
|
||||
callID: Schema.String,
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "PermissionRequest" })
|
||||
export type Request = Schema.Schema.Type<typeof Request>
|
||||
|
||||
export const Reply = Schema.Literals(["once", "always", "reject"])
|
||||
export type Reply = Schema.Schema.Type<typeof Reply>
|
||||
|
||||
const reply = {
|
||||
reply: Reply,
|
||||
message: Schema.optional(Schema.String),
|
||||
}
|
||||
|
||||
export const ReplyBody = Schema.Struct(reply).annotate({ identifier: "PermissionReplyBody" })
|
||||
export type ReplyBody = Schema.Schema.Type<typeof ReplyBody>
|
||||
|
||||
export const Approval = Schema.Struct({
|
||||
projectID: ProjectV2.ID,
|
||||
patterns: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "PermissionApproval" })
|
||||
export type Approval = Schema.Schema.Type<typeof Approval>
|
||||
|
||||
export const Event = {
|
||||
Asked: EventV2.define({ type: "permission.asked", schema: Request.fields }),
|
||||
Asked: EventV2.define({ type: "permission.asked", schema: PermissionLegacy.Request.fields }),
|
||||
Replied: EventV2.define({
|
||||
type: "permission.replied",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
requestID: PermissionID,
|
||||
reply: Reply,
|
||||
sessionID: PermissionLegacy.Request.fields.sessionID,
|
||||
requestID: PermissionLegacy.ID,
|
||||
reply: PermissionLegacy.Reply,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionRejectedError", {}) {
|
||||
override get message() {
|
||||
return "The user rejected permission to use this specific tool call."
|
||||
}
|
||||
}
|
||||
|
||||
export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("PermissionCorrectedError", {
|
||||
feedback: Schema.String,
|
||||
}) {
|
||||
override get message() {
|
||||
return `The user rejected permission to use this specific tool call with the following feedback: ${this.feedback}`
|
||||
}
|
||||
}
|
||||
|
||||
export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("PermissionDeniedError", {
|
||||
ruleset: Schema.Any,
|
||||
}) {
|
||||
override get message() {
|
||||
return `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(this.ruleset)}`
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Permission.NotFoundError", {
|
||||
requestID: PermissionID,
|
||||
}) {}
|
||||
|
||||
export type Error = DeniedError | RejectedError | CorrectedError
|
||||
|
||||
export const AskInput = Schema.Struct({
|
||||
...Request.fields,
|
||||
id: Schema.optional(PermissionID),
|
||||
ruleset: Ruleset,
|
||||
}).annotate({ identifier: "PermissionAskInput" })
|
||||
export type AskInput = Schema.Schema.Type<typeof AskInput>
|
||||
|
||||
export const ReplyInput = Schema.Struct({
|
||||
requestID: PermissionID,
|
||||
...reply,
|
||||
}).annotate({ identifier: "PermissionReplyInput" })
|
||||
export type ReplyInput = Schema.Schema.Type<typeof ReplyInput>
|
||||
|
||||
export interface Interface {
|
||||
readonly ask: (input: AskInput) => Effect.Effect<void, Error>
|
||||
readonly reply: (input: ReplyInput) => Effect.Effect<void, NotFoundError>
|
||||
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
|
||||
readonly ask: (input: PermissionLegacy.AskInput) => Effect.Effect<void, PermissionLegacy.Error>
|
||||
readonly reply: (input: PermissionLegacy.ReplyInput) => Effect.Effect<void, PermissionLegacy.NotFoundError>
|
||||
readonly list: () => Effect.Effect<ReadonlyArray<PermissionLegacy.Request>>
|
||||
}
|
||||
|
||||
interface PendingEntry {
|
||||
info: Request
|
||||
deferred: Deferred.Deferred<void, RejectedError | CorrectedError>
|
||||
info: PermissionLegacy.Request
|
||||
deferred: Deferred.Deferred<void, PermissionLegacy.RejectedError | PermissionLegacy.CorrectedError>
|
||||
}
|
||||
|
||||
interface State {
|
||||
pending: Map<PermissionID, PendingEntry>
|
||||
approved: Rule[]
|
||||
pending: Map<PermissionLegacy.ID, PendingEntry>
|
||||
approved: PermissionLegacy.Rule[]
|
||||
}
|
||||
|
||||
export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule {
|
||||
return PermissionV2.evaluate(permission, pattern, ...rulesets)
|
||||
export function evaluate(permission: string, pattern: string, ...rulesets: PermissionLegacy.Ruleset[]): PermissionLegacy.Rule {
|
||||
return (
|
||||
rulesets
|
||||
.flat()
|
||||
.findLast((rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)) ?? {
|
||||
action: "ask",
|
||||
permission,
|
||||
pattern: "*",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Permission") {}
|
||||
@@ -145,24 +56,18 @@ export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const { db } = yield* Database.Service
|
||||
const state = yield* InstanceState.make<State>(
|
||||
Effect.fn("Permission.state")(function* (ctx) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(PermissionTable)
|
||||
.where(eq(PermissionTable.project_id, ctx.project.id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
void ctx
|
||||
const state = {
|
||||
pending: new Map<PermissionID, PendingEntry>(),
|
||||
approved: [...(row?.data ?? [])],
|
||||
pending: new Map<PermissionLegacy.ID, PendingEntry>(),
|
||||
approved: [],
|
||||
}
|
||||
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.gen(function* () {
|
||||
for (const item of state.pending.values()) {
|
||||
yield* Deferred.fail(item.deferred, new RejectedError())
|
||||
yield* Deferred.fail(item.deferred, new PermissionLegacy.RejectedError())
|
||||
}
|
||||
state.pending.clear()
|
||||
}),
|
||||
@@ -172,7 +77,7 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
const ask = Effect.fn("Permission.ask")(function* (input: AskInput) {
|
||||
const ask = Effect.fn("Permission.ask")(function* (input: PermissionLegacy.AskInput) {
|
||||
const { approved, pending } = yield* InstanceState.get(state)
|
||||
const { ruleset, ...request } = input
|
||||
let needsAsk = false
|
||||
@@ -181,7 +86,7 @@ export const layer = Layer.effect(
|
||||
const rule = evaluate(request.permission, pattern, ruleset, approved)
|
||||
log.info("evaluated", { permission: request.permission, pattern, action: rule })
|
||||
if (rule.action === "deny") {
|
||||
return yield* new DeniedError({
|
||||
return yield* new PermissionLegacy.DeniedError({
|
||||
ruleset: ruleset.filter((rule) => Wildcard.match(request.permission, rule.permission)),
|
||||
})
|
||||
}
|
||||
@@ -191,8 +96,8 @@ export const layer = Layer.effect(
|
||||
|
||||
if (!needsAsk) return
|
||||
|
||||
const id = request.id ?? PermissionID.ascending()
|
||||
const info: Request = {
|
||||
const id = request.id ?? PermissionLegacy.ID.ascending()
|
||||
const info: PermissionLegacy.Request = {
|
||||
id,
|
||||
sessionID: request.sessionID,
|
||||
permission: request.permission,
|
||||
@@ -203,7 +108,7 @@ export const layer = Layer.effect(
|
||||
}
|
||||
log.info("asking", { id, permission: info.permission, patterns: info.patterns })
|
||||
|
||||
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
|
||||
const deferred = yield* Deferred.make<void, PermissionLegacy.RejectedError | PermissionLegacy.CorrectedError>()
|
||||
pending.set(id, { info, deferred })
|
||||
yield* events.publish(Event.Asked, info)
|
||||
return yield* Effect.ensuring(
|
||||
@@ -214,10 +119,10 @@ export const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
const reply = Effect.fn("Permission.reply")(function* (input: ReplyInput) {
|
||||
const reply = Effect.fn("Permission.reply")(function* (input: PermissionLegacy.ReplyInput) {
|
||||
const { approved, pending } = yield* InstanceState.get(state)
|
||||
const existing = pending.get(input.requestID)
|
||||
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
|
||||
if (!existing) return yield* new PermissionLegacy.NotFoundError({ requestID: input.requestID })
|
||||
|
||||
pending.delete(input.requestID)
|
||||
yield* events.publish(Event.Replied, {
|
||||
@@ -229,7 +134,7 @@ export const layer = Layer.effect(
|
||||
if (input.reply === "reject") {
|
||||
yield* Deferred.fail(
|
||||
existing.deferred,
|
||||
input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(),
|
||||
input.message ? new PermissionLegacy.CorrectedError({ feedback: input.message }) : new PermissionLegacy.RejectedError(),
|
||||
)
|
||||
|
||||
for (const [id, item] of pending.entries()) {
|
||||
@@ -240,7 +145,7 @@ export const layer = Layer.effect(
|
||||
requestID: item.info.id,
|
||||
reply: "reject",
|
||||
})
|
||||
yield* Deferred.fail(item.deferred, new RejectedError())
|
||||
yield* Deferred.fail(item.deferred, new PermissionLegacy.RejectedError())
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -290,7 +195,7 @@ function expand(pattern: string): string {
|
||||
}
|
||||
|
||||
export function fromConfig(permission: ConfigPermission.Info) {
|
||||
const ruleset: Rule[] = []
|
||||
const ruleset: PermissionLegacy.Rule[] = []
|
||||
for (const [key, value] of Object.entries(permission)) {
|
||||
if (typeof value === "string") {
|
||||
ruleset.push({ permission: key, action: value, pattern: "*" })
|
||||
@@ -303,14 +208,21 @@ export function fromConfig(permission: ConfigPermission.Info) {
|
||||
return ruleset
|
||||
}
|
||||
|
||||
export function merge(...rulesets: Ruleset[]): Rule[] {
|
||||
return [...PermissionV2.merge(...rulesets)]
|
||||
export function merge(...rulesets: PermissionLegacy.Ruleset[]): PermissionLegacy.Rule[] {
|
||||
return rulesets.flat()
|
||||
}
|
||||
|
||||
export function disabled(tools: string[], ruleset: Ruleset): Set<string> {
|
||||
return PermissionV2.disabled(tools, ruleset)
|
||||
export function disabled(tools: string[], ruleset: PermissionLegacy.Ruleset): Set<string> {
|
||||
const edits = ["edit", "write", "apply_patch"]
|
||||
return new Set(
|
||||
tools.filter((tool) => {
|
||||
const permission = edits.includes(tool) ? "edit" : tool
|
||||
const rule = ruleset.findLast((rule) => Wildcard.match(permission, rule.permission))
|
||||
return rule?.pattern === "*" && rule.action === "deny"
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer))
|
||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
|
||||
|
||||
export * as Permission from "."
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { Newtype } from "@opencode-ai/core/schema"
|
||||
|
||||
export class PermissionID extends Newtype<PermissionID>()(
|
||||
"PermissionID",
|
||||
Schema.String.check(Schema.isStartsWith("per")),
|
||||
) {
|
||||
static ascending(id?: string): PermissionID {
|
||||
return this.make(Identifier.ascending("permission", id))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { and, eq, sql } from "drizzle-orm"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { PermissionTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
@@ -86,10 +86,6 @@ export function fromRow(row: Row): Info {
|
||||
}
|
||||
}
|
||||
|
||||
function mergePermissionRules<T extends readonly unknown[]>(oldRules: T, newRules: T): T {
|
||||
return [...new Map([...oldRules, ...newRules].map((rule) => [JSON.stringify(rule), rule])).values()] as unknown as T
|
||||
}
|
||||
|
||||
export const UpdateInput = Schema.Struct({
|
||||
projectID: ProjectV2.ID,
|
||||
name: Schema.optional(Schema.String),
|
||||
@@ -201,36 +197,6 @@ export const layer = Layer.effect(
|
||||
.run()
|
||||
}
|
||||
|
||||
const oldPermission = yield* d
|
||||
.select()
|
||||
.from(PermissionTable)
|
||||
.where(eq(PermissionTable.project_id, oldID))
|
||||
.get()
|
||||
const newPermission = yield* d
|
||||
.select()
|
||||
.from(PermissionTable)
|
||||
.where(eq(PermissionTable.project_id, newID))
|
||||
.get()
|
||||
if (oldPermission && newPermission) {
|
||||
yield* d
|
||||
.update(PermissionTable)
|
||||
.set({
|
||||
data: mergePermissionRules(oldPermission.data, newPermission.data),
|
||||
time_created: Math.min(oldPermission.time_created, newPermission.time_created),
|
||||
time_updated: Date.now(),
|
||||
})
|
||||
.where(eq(PermissionTable.project_id, newID))
|
||||
.run()
|
||||
yield* d.delete(PermissionTable).where(eq(PermissionTable.project_id, oldID)).run()
|
||||
}
|
||||
if (oldPermission && !newPermission) {
|
||||
yield* d
|
||||
.update(PermissionTable)
|
||||
.set({ project_id: newID })
|
||||
.where(eq(PermissionTable.project_id, oldID))
|
||||
.run()
|
||||
}
|
||||
|
||||
yield* d
|
||||
.update(SessionTable)
|
||||
.set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Permission } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { PermissionNotFoundError } from "../errors"
|
||||
@@ -10,7 +10,7 @@ import { described } from "./metadata"
|
||||
|
||||
const root = "/permission"
|
||||
const ReplyPayload = Schema.Struct({
|
||||
reply: Permission.Reply,
|
||||
reply: PermissionLegacy.Reply,
|
||||
message: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ export const PermissionApi = HttpApi.make("permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", root, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Permission.Request), "List of pending permissions"),
|
||||
success: described(Schema.Array(PermissionLegacy.Request), "List of pending permissions"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "permission.list",
|
||||
@@ -29,7 +29,7 @@ export const PermissionApi = HttpApi.make("permission")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
||||
params: { requestID: PermissionID },
|
||||
params: { requestID: PermissionLegacy.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ReplyPayload,
|
||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Permission } from "@/permission"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
@@ -48,7 +48,7 @@ export const StatusMap = Schema.Record(Schema.String, SessionStatus.Info)
|
||||
export const UpdatePayload = Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Session.Metadata),
|
||||
permission: Schema.optional(Permission.Ruleset),
|
||||
permission: Schema.optional(PermissionLegacy.Ruleset),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
archived: Schema.optional(Session.ArchivedTimestamp),
|
||||
@@ -71,7 +71,7 @@ export const CommandPayload = Schema.Struct(Struct.omit(SessionPrompt.CommandInp
|
||||
export const ShellPayload = Schema.Struct(Struct.omit(SessionPrompt.ShellInput.fields, ["sessionID"]))
|
||||
export const RevertPayload = Schema.Struct(Struct.omit(SessionRevert.RevertInput.fields, ["sessionID"]))
|
||||
export const PermissionResponsePayload = Schema.Struct({
|
||||
response: Permission.Reply,
|
||||
response: PermissionLegacy.Reply,
|
||||
})
|
||||
|
||||
export const SessionPaths = {
|
||||
@@ -392,7 +392,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
|
||||
params: { sessionID: SessionID, permissionID: PermissionID },
|
||||
params: { sessionID: SessionID, permissionID: PermissionLegacy.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: PermissionResponsePayload,
|
||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||
|
||||
@@ -3,12 +3,16 @@ import { MessageGroup } from "./v2/message"
|
||||
import { ModelGroup } from "./v2/model"
|
||||
import { ProviderGroup } from "./v2/provider"
|
||||
import { SessionGroup } from "./v2/session"
|
||||
import { PermissionGroup, PermissionSavedGroup, SessionPermissionGroup } from "./v2/permission"
|
||||
|
||||
export const V2Api = HttpApi.make("v2")
|
||||
.add(SessionGroup)
|
||||
.add(MessageGroup)
|
||||
.add(ModelGroup)
|
||||
.add(ProviderGroup)
|
||||
.add(PermissionGroup)
|
||||
.add(SessionPermissionGroup)
|
||||
.add(PermissionSavedGroup)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
@@ -34,7 +35,7 @@ export const locationQueryOpenApi = OpenApi.annotations({
|
||||
export class V2LocationMiddleware extends HttpApiMiddleware.Service<
|
||||
V2LocationMiddleware,
|
||||
{
|
||||
provides: Catalog.Service | PluginBoot.Service
|
||||
provides: Catalog.Service | PluginBoot.Service | PermissionV2.Service
|
||||
}
|
||||
>()("@opencode/ExperimentalHttpApiV2Location") {}
|
||||
|
||||
@@ -59,4 +60,4 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
}),
|
||||
).pipe(Layer.provide(LocationServiceMap.layer))
|
||||
)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
export const PermissionGroup = HttpApiGroup.make("v2.permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("permissionRequests", "/api/permission/request", {
|
||||
query: LocationQuery,
|
||||
success: Schema.Array(PermissionV2.Request),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.request.list",
|
||||
summary: "List pending permission requests",
|
||||
description: "Retrieve pending permission requests for a location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "v2 permissions", description: "Experimental v2 permission routes." }))
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
|
||||
export const SessionPermissionGroup = HttpApiGroup.make("v2.session.permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("sessionPermissionRequests", "/api/session/:sessionID/permission/request", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: Schema.Array(PermissionV2.Request),
|
||||
error: SessionNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.permission.list",
|
||||
summary: "List session permission requests",
|
||||
description: "Retrieve pending permission requests owned by a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("permissionRequestReply", "/api/session/:sessionID/permission/request/:requestID/reply", {
|
||||
params: { sessionID: SessionV2.ID, requestID: PermissionV2.ID },
|
||||
payload: Schema.Struct({
|
||||
reply: PermissionV2.Reply,
|
||||
message: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, PermissionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.permission.reply",
|
||||
summary: "Reply to pending permission request",
|
||||
description: "Respond to a pending permission request owned by a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "v2 session permissions", description: "Experimental v2 session permission routes." }))
|
||||
.middleware(V2Authorization)
|
||||
|
||||
export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
|
||||
.add(
|
||||
HttpApiEndpoint.get("savedPermissions", "/api/permission/saved", {
|
||||
query: Schema.Struct({ projectID: ProjectV2.ID.pipe(Schema.optional) }),
|
||||
success: Schema.Array(PermissionSaved.Info),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.saved.list",
|
||||
summary: "List saved permissions",
|
||||
description: "Retrieve saved permissions, optionally filtered by project.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("removeSavedPermission", "/api/permission/saved/:id", {
|
||||
params: { id: PermissionSaved.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.saved.remove",
|
||||
summary: "Remove saved permission",
|
||||
description: "Remove a saved permission by ID.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "v2 saved permissions", description: "Experimental v2 saved permission routes." }))
|
||||
.middleware(V2Authorization)
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Permission } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../api"
|
||||
@@ -14,8 +14,8 @@ export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permiss
|
||||
})
|
||||
|
||||
const reply = Effect.fn("PermissionHttpApi.reply")(function* (ctx: {
|
||||
params: { requestID: PermissionID }
|
||||
payload: Permission.ReplyBody
|
||||
params: { requestID: PermissionLegacy.ID }
|
||||
payload: PermissionLegacy.ReplyBody
|
||||
}) {
|
||||
yield* svc
|
||||
.reply({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { Command } from "@/command"
|
||||
import { Permission } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionCompaction } from "@/session/compaction"
|
||||
@@ -360,7 +360,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
})
|
||||
|
||||
const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
|
||||
params: { sessionID: SessionID; permissionID: PermissionID }
|
||||
params: { sessionID: SessionID; permissionID: PermissionLegacy.ID }
|
||||
payload: typeof PermissionResponsePayload.Type
|
||||
}) {
|
||||
yield* requireSession(ctx.params.sessionID)
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { Layer } from "effect"
|
||||
import { layer as v2LocationLayer } from "../groups/v2/location"
|
||||
import { messageHandlers } from "./v2/message"
|
||||
import { modelHandlers } from "./v2/model"
|
||||
import { providerHandlers } from "./v2/provider"
|
||||
import { sessionHandlers } from "./v2/session"
|
||||
import { permissionHandlers, savedPermissionHandlers, sessionPermissionHandlers } from "./v2/permission"
|
||||
|
||||
export const v2Handlers = Layer.mergeAll(sessionHandlers, messageHandlers, modelHandlers, providerHandlers).pipe(
|
||||
export const v2Handlers = Layer.mergeAll(
|
||||
sessionHandlers,
|
||||
messageHandlers,
|
||||
modelHandlers,
|
||||
providerHandlers,
|
||||
permissionHandlers,
|
||||
sessionPermissionHandlers,
|
||||
savedPermissionHandlers,
|
||||
).pipe(
|
||||
Layer.provide(v2LocationLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(PermissionSaved.layer),
|
||||
Layer.provide(SessionV2.defaultLayer),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../../api"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
|
||||
function missingRequest(id: PermissionV2.ID) {
|
||||
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
|
||||
}
|
||||
|
||||
export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.permission", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers.handle(
|
||||
"permissionRequests",
|
||||
Effect.fn(function* () {
|
||||
return yield* (yield* PermissionV2.Service).list()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
export const sessionPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session.permission", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
|
||||
const withSessionPermission = Effect.fnUntraced(function* <A, E>(
|
||||
sessionID: Parameters<PermissionV2.Interface["forSession"]>[0],
|
||||
use: (permission: PermissionV2.Interface) => Effect.Effect<A, E>,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row)
|
||||
return yield* new SessionNotFoundError({
|
||||
sessionID,
|
||||
message: `Session not found: ${sessionID}`,
|
||||
})
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
return yield* use(yield* PermissionV2.Service)
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(
|
||||
locations.get({ directory: AbsolutePath.make(row.directory), workspaceID: row.workspaceID ?? undefined }),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return handlers
|
||||
.handle(
|
||||
"sessionPermissionRequests",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* withSessionPermission(ctx.params.sessionID, (permission) =>
|
||||
permission.forSession(ctx.params.sessionID),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"permissionRequestReply",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* withSessionPermission(ctx.params.sessionID, (permission) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* permission.get(ctx.params.requestID)
|
||||
if (!request || request.sessionID !== ctx.params.sessionID)
|
||||
return yield* missingRequest(ctx.params.requestID)
|
||||
yield* permission
|
||||
.reply({ requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message })
|
||||
.pipe(Effect.catchTag("PermissionV2.NotFoundError", () => missingRequest(ctx.params.requestID)))
|
||||
}),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
export const savedPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.permission.saved", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const saved = yield* PermissionSaved.Service
|
||||
return handlers
|
||||
.handle(
|
||||
"savedPermissions",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* saved.list({ projectID: ctx.query.projectID })
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"removeSavedPermission",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* saved.remove(ctx.params.id)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
@@ -15,7 +16,6 @@ import type { Agent } from "@/agent/agent"
|
||||
import type { MessageV2 } from "./message-v2"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Permission } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Wildcard } from "@/util/wildcard"
|
||||
@@ -38,7 +38,7 @@ export type StreamInput = {
|
||||
parentSessionID?: string
|
||||
model: Provider.Model
|
||||
agent: Agent.Info
|
||||
permission?: Permission.Ruleset
|
||||
permission?: PermissionLegacy.Ruleset
|
||||
system: string[]
|
||||
messages: ModelMessage[]
|
||||
small?: boolean
|
||||
@@ -165,7 +165,7 @@ const live: Layer.Layer<
|
||||
return { approved: true }
|
||||
}
|
||||
|
||||
const id = PermissionID.ascending()
|
||||
const id = PermissionLegacy.ID.ascending()
|
||||
let unsub: EventV2.Unsubscribe | undefined
|
||||
try {
|
||||
unsub = await bridge.promise(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import type { Auth } from "@/auth"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import type { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
@@ -22,7 +23,7 @@ type PrepareInput = {
|
||||
readonly parentSessionID?: string
|
||||
readonly model: Provider.Model
|
||||
readonly agent: Agent.Info
|
||||
readonly permission?: Permission.Ruleset
|
||||
readonly permission?: PermissionLegacy.Ruleset
|
||||
readonly system: string[]
|
||||
readonly messages: ModelMessage[]
|
||||
readonly small?: boolean
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Image } from "@/image/image"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Cause, Deferred, Effect, Exit, Layer, Context, Scope, Schema } from "effect"
|
||||
@@ -204,7 +205,7 @@ export const layer = Layer.effect(
|
||||
time: { start: match.part.state.time.start, end: Date.now() },
|
||||
},
|
||||
})
|
||||
if (error instanceof Permission.RejectedError || error instanceof Question.RejectedError) {
|
||||
if (error instanceof PermissionLegacy.RejectedError || error instanceof Question.RejectedError) {
|
||||
ctx.blocked = ctx.shouldBreak
|
||||
}
|
||||
yield* settleToolCall(toolCallID)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import path from "path"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import os from "os"
|
||||
@@ -1220,7 +1221,7 @@ export const layer = Layer.effect(
|
||||
const message = yield* createUserMessage(input)
|
||||
yield* sessions.touch(input.sessionID)
|
||||
|
||||
const permissions: Permission.Rule[] = []
|
||||
const permissions: PermissionLegacy.Rule[] = []
|
||||
for (const [t, enabled] of Object.entries(input.tools ?? {})) {
|
||||
permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
@@ -233,7 +234,7 @@ export const Info = Schema.Struct({
|
||||
version: Schema.String,
|
||||
metadata: optionalOmitUndefined(Metadata),
|
||||
time: Time,
|
||||
permission: optionalOmitUndefined(Permission.Ruleset),
|
||||
permission: optionalOmitUndefined(PermissionLegacy.Ruleset),
|
||||
revert: optionalOmitUndefined(Revert),
|
||||
}).annotate({ identifier: "Session" })
|
||||
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
@@ -258,7 +259,7 @@ export const CreateInput = Schema.optional(
|
||||
agent: Schema.optional(Schema.String),
|
||||
model: Schema.optional(Model),
|
||||
metadata: Schema.optional(Metadata),
|
||||
permission: Schema.optional(Permission.Ruleset),
|
||||
permission: Schema.optional(PermissionLegacy.Ruleset),
|
||||
workspaceID: Schema.optional(WorkspaceV2.ID),
|
||||
}),
|
||||
)
|
||||
@@ -282,7 +283,7 @@ export const SetMetadataInput = Schema.Struct({
|
||||
})
|
||||
export const SetPermissionInput = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
permission: Permission.Ruleset,
|
||||
permission: PermissionLegacy.Ruleset,
|
||||
})
|
||||
export const SetRevertInput = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
@@ -348,7 +349,7 @@ const UpdatedInfo = Schema.Struct({
|
||||
version: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
metadata: Schema.optional(Schema.NullOr(Metadata)),
|
||||
time: Schema.optional(UpdatedTime),
|
||||
permission: Schema.optional(Schema.NullOr(Permission.Ruleset)),
|
||||
permission: Schema.optional(Schema.NullOr(PermissionLegacy.Ruleset)),
|
||||
revert: Schema.optional(Schema.NullOr(Revert)),
|
||||
})
|
||||
|
||||
@@ -472,7 +473,7 @@ export interface Interface {
|
||||
agent?: string
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
metadata?: typeof Metadata.Type
|
||||
permission?: Permission.Ruleset
|
||||
permission?: PermissionLegacy.Ruleset
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
}) => Effect.Effect<Info>
|
||||
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info, NotFound>
|
||||
@@ -481,7 +482,7 @@ export interface Interface {
|
||||
readonly setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect<void>
|
||||
readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect<void>
|
||||
readonly setMetadata: (input: typeof SetMetadataInput.Type) => Effect.Effect<void>
|
||||
readonly setPermission: (input: { sessionID: SessionID; permission: Permission.Ruleset }) => Effect.Effect<void>
|
||||
readonly setPermission: (input: { sessionID: SessionID; permission: PermissionLegacy.Ruleset }) => Effect.Effect<void>
|
||||
readonly setRevert: (input: {
|
||||
sessionID: SessionID
|
||||
revert: Info["revert"]
|
||||
@@ -570,7 +571,7 @@ export const layer: Layer.Layer<
|
||||
directory: string
|
||||
path?: string
|
||||
metadata?: typeof Metadata.Type
|
||||
permission?: Permission.Ruleset
|
||||
permission?: PermissionLegacy.Ruleset
|
||||
}) {
|
||||
const ctx = yield* InstanceState.context
|
||||
const result: Info = {
|
||||
@@ -748,7 +749,7 @@ export const layer: Layer.Layer<
|
||||
agent?: string
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
metadata?: typeof Metadata.Type
|
||||
permission?: Permission.Ruleset
|
||||
permission?: PermissionLegacy.Ruleset
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
}) {
|
||||
const ctx = yield* InstanceState.context
|
||||
@@ -842,7 +843,7 @@ export const layer: Layer.Layer<
|
||||
|
||||
const setPermission = Effect.fn("Session.setPermission")(function* (input: {
|
||||
sessionID: SessionID
|
||||
permission: Permission.Ruleset
|
||||
permission: PermissionLegacy.Ruleset
|
||||
}) {
|
||||
yield* patch(input.sessionID, { permission: [...input.permission], time: { updated: Date.now() } }).pipe(
|
||||
Effect.orDie,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionTable, MessageTable, PartTable, TodoTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionShareTable } from "@opencode-ai/core/share/sql"
|
||||
import path from "path"
|
||||
import { existsSync } from "fs"
|
||||
@@ -108,13 +108,12 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
|
||||
// Pre-scan all files upfront to avoid repeated glob operations
|
||||
log.info("scanning files...")
|
||||
const [projectFiles, sessionFiles, messageFiles, partFiles, todoFiles, permFiles, shareFiles] = await Promise.all([
|
||||
const [projectFiles, sessionFiles, messageFiles, partFiles, todoFiles, shareFiles] = await Promise.all([
|
||||
list("project/*.json"),
|
||||
list("session/*/*.json"),
|
||||
list("message/*/*.json"),
|
||||
list("part/*/*.json"),
|
||||
list("todo/*.json"),
|
||||
list("permission/*.json"),
|
||||
list("session_share/*.json"),
|
||||
])
|
||||
|
||||
@@ -124,7 +123,6 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
messages: messageFiles.length,
|
||||
parts: partFiles.length,
|
||||
todos: todoFiles.length,
|
||||
permissions: permFiles.length,
|
||||
shares: shareFiles.length,
|
||||
})
|
||||
|
||||
@@ -135,7 +133,6 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
messageFiles.length +
|
||||
partFiles.length +
|
||||
todoFiles.length +
|
||||
permFiles.length +
|
||||
shareFiles.length,
|
||||
)
|
||||
const progress = options?.progress
|
||||
@@ -357,31 +354,6 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
|
||||
log.warn("skipped orphaned todos", { count: orphans.todos })
|
||||
}
|
||||
|
||||
// Migrate permissions
|
||||
const permProjects = permFiles.map((file) => path.basename(file, ".json"))
|
||||
const permValues: unknown[] = []
|
||||
for (let i = 0; i < permFiles.length; i += batchSize) {
|
||||
const end = Math.min(i + batchSize, permFiles.length)
|
||||
const batch = await read(permFiles, i, end)
|
||||
permValues.length = 0
|
||||
for (let j = 0; j < batch.length; j++) {
|
||||
const data = batch[j]
|
||||
if (!data) continue
|
||||
const projectID = permProjects[i + j]
|
||||
if (!projectIds.has(projectID)) {
|
||||
orphans.permissions++
|
||||
continue
|
||||
}
|
||||
permValues.push({ project_id: projectID, data })
|
||||
}
|
||||
stats.permissions += insert(permValues, PermissionTable, "permission")
|
||||
step("permissions", end - i)
|
||||
}
|
||||
log.info("migrated permissions", { count: stats.permissions })
|
||||
if (orphans.permissions > 0) {
|
||||
log.warn("skipped orphaned permissions", { count: orphans.permissions })
|
||||
}
|
||||
|
||||
// Migrate session shares
|
||||
const shareSessions = shareFiles.map((file) => path.basename(file, ".json"))
|
||||
const shareValues: unknown[] = []
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { AccountTable, AccountStateTable, ControlAccountTable } from "@opencode-ai/core/account/sql"
|
||||
export { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
export { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "@opencode-ai/core/session/sql"
|
||||
export { SessionTable, MessageTable, PartTable, TodoTable } from "@opencode-ai/core/session/sql"
|
||||
export { SessionShareTable } from "@opencode-ai/core/share/sql"
|
||||
export { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||
@@ -41,7 +42,7 @@ export type Context<M extends Metadata = Metadata> = {
|
||||
extra?: { [key: string]: unknown }
|
||||
messages: SessionLegacy.WithParts[]
|
||||
metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
|
||||
ask(input: Omit<Permission.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
|
||||
ask(input: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface ExecuteResult<M extends Metadata = Metadata> {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Config } from "../../src/config/config"
|
||||
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { Plugin } from "../../src/plugin"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { Skill } from "../../src/skill"
|
||||
@@ -28,7 +29,7 @@ const it = testEffect(agentLayer())
|
||||
const scout = testEffect(agentLayer({ experimentalScout: true }))
|
||||
|
||||
// Helper to evaluate permission for a tool with wildcard pattern
|
||||
function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
|
||||
function evalPerm(agent: Agent.Info | undefined, permission: string): PermissionLegacy.Action | undefined {
|
||||
if (!agent) return undefined
|
||||
return Permission.evaluate(permission, "*", agent.permission).action
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
/**
|
||||
* Reproducer for opencode issue #26514:
|
||||
*
|
||||
@@ -60,7 +61,7 @@ it.instance("[#26514] subagent spawned from plan mode inherits read-only restric
|
||||
// session's `permission` field is empty (Plan Mode lives on the agent
|
||||
// ruleset, not the session). So we pass [] through as the parent
|
||||
// session permission, exactly like the actual code path.
|
||||
const parentSessionPermission: Permission.Ruleset = []
|
||||
const parentSessionPermission: PermissionLegacy.Ruleset = []
|
||||
|
||||
const subagentSessionPermission = deriveSubagentSessionPermission({
|
||||
parentSessionPermission,
|
||||
@@ -88,7 +89,7 @@ it.instance("[#26514] explore subagent launched from plan mode also stays read-o
|
||||
expect(planAgent).toBeDefined()
|
||||
expect(explore).toBeDefined()
|
||||
|
||||
const parentSessionPermission: Permission.Ruleset = []
|
||||
const parentSessionPermission: PermissionLegacy.Ruleset = []
|
||||
const subagentSessionPermission = deriveSubagentSessionPermission({
|
||||
parentSessionPermission,
|
||||
parentAgent: planAgent,
|
||||
@@ -113,7 +114,7 @@ it.instance(
|
||||
expect(planAgent).toBeDefined()
|
||||
expect(my).toBeDefined()
|
||||
|
||||
const parentSessionPermission: Permission.Ruleset = []
|
||||
const parentSessionPermission: PermissionLegacy.Ruleset = []
|
||||
const subagentSessionPermission = deriveSubagentSessionPermission({
|
||||
parentSessionPermission,
|
||||
parentAgent: planAgent,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Permission } from "../src/permission"
|
||||
@@ -9,7 +10,7 @@ const it = testEffect(Config.defaultLayer)
|
||||
const load = Config.use.get()
|
||||
|
||||
describe("Permission.evaluate for permission.task", () => {
|
||||
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): Permission.Ruleset =>
|
||||
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionLegacy.Ruleset =>
|
||||
Object.entries(rules).map(([pattern, action]) => ({
|
||||
permission: "task",
|
||||
pattern,
|
||||
@@ -75,7 +76,7 @@ describe("Permission.disabled for task tool", () => {
|
||||
// Note: The `disabled` function checks if a TOOL should be completely removed from the tool list.
|
||||
// It only disables a tool when there's a rule with `pattern: "*"` and `action: "deny"`.
|
||||
// It does NOT evaluate complex subagent patterns - those are handled at runtime by `evaluate`.
|
||||
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): Permission.Ruleset =>
|
||||
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionLegacy.Ruleset =>
|
||||
Object.entries(rules).map(([pattern, action]) => ({
|
||||
permission: "task",
|
||||
pattern,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { test, expect } from "bun:test"
|
||||
import os from "os"
|
||||
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
|
||||
@@ -5,7 +6,6 @@ import { EventV2Bridge } from "../../src/event-v2-bridge"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { PermissionID } from "../../src/permission/schema"
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { TestInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
@@ -261,8 +261,8 @@ test("merge - preserves rule order", () => {
|
||||
})
|
||||
|
||||
test("merge - config permission overrides default ask", () => {
|
||||
const defaults: Permission.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }]
|
||||
const config: Permission.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
|
||||
const defaults: PermissionLegacy.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }]
|
||||
const config: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
|
||||
const merged = Permission.merge(defaults, config)
|
||||
|
||||
expect(Permission.evaluate("bash", "ls", merged).action).toBe("allow")
|
||||
@@ -270,8 +270,8 @@ test("merge - config permission overrides default ask", () => {
|
||||
})
|
||||
|
||||
test("merge - config ask overrides default allow", () => {
|
||||
const defaults: Permission.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
|
||||
const config: Permission.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }]
|
||||
const defaults: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
|
||||
const config: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }]
|
||||
const merged = Permission.merge(defaults, config)
|
||||
|
||||
expect(Permission.evaluate("bash", "ls", merged).action).toBe("ask")
|
||||
@@ -443,8 +443,8 @@ test("evaluate - later wildcard permission can override earlier specific permiss
|
||||
})
|
||||
|
||||
test("evaluate - merges multiple rulesets", () => {
|
||||
const config: Permission.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
|
||||
const approved: Permission.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }]
|
||||
const config: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
|
||||
const approved: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }]
|
||||
const result = Permission.evaluate("bash", "rm", config, approved)
|
||||
expect(result.action).toBe("deny")
|
||||
})
|
||||
@@ -588,7 +588,7 @@ it.instance(
|
||||
ruleset: [{ permission: "bash", pattern: "*", action: "deny" }],
|
||||
}),
|
||||
)
|
||||
expect(err).toBeInstanceOf(Permission.DeniedError)
|
||||
expect(err).toBeInstanceOf(PermissionLegacy.DeniedError)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -655,10 +655,10 @@ it.instance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const seen = yield* Deferred.make<Permission.Request>()
|
||||
const seen = yield* Deferred.make<PermissionLegacy.Request>()
|
||||
const unsub = yield* events.listen((event) => {
|
||||
if (event.type === Permission.Event.Asked.type)
|
||||
Deferred.doneUnsafe(seen, Effect.succeed(event.data as Permission.Request))
|
||||
Deferred.doneUnsafe(seen, Effect.succeed(event.data as PermissionLegacy.Request))
|
||||
return Effect.void
|
||||
})
|
||||
yield* Effect.addFinalizer(() => unsub)
|
||||
@@ -703,7 +703,7 @@ it.instance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const fiber = yield* ask({
|
||||
id: PermissionID.make("per_test1"),
|
||||
id: PermissionLegacy.ID.make("per_test1"),
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -713,7 +713,7 @@ it.instance(
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
yield* waitForPending(1)
|
||||
yield* reply({ requestID: PermissionID.make("per_test1"), reply: "once" })
|
||||
yield* reply({ requestID: PermissionLegacy.ID.make("per_test1"), reply: "once" })
|
||||
yield* Fiber.join(fiber)
|
||||
}),
|
||||
{ git: true },
|
||||
@@ -724,7 +724,7 @@ it.instance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const fiber = yield* ask({
|
||||
id: PermissionID.make("per_test2"),
|
||||
id: PermissionLegacy.ID.make("per_test2"),
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -734,11 +734,11 @@ it.instance(
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
yield* waitForPending(1)
|
||||
yield* reply({ requestID: PermissionID.make("per_test2"), reply: "reject" })
|
||||
yield* reply({ requestID: PermissionLegacy.ID.make("per_test2"), reply: "reject" })
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -748,7 +748,7 @@ it.instance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const fiber = yield* ask({
|
||||
id: PermissionID.make("per_test2b"),
|
||||
id: PermissionLegacy.ID.make("per_test2b"),
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -759,7 +759,7 @@ it.instance(
|
||||
|
||||
yield* waitForPending(1)
|
||||
yield* reply({
|
||||
requestID: PermissionID.make("per_test2b"),
|
||||
requestID: PermissionLegacy.ID.make("per_test2b"),
|
||||
reply: "reject",
|
||||
message: "Use a safer command",
|
||||
})
|
||||
@@ -768,7 +768,7 @@ it.instance(
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) {
|
||||
const err = Cause.squash(exit.cause)
|
||||
expect(err).toBeInstanceOf(Permission.CorrectedError)
|
||||
expect(err).toBeInstanceOf(PermissionLegacy.CorrectedError)
|
||||
expect(String(err)).toContain("Use a safer command")
|
||||
}
|
||||
}),
|
||||
@@ -780,7 +780,7 @@ it.instance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const fiber = yield* ask({
|
||||
id: PermissionID.make("per_test3"),
|
||||
id: PermissionLegacy.ID.make("per_test3"),
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -790,7 +790,7 @@ it.instance(
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
yield* waitForPending(1)
|
||||
yield* reply({ requestID: PermissionID.make("per_test3"), reply: "always" })
|
||||
yield* reply({ requestID: PermissionLegacy.ID.make("per_test3"), reply: "always" })
|
||||
yield* Fiber.join(fiber)
|
||||
|
||||
const result = yield* ask({
|
||||
@@ -811,7 +811,7 @@ it.instance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const a = yield* ask({
|
||||
id: PermissionID.make("per_test4a"),
|
||||
id: PermissionLegacy.ID.make("per_test4a"),
|
||||
sessionID: SessionID.make("session_same"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -821,7 +821,7 @@ it.instance(
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
const b = yield* ask({
|
||||
id: PermissionID.make("per_test4b"),
|
||||
id: PermissionLegacy.ID.make("per_test4b"),
|
||||
sessionID: SessionID.make("session_same"),
|
||||
permission: "edit",
|
||||
patterns: ["foo.ts"],
|
||||
@@ -831,13 +831,13 @@ it.instance(
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
yield* waitForPending(2)
|
||||
yield* reply({ requestID: PermissionID.make("per_test4a"), reply: "reject" })
|
||||
yield* reply({ requestID: PermissionLegacy.ID.make("per_test4a"), reply: "reject" })
|
||||
|
||||
const [ea, eb] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
|
||||
expect(Exit.isFailure(ea)).toBe(true)
|
||||
expect(Exit.isFailure(eb)).toBe(true)
|
||||
if (Exit.isFailure(ea)) expect(Cause.squash(ea.cause)).toBeInstanceOf(Permission.RejectedError)
|
||||
if (Exit.isFailure(eb)) expect(Cause.squash(eb.cause)).toBeInstanceOf(Permission.RejectedError)
|
||||
if (Exit.isFailure(ea)) expect(Cause.squash(ea.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
|
||||
if (Exit.isFailure(eb)) expect(Cause.squash(eb.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -847,7 +847,7 @@ it.instance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const a = yield* ask({
|
||||
id: PermissionID.make("per_test5a"),
|
||||
id: PermissionLegacy.ID.make("per_test5a"),
|
||||
sessionID: SessionID.make("session_same"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -857,7 +857,7 @@ it.instance(
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
const b = yield* ask({
|
||||
id: PermissionID.make("per_test5b"),
|
||||
id: PermissionLegacy.ID.make("per_test5b"),
|
||||
sessionID: SessionID.make("session_same"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -867,7 +867,7 @@ it.instance(
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
yield* waitForPending(2)
|
||||
yield* reply({ requestID: PermissionID.make("per_test5a"), reply: "always" })
|
||||
yield* reply({ requestID: PermissionLegacy.ID.make("per_test5a"), reply: "always" })
|
||||
|
||||
yield* Fiber.join(a)
|
||||
yield* Fiber.join(b)
|
||||
@@ -881,7 +881,7 @@ it.instance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const a = yield* ask({
|
||||
id: PermissionID.make("per_test6a"),
|
||||
id: PermissionLegacy.ID.make("per_test6a"),
|
||||
sessionID: SessionID.make("session_a"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -891,7 +891,7 @@ it.instance(
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
const b = yield* ask({
|
||||
id: PermissionID.make("per_test6b"),
|
||||
id: PermissionLegacy.ID.make("per_test6b"),
|
||||
sessionID: SessionID.make("session_b"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -901,10 +901,10 @@ it.instance(
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
yield* waitForPending(2)
|
||||
yield* reply({ requestID: PermissionID.make("per_test6a"), reply: "always" })
|
||||
yield* reply({ requestID: PermissionLegacy.ID.make("per_test6a"), reply: "always" })
|
||||
|
||||
yield* Fiber.join(a)
|
||||
expect((yield* list()).map((item) => item.id)).toEqual([PermissionID.make("per_test6b")])
|
||||
expect((yield* list()).map((item) => item.id)).toEqual([PermissionLegacy.ID.make("per_test6b")])
|
||||
|
||||
yield* rejectAll()
|
||||
yield* Fiber.await(b)
|
||||
@@ -917,10 +917,10 @@ it.instance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const seen = yield* Deferred.make<{ sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }>()
|
||||
const seen = yield* Deferred.make<{ sessionID: SessionID; requestID: PermissionLegacy.ID; reply: PermissionLegacy.Reply }>()
|
||||
|
||||
const fiber = yield* ask({
|
||||
id: PermissionID.make("per_test7"),
|
||||
id: PermissionLegacy.ID.make("per_test7"),
|
||||
sessionID: SessionID.make("session_test"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -935,13 +935,13 @@ it.instance(
|
||||
if (event.type === Permission.Event.Replied.type)
|
||||
Deferred.doneUnsafe(
|
||||
seen,
|
||||
Effect.succeed(event.data as { sessionID: SessionID; requestID: PermissionID; reply: Permission.Reply }),
|
||||
Effect.succeed(event.data as { sessionID: SessionID; requestID: PermissionLegacy.ID; reply: PermissionLegacy.Reply }),
|
||||
)
|
||||
return Effect.void
|
||||
})
|
||||
yield* Effect.addFinalizer(() => unsub)
|
||||
|
||||
yield* reply({ requestID: PermissionID.make("per_test7"), reply: "once" })
|
||||
yield* reply({ requestID: PermissionLegacy.ID.make("per_test7"), reply: "once" })
|
||||
yield* Fiber.join(fiber)
|
||||
expect(
|
||||
yield* Deferred.await(seen).pipe(
|
||||
@@ -952,7 +952,7 @@ it.instance(
|
||||
),
|
||||
).toEqual({
|
||||
sessionID: SessionID.make("session_test"),
|
||||
requestID: PermissionID.make("per_test7"),
|
||||
requestID: PermissionLegacy.ID.make("per_test7"),
|
||||
reply: "once",
|
||||
})
|
||||
}),
|
||||
@@ -969,7 +969,7 @@ it.live("permission requests stay isolated by directory", () =>
|
||||
.provide(
|
||||
{ directory: one },
|
||||
ask({
|
||||
id: PermissionID.make("per_dir_a"),
|
||||
id: PermissionLegacy.ID.make("per_dir_a"),
|
||||
sessionID: SessionID.make("session_dir_a"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -984,7 +984,7 @@ it.live("permission requests stay isolated by directory", () =>
|
||||
.provide(
|
||||
{ directory: two },
|
||||
ask({
|
||||
id: PermissionID.make("per_dir_b"),
|
||||
id: PermissionLegacy.ID.make("per_dir_b"),
|
||||
sessionID: SessionID.make("session_dir_b"),
|
||||
permission: "bash",
|
||||
patterns: ["pwd"],
|
||||
@@ -1000,8 +1000,8 @@ it.live("permission requests stay isolated by directory", () =>
|
||||
|
||||
expect(onePending).toHaveLength(1)
|
||||
expect(twoPending).toHaveLength(1)
|
||||
expect(onePending[0].id).toBe(PermissionID.make("per_dir_a"))
|
||||
expect(twoPending[0].id).toBe(PermissionID.make("per_dir_b"))
|
||||
expect(onePending[0].id).toBe(PermissionLegacy.ID.make("per_dir_a"))
|
||||
expect(twoPending[0].id).toBe(PermissionLegacy.ID.make("per_dir_b"))
|
||||
|
||||
yield* store.provide({ directory: one }, reply({ requestID: onePending[0].id, reply: "reject" }))
|
||||
yield* store.provide({ directory: two }, reply({ requestID: twoPending[0].id, reply: "reject" }))
|
||||
@@ -1018,7 +1018,7 @@ it.instance(
|
||||
const test = yield* TestInstance
|
||||
const store = yield* InstanceStore.Service
|
||||
const fiber = yield* ask({
|
||||
id: PermissionID.make("per_dispose"),
|
||||
id: PermissionLegacy.ID.make("per_dispose"),
|
||||
sessionID: SessionID.make("session_dispose"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -1033,7 +1033,7 @@ it.instance(
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -1045,7 +1045,7 @@ it.instance(
|
||||
const test = yield* TestInstance
|
||||
const store = yield* InstanceStore.Service
|
||||
const fiber = yield* ask({
|
||||
id: PermissionID.make("per_reload"),
|
||||
id: PermissionLegacy.ID.make("per_reload"),
|
||||
sessionID: SessionID.make("session_reload"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -1059,7 +1059,7 @@ it.instance(
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -1068,7 +1068,7 @@ it.instance(
|
||||
"reply - fails for unknown requestID",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* reply({ requestID: PermissionID.make("per_unknown"), reply: "once" }).pipe(Effect.exit)
|
||||
const exit = yield* reply({ requestID: PermissionLegacy.ID.make("per_unknown"), reply: "once" }).pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) {
|
||||
expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "Permission.NotFoundError", requestID: "per_unknown" })
|
||||
@@ -1095,7 +1095,7 @@ it.instance(
|
||||
],
|
||||
}),
|
||||
)
|
||||
expect(err).toBeInstanceOf(Permission.DeniedError)
|
||||
expect(err).toBeInstanceOf(PermissionLegacy.DeniedError)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
@@ -1135,7 +1135,7 @@ it.instance(
|
||||
}),
|
||||
)
|
||||
|
||||
expect(err).toBeInstanceOf(Permission.DeniedError)
|
||||
expect(err).toBeInstanceOf(PermissionLegacy.DeniedError)
|
||||
expect(yield* list()).toHaveLength(0)
|
||||
}),
|
||||
{ git: true },
|
||||
@@ -1149,7 +1149,7 @@ it.instance(
|
||||
const store = yield* InstanceStore.Service
|
||||
|
||||
const fiber = yield* ask({
|
||||
id: PermissionID.make("per_reload"),
|
||||
id: PermissionLegacy.ID.make("per_reload"),
|
||||
sessionID: SessionID.make("session_reload"),
|
||||
permission: "bash",
|
||||
patterns: ["ls"],
|
||||
@@ -1164,7 +1164,7 @@ it.instance(
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { PermissionTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
@@ -218,16 +218,6 @@ describe("Project.fromDirectory", () => {
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(PermissionTable)
|
||||
.values({
|
||||
project_id: rootProject.id,
|
||||
data: [{ permission: "edit", pattern: "*", action: "allow" }],
|
||||
time_created: Date.now(),
|
||||
time_updated: Date.now(),
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(WorkspaceTable)
|
||||
.values({ id: workspaceID, type: "local", name: "test", project_id: rootProject.id })
|
||||
@@ -245,14 +235,6 @@ describe("Project.fromDirectory", () => {
|
||||
(yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie))
|
||||
?.project_id,
|
||||
).toBe(remoteID)
|
||||
expect(
|
||||
yield* db
|
||||
.select()
|
||||
.from(PermissionTable)
|
||||
.where(eq(PermissionTable.project_id, remoteID))
|
||||
.get()
|
||||
.pipe(Effect.orDie),
|
||||
).toBeDefined()
|
||||
expect(
|
||||
(yield* db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get().pipe(Effect.orDie))
|
||||
?.project_id,
|
||||
|
||||
@@ -579,6 +579,32 @@ const scenarios: Scenario[] = [
|
||||
.get("/api/provider/{providerID}", "v2.provider.get")
|
||||
.at((ctx) => ({ path: route("/api/provider/{providerID}", { providerID: "missing" }), headers: ctx.headers() }))
|
||||
.json(404, object, "status"),
|
||||
http.protected.get("/api/permission/request", "v2.permission.request.list").json(200, array),
|
||||
http.protected
|
||||
.get("/api/session/{sessionID}/permission/request", "v2.session.permission.list")
|
||||
.seeded((ctx) => ctx.session({ title: "Permission list owner" }))
|
||||
.at((ctx) => ({
|
||||
path: route("/api/session/{sessionID}/permission/request", { sessionID: ctx.state.id }),
|
||||
headers: ctx.headers(),
|
||||
}))
|
||||
.json(200, array),
|
||||
http.protected
|
||||
.post("/api/session/{sessionID}/permission/request/{requestID}/reply", "v2.session.permission.reply")
|
||||
.seeded((ctx) => ctx.session({ title: "Permission owner" }))
|
||||
.at((ctx) => ({
|
||||
path: route("/api/session/{sessionID}/permission/request/{requestID}/reply", {
|
||||
sessionID: ctx.state.id,
|
||||
requestID: "per_httpapi_missing",
|
||||
}),
|
||||
headers: ctx.headers(),
|
||||
body: { reply: "once" },
|
||||
}))
|
||||
.json(404, object, "status"),
|
||||
http.protected.get("/api/permission/saved", "v2.permission.saved.list").json(200, array),
|
||||
http.protected
|
||||
.delete("/api/permission/saved/{id}", "v2.permission.saved.remove")
|
||||
.at((ctx) => ({ path: route("/api/permission/saved/{id}", { id: "psv_httpapi_missing" }), headers: ctx.headers() }))
|
||||
.status(204, undefined, "status"),
|
||||
http.protected
|
||||
.get("/api/session", "v2.session.list")
|
||||
.at((ctx) => ({ path: "/api/session?roots=true", headers: ctx.headers() }))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { describe, expect } from "bun:test"
|
||||
@@ -8,7 +9,6 @@ import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { ControlPaths } from "../../src/server/routes/instance/httpapi/groups/control"
|
||||
import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
|
||||
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
||||
import { PermissionID } from "../../src/permission/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { QuestionID } from "../../src/question/schema"
|
||||
import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
|
||||
@@ -167,7 +167,7 @@ describe("instance HttpApi", () => {
|
||||
handlerContext,
|
||||
),
|
||||
)
|
||||
const permissionID = PermissionID.ascending()
|
||||
const permissionID = PermissionLegacy.ID.ascending()
|
||||
const questionReplyID = QuestionID.ascending()
|
||||
const questionRejectID = QuestionID.ascending()
|
||||
const [permission, questionReply, questionReject] = yield* Effect.all(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
@@ -11,7 +12,6 @@ import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import type { WorkspaceAdapter } from "../../src/control-plane/types"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { PermissionID } from "../../src/permission/schema"
|
||||
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap"
|
||||
import { InstanceBootstrap as InstanceBootstrapService } from "../../src/project/bootstrap-service"
|
||||
@@ -913,7 +913,7 @@ describe("session HttpApi", () => {
|
||||
}),
|
||||
).toMatchObject({ id: session.id })
|
||||
|
||||
const permissionID = String(PermissionID.ascending())
|
||||
const permissionID = String(PermissionLegacy.ID.ascending())
|
||||
const permission = yield* request(
|
||||
pathFor(SessionPaths.permissions, {
|
||||
sessionID: session.id,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import path from "path"
|
||||
@@ -332,7 +333,7 @@ describe("session.llm.ai-sdk adapter", () => {
|
||||
})
|
||||
|
||||
test("preserves tool-error cause", async () => {
|
||||
const error = new Permission.RejectedError()
|
||||
const error = new PermissionLegacy.RejectedError()
|
||||
const events = await Effect.runPromise(
|
||||
LLMAISDK.toLLMEvents(LLMAISDK.adapterState(), {
|
||||
type: "tool-error",
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Global } from "@opencode-ai/core/global"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionTable, MessageTable, PartTable, TodoTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionShareTable } from "@opencode-ai/core/share/sql"
|
||||
import { SessionID, MessageID, PartID } from "../../src/session/schema"
|
||||
|
||||
@@ -574,7 +574,7 @@ describe("JSON to SQLite migration", () => {
|
||||
expect(todos[2].position).toBe(2)
|
||||
})
|
||||
|
||||
test("migrates permissions", async () => {
|
||||
test("does not migrate legacy permissions", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
@@ -592,12 +592,7 @@ describe("JSON to SQLite migration", () => {
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.permissions).toBe(1)
|
||||
|
||||
const permissions = db.select().from(PermissionTable).all()
|
||||
expect(permissions.length).toBe(1)
|
||||
expect(permissions[0].project_id).toBe("proj_test123abc")
|
||||
expect(permissions[0].data).toEqual(permissionData)
|
||||
expect(stats?.permissions).toBe(0)
|
||||
})
|
||||
|
||||
test("migrates session shares", async () => {
|
||||
@@ -694,7 +689,7 @@ describe("JSON to SQLite migration", () => {
|
||||
expect(todos[1].position).toBe(2)
|
||||
})
|
||||
|
||||
test("skips orphaned todos, permissions, and shares", async () => {
|
||||
test("skips orphaned todos and shares", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
@@ -733,11 +728,10 @@ describe("JSON to SQLite migration", () => {
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats.todos).toBe(1)
|
||||
expect(stats.permissions).toBe(1)
|
||||
expect(stats.permissions).toBe(0)
|
||||
expect(stats.shares).toBe(1)
|
||||
|
||||
expect(db.select().from(TodoTable).all().length).toBe(1)
|
||||
expect(db.select().from(PermissionTable).all().length).toBe(1)
|
||||
expect(db.select().from(SessionShareTable).all().length).toBe(1)
|
||||
})
|
||||
|
||||
@@ -848,7 +842,7 @@ describe("JSON to SQLite migration", () => {
|
||||
expect(stats.messages).toBe(1)
|
||||
expect(stats.parts).toBe(1)
|
||||
expect(stats.todos).toBe(1)
|
||||
expect(stats.permissions).toBe(1)
|
||||
expect(stats.permissions).toBe(0)
|
||||
expect(stats.shares).toBe(1)
|
||||
expect(stats.errors.length).toBeGreaterThanOrEqual(6)
|
||||
|
||||
@@ -857,7 +851,6 @@ describe("JSON to SQLite migration", () => {
|
||||
expect(db.select().from(MessageTable).all().length).toBe(1)
|
||||
expect(db.select().from(PartTable).all().length).toBe(1)
|
||||
expect(db.select().from(TodoTable).all().length).toBe(1)
|
||||
expect(db.select().from(PermissionTable).all().length).toBe(1)
|
||||
expect(db.select().from(SessionShareTable).all().length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
@@ -26,7 +27,7 @@ const glob = (p: string) =>
|
||||
process.platform === "win32" ? Filesystem.normalizePathPattern(p) : p.replaceAll("\\", "/")
|
||||
|
||||
function makeCtx() {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
const ctx: Tool.Context = {
|
||||
...baseCtx,
|
||||
ask: (req) =>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
@@ -52,12 +53,12 @@ const ctx = {
|
||||
}
|
||||
|
||||
const asks = () => {
|
||||
const items: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const items: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
return {
|
||||
items,
|
||||
next: {
|
||||
...ctx,
|
||||
ask: (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) =>
|
||||
ask: (req: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">) =>
|
||||
Effect.sync(() => {
|
||||
items.push(req)
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { describe, expect } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
@@ -186,7 +187,7 @@ describe("tool.grep", () => {
|
||||
[path.join(alias, "*")]: "allow",
|
||||
},
|
||||
})
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
const next: Tool.Context = {
|
||||
...ctx,
|
||||
ask: (req) =>
|
||||
@@ -234,7 +235,7 @@ describe("tool.grep", () => {
|
||||
yield* appfs.makeDirectory(remoteDir, { recursive: true }).pipe(Effect.orDie)
|
||||
yield* git(remoteRoot, ["clone", "--bare", source, remoteRepo])
|
||||
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
const next: Tool.Context = {
|
||||
...ctx,
|
||||
ask: (req) =>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import path from "path"
|
||||
@@ -83,12 +84,12 @@ const put = Effect.fn("LspToolTest.put")(function* (file: string) {
|
||||
})
|
||||
|
||||
const asks = () => {
|
||||
const items: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const items: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
return {
|
||||
items,
|
||||
next: {
|
||||
...ctx,
|
||||
ask: (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) =>
|
||||
ask: (req: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">) =>
|
||||
Effect.sync(() => {
|
||||
items.push(req)
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { Cause, Effect, Exit, Layer, Stream } from "effect"
|
||||
import path from "path"
|
||||
@@ -140,12 +141,12 @@ const load = Effect.fn("ReadToolTest.load")(function* (p: string) {
|
||||
return yield* fs.readFileString(p)
|
||||
})
|
||||
const asks = () => {
|
||||
const items: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const items: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
return {
|
||||
items,
|
||||
next: {
|
||||
...ctx,
|
||||
ask: (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) =>
|
||||
ask: (req: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">) =>
|
||||
Effect.sync(() => {
|
||||
items.push(req)
|
||||
}),
|
||||
@@ -328,7 +329,7 @@ describe("tool.read env file permissions", () => {
|
||||
let asked = false
|
||||
const next = {
|
||||
...ctx,
|
||||
ask: (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) =>
|
||||
ask: (req: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">) =>
|
||||
Effect.sync(() => {
|
||||
for (const pattern of req.patterns) {
|
||||
const rule = Permission.evaluate(req.permission, pattern, info.permission)
|
||||
@@ -336,7 +337,7 @@ describe("tool.read env file permissions", () => {
|
||||
asked = true
|
||||
}
|
||||
if (rule.action === "deny") {
|
||||
throw new Permission.DeniedError({ ruleset: info.permission })
|
||||
throw new PermissionLegacy.DeniedError({ ruleset: info.permission })
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
import type * as Scope from "effect/Scope"
|
||||
@@ -155,9 +156,9 @@ const each = (
|
||||
}
|
||||
}
|
||||
|
||||
const capture = (requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">>, stop?: Error) => ({
|
||||
const capture = (requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">>, stop?: Error) => ({
|
||||
...ctx,
|
||||
ask: (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) =>
|
||||
ask: (req: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">) =>
|
||||
Effect.sync(() => {
|
||||
requests.push(req)
|
||||
if (stop) throw stop
|
||||
@@ -222,7 +223,7 @@ describe("tool.shell permissions", () => {
|
||||
yield* runIn(
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: "echo hello",
|
||||
@@ -244,7 +245,7 @@ describe("tool.shell permissions", () => {
|
||||
yield* runIn(
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: "echo foo && echo bar",
|
||||
@@ -268,7 +269,7 @@ describe("tool.shell permissions", () => {
|
||||
runIn(
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: "Write-Host foo; if ($?) { Write-Host bar }",
|
||||
@@ -297,7 +298,7 @@ describe("tool.shell permissions", () => {
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -323,7 +324,7 @@ describe("tool.shell permissions", () => {
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
const file = process.platform === "win32" ? `${process.env.WINDIR!.replaceAll("\\", "/")}/*` : "/etc/*"
|
||||
const want = process.platform === "win32" ? glob(path.join(process.env.WINDIR!, "*")) : "/etc/*"
|
||||
expect(
|
||||
@@ -354,7 +355,7 @@ describe("tool.shell permissions", () => {
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const file = path.join(outerTmp, "outside.txt").replaceAll("\\", "/")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: `echo $(cat "${file}")`,
|
||||
@@ -383,7 +384,7 @@ describe("tool.shell permissions", () => {
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -409,7 +410,7 @@ describe("tool.shell permissions", () => {
|
||||
runIn(
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
const file = `${process.env.WINDIR!.replaceAll("\\", "/")}/win.ini`
|
||||
yield* run(
|
||||
{
|
||||
@@ -440,7 +441,7 @@ describe("tool.shell permissions", () => {
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -468,7 +469,7 @@ describe("tool.shell permissions", () => {
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -497,7 +498,7 @@ describe("tool.shell permissions", () => {
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -525,7 +526,7 @@ describe("tool.shell permissions", () => {
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -560,7 +561,7 @@ describe("tool.shell permissions", () => {
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
const root = path.parse(process.env.WINDIR!).root.replace(/[\\/]+$/, "")
|
||||
expect(
|
||||
yield* fail(
|
||||
@@ -593,7 +594,7 @@ describe("tool.shell permissions", () => {
|
||||
runIn(
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: "Get-Content $env:WINDIR/win.ini",
|
||||
@@ -620,7 +621,7 @@ describe("tool.shell permissions", () => {
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -649,7 +650,7 @@ describe("tool.shell permissions", () => {
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -677,7 +678,7 @@ describe("tool.shell permissions", () => {
|
||||
runIn(
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: "Set-Location C:/Windows",
|
||||
@@ -705,7 +706,7 @@ describe("tool.shell permissions", () => {
|
||||
runIn(
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: "Write-Output ('a' * 3)",
|
||||
@@ -731,7 +732,7 @@ describe("tool.shell permissions", () => {
|
||||
runIn(
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: `TYPE "${path.join(process.env.WINDIR!, "win.ini")}"`,
|
||||
@@ -755,7 +756,7 @@ describe("tool.shell permissions", () => {
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -779,7 +780,7 @@ describe("tool.shell permissions", () => {
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -810,7 +811,7 @@ describe("tool.shell permissions", () => {
|
||||
const want = Filesystem.normalizePathPattern(path.join(outerTmp, "*"))
|
||||
|
||||
for (const dir of forms(outerTmp)) {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{
|
||||
@@ -842,7 +843,7 @@ describe("tool.shell permissions", () => {
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
const want = glob(path.join(os.tmpdir(), "*"))
|
||||
expect(
|
||||
yield* fail(
|
||||
@@ -871,7 +872,7 @@ describe("tool.shell permissions", () => {
|
||||
projectRoot,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
const want = glob(path.join(os.tmpdir(), "*"))
|
||||
expect(
|
||||
yield* fail(
|
||||
@@ -903,7 +904,7 @@ describe("tool.shell permissions", () => {
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
const filepath = path.join(outerTmp, "outside.txt")
|
||||
expect(
|
||||
yield* fail(
|
||||
@@ -931,7 +932,7 @@ describe("tool.shell permissions", () => {
|
||||
yield* runIn(
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: `rm -rf ${path.join(tmp, "nested")}`,
|
||||
@@ -952,7 +953,7 @@ describe("tool.shell permissions", () => {
|
||||
yield* runIn(
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: "git log --oneline -5",
|
||||
@@ -974,7 +975,7 @@ describe("tool.shell permissions", () => {
|
||||
yield* runIn(
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run(
|
||||
{
|
||||
command: "cd .",
|
||||
@@ -996,7 +997,7 @@ describe("tool.shell permissions", () => {
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const err = new Error("stop after permission")
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
expect(
|
||||
yield* fail(
|
||||
{ command: "echo test > output.txt", description: "Redirect test output" },
|
||||
@@ -1017,7 +1018,7 @@ describe("tool.shell permissions", () => {
|
||||
yield* runIn(
|
||||
tmp,
|
||||
Effect.gen(function* () {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
yield* run({ command: "ls -la", description: "List" }, capture(requests))
|
||||
const bashReq = requests.find((r) => r.permission === "bash")
|
||||
expect(bashReq).toBeDefined()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
@@ -67,7 +68,7 @@ Use this skill.
|
||||
})).find((tool) => tool.id === SkillTool.id)
|
||||
if (!tool) throw new Error("Skill tool not found")
|
||||
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const requests: Array<Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">> = []
|
||||
const ctx: Tool.Context = {
|
||||
...baseCtx,
|
||||
ask: (req) =>
|
||||
|
||||
Reference in New Issue
Block a user