refactor(core): remove schema forwarding facades (#33577)

This commit is contained in:
Kit Langton 2026-06-24 04:52:58 +02:00 committed by GitHub
parent 6298db7a77
commit 57895586c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 31 additions and 70 deletions

View File

@ -3,10 +3,10 @@ export * as Config from "./config"
import path from "path" import path from "path"
import { type ParseError, parse } from "jsonc-parser" import { type ParseError, parse } from "jsonc-parser"
import { Context, Effect, Layer, Option, Schema } from "effect" import { Context, Effect, Layer, Option, Schema } from "effect"
import { Permission } from "@opencode-ai/schema/permission"
import { FSUtil } from "./fs-util" import { FSUtil } from "./fs-util"
import { Global } from "./global" import { Global } from "./global"
import { Location } from "./location" import { Location } from "./location"
import { PermissionSchema } from "./permission/schema"
import { Policy } from "./policy" import { Policy } from "./policy"
import { AbsolutePath } from "./schema" import { AbsolutePath } from "./schema"
import { ConfigAgent } from "./config/agent" import { ConfigAgent } from "./config/agent"
@ -56,7 +56,7 @@ export class Info extends Schema.Class<Info>("Config.Info")({
username: Schema.String.pipe(Schema.optional).annotate({ username: Schema.String.pipe(Schema.optional).annotate({
description: "Username displayed in conversations and used for telemetry identity", description: "Username displayed in conversations and used for telemetry identity",
}), }),
permissions: PermissionSchema.Ruleset.pipe(Schema.optional).annotate({ permissions: Permission.Ruleset.pipe(Schema.optional).annotate({
description: "Ordered tool permission rules applied to agent tool use", description: "Ordered tool permission rules applied to agent tool use",
}), }),
agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({ agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({

View File

@ -1,7 +1,7 @@
export * as ConfigAgent from "./agent" export * as ConfigAgent from "./agent"
import { Schema } from "effect" import { Schema } from "effect"
import { PermissionSchema } from "../permission/schema" import { Permission } from "@opencode-ai/schema/permission"
import { ConfigProvider } from "./provider" import { ConfigProvider } from "./provider"
import { PositiveInt } from "../schema" import { PositiveInt } from "../schema"
@ -21,5 +21,5 @@ export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
color: Color.pipe(Schema.optional), color: Color.pipe(Schema.optional),
steps: PositiveInt.pipe(Schema.optional), steps: PositiveInt.pipe(Schema.optional),
disabled: Schema.Boolean.pipe(Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional),
permissions: PermissionSchema.Ruleset.pipe(Schema.optional), permissions: Permission.Ruleset.pipe(Schema.optional),
}) {} }) {}

View File

@ -3,8 +3,8 @@ export * as Credential from "./credential"
import { asc, eq } from "drizzle-orm" import { asc, eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { Credential } from "@opencode-ai/schema/credential" import { Credential } from "@opencode-ai/schema/credential"
import { Integration } from "@opencode-ai/schema/integration"
import { Database } from "./database/database" import { Database } from "./database/database"
import { IntegrationSchema } from "./integration/schema"
import { CredentialTable } from "./credential/sql" import { CredentialTable } from "./credential/sql"
export const ID = Credential.ID export const ID = Credential.ID
@ -21,7 +21,7 @@ export type Value = Credential.Value
export class Info extends Schema.Class<Info>("Credential.Info")({ export class Info extends Schema.Class<Info>("Credential.Info")({
id: ID, id: ID,
integrationID: IntegrationSchema.ID, integrationID: Integration.ID,
label: Schema.String, label: Schema.String,
value: Value, value: Value,
}) {} }) {}
@ -30,12 +30,12 @@ export interface Interface {
/** Returns every stored credential. */ /** Returns every stored credential. */
readonly all: () => Effect.Effect<Info[]> readonly all: () => Effect.Effect<Info[]>
/** Returns stored credentials belonging to one integration. */ /** Returns stored credentials belonging to one integration. */
readonly list: (integrationID: IntegrationSchema.ID) => Effect.Effect<Info[]> readonly list: (integrationID: Integration.ID) => Effect.Effect<Info[]>
/** Returns one stored credential by ID. */ /** Returns one stored credential by ID. */
readonly get: (id: ID) => Effect.Effect<Info | undefined> readonly get: (id: ID) => Effect.Effect<Info | undefined>
/** Replaces any credential for an integration and returns the new record. */ /** Replaces any credential for an integration and returns the new record. */
readonly create: (input: { readonly create: (input: {
readonly integrationID: IntegrationSchema.ID readonly integrationID: Integration.ID
readonly value: Value readonly value: Value
readonly label?: string readonly label?: string
}) => Effect.Effect<Info> }) => Effect.Effect<Info>

View File

@ -1,11 +1,10 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
import { Timestamps } from "../database/schema.sql" import { Timestamps } from "../database/schema.sql"
import type { IntegrationSchema } from "../integration/schema"
import type { Credential } from "../credential" import type { Credential } from "../credential"
export const CredentialTable = sqliteTable("credential", { export const CredentialTable = sqliteTable("credential", {
id: text().$type<Credential.ID>().primaryKey(), id: text().$type<Credential.ID>().primaryKey(),
integration_id: text().$type<IntegrationSchema.ID>(), integration_id: text().$type<Credential.Info["integrationID"]>(),
label: text().notNull(), label: text().notNull(),
value: text({ mode: "json" }).$type<Credential.Value>().notNull(), value: text({ mode: "json" }).$type<Credential.Value>().notNull(),
connector_id: text(), connector_id: text(),

View File

@ -7,8 +7,8 @@ import { FSUtil } from "./fs-util"
import { Location } from "./location" import { Location } from "./location"
import { PositiveInt, RelativePath } from "./schema" import { PositiveInt, RelativePath } from "./schema"
import { FileSystemSearch } from "./filesystem/search" import { FileSystemSearch } from "./filesystem/search"
import { Entry, Match } from "./filesystem/schema" import { Entry, Match } from "@opencode-ai/schema/filesystem"
export { Entry, Match, Submatch } from "./filesystem/schema" export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem"
export const ReadInput = Schema.Struct({ export const ReadInput = Schema.Struct({
path: RelativePath, path: RelativePath,

View File

@ -1,10 +0,0 @@
import { FileSystem } from "@opencode-ai/schema/filesystem"
export const Entry = FileSystem.Entry
export type Entry = FileSystem.Entry
export const Submatch = FileSystem.Submatch
export type Submatch = FileSystem.Submatch
export const Match = FileSystem.Match
export type Match = FileSystem.Match

View File

@ -16,18 +16,17 @@ import {
} from "effect" } from "effect"
import { Integration } from "@opencode-ai/schema/integration" import { Integration } from "@opencode-ai/schema/integration"
import { Credential } from "./credential" import { Credential } from "./credential"
import { IntegrationSchema } from "./integration/schema"
import { withStatics } from "./schema" import { withStatics } from "./schema"
import { State } from "./state" import { State } from "./state"
import { Identifier } from "./util/identifier" import { Identifier } from "./util/identifier"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { IntegrationConnection } from "./integration/connection" import { IntegrationConnection } from "./integration/connection"
export const ID = IntegrationSchema.ID export const ID = Integration.ID
export type ID = IntegrationSchema.ID export type ID = Integration.ID
export const MethodID = IntegrationSchema.MethodID export const MethodID = Integration.MethodID
export type MethodID = IntegrationSchema.MethodID export type MethodID = Integration.MethodID
export const AttemptID = Schema.String.pipe( export const AttemptID = Schema.String.pipe(
Schema.brand("Integration.AttemptID"), Schema.brand("Integration.AttemptID"),
@ -103,10 +102,6 @@ export interface EnvImplementation {
export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation
function isOAuthImplementation(implementation: Implementation): implementation is OAuthImplementation {
return implementation.method.type === "oauth"
}
export class Attempt extends Schema.Class<Attempt>("Integration.Attempt")({ export class Attempt extends Schema.Class<Attempt>("Integration.Attempt")({
attemptID: AttemptID, attemptID: AttemptID,
url: Schema.String, url: Schema.String,

View File

@ -1,9 +0,0 @@
export * as IntegrationSchema from "./schema"
import { Integration } from "@opencode-ai/schema/integration"
export const ID = Integration.ID
export type ID = Integration.ID
export const MethodID = Integration.MethodID
export type MethodID = Integration.MethodID

View File

@ -1,6 +1,7 @@
export * as PermissionV2 from "./permission" export * as PermissionV2 from "./permission"
import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect" import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect"
import { Permission } from "@opencode-ai/schema/permission"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Location } from "./location" import { Location } from "./location"
import { AgentV2 } from "./agent" import { AgentV2 } from "./agent"
@ -9,14 +10,10 @@ import { SessionStore } from "./session/store"
import { withStatics } from "./schema" import { withStatics } from "./schema"
import { Identifier } from "./util/identifier" import { Identifier } from "./util/identifier"
import { Wildcard } from "./util/wildcard" import { Wildcard } from "./util/wildcard"
import { PermissionSchema } from "./permission/schema"
import { PermissionSaved } from "./permission/saved" import { PermissionSaved } from "./permission/saved"
export { Effect, Rule, Ruleset } from "./permission/schema" export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission"
type Effect = PermissionSchema.Effect const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
type Rule = PermissionSchema.Rule
type Ruleset = PermissionSchema.Ruleset
const missingAgentPermissions: Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
Schema.brand("PermissionV2.ID"), Schema.brand("PermissionV2.ID"),
@ -67,7 +64,7 @@ export type ReplyInput = typeof ReplyInput.Type
export const AskResult = Schema.Struct({ export const AskResult = Schema.Struct({
id: ID, id: ID,
effect: PermissionSchema.Effect, effect: Permission.Effect,
}).annotate({ identifier: "PermissionV2.AskResult" }) }).annotate({ identifier: "PermissionV2.AskResult" })
export type AskResult = typeof AskResult.Type export type AskResult = typeof AskResult.Type
@ -90,7 +87,7 @@ export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("P
}) {} }) {}
export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("PermissionV2.DeniedError", { export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("PermissionV2.DeniedError", {
rules: PermissionSchema.Ruleset, rules: Permission.Ruleset,
}) {} }) {}
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", { export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", {
@ -99,7 +96,7 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Per
export type Error = DeniedError | RejectedError | CorrectedError export type Error = DeniedError | RejectedError | CorrectedError
export function evaluate(action: string, resource: string, ...rulesets: Ruleset[]): Rule { export function evaluate(action: string, resource: string, ...rulesets: Permission.Ruleset[]): Permission.Rule {
return ( return (
rulesets rulesets
.flat() .flat()
@ -111,7 +108,7 @@ export function evaluate(action: string, resource: string, ...rulesets: Ruleset[
) )
} }
export function merge(...rulesets: Ruleset[]): Ruleset { export function merge(...rulesets: Permission.Ruleset[]): Permission.Ruleset {
return rulesets.flat() return rulesets.flat()
} }
@ -156,7 +153,7 @@ export const layer = Layer.effect(
const savedRules = EffectRuntime.fnUntraced(function* () { const savedRules = EffectRuntime.fnUntraced(function* () {
return (yield* saved.list({ projectID: location.project.id })).map( return (yield* saved.list({ projectID: location.project.id })).map(
(item): Rule => ({ action: item.action, resource: item.resource, effect: "allow" }), (item): Permission.Rule => ({ action: item.action, resource: item.resource, effect: "allow" }),
) )
}) })
@ -170,11 +167,11 @@ export const layer = Layer.effect(
return agent?.permissions ?? missingAgentPermissions return agent?.permissions ?? missingAgentPermissions
}) })
function denied(input: AssertInput, rules: Ruleset) { function denied(input: AssertInput, rules: Permission.Ruleset) {
return input.resources.some((resource) => evaluate(input.action, resource, rules).effect === "deny") return input.resources.some((resource) => evaluate(input.action, resource, rules).effect === "deny")
} }
function relevant(input: AssertInput, rules: Ruleset) { function relevant(input: AssertInput, rules: Permission.Ruleset) {
return rules.filter((rule) => Wildcard.match(input.action, rule.action)) return rules.filter((rule) => Wildcard.match(input.action, rule.action))
} }
@ -183,7 +180,7 @@ export const layer = Layer.effect(
if (denied(input, rules)) return { effect: "deny" as const, rules } if (denied(input, rules)) return { effect: "deny" as const, rules }
const all = [...rules, ...(yield* savedRules())] const all = [...rules, ...(yield* savedRules())]
const effects = input.resources.map((resource) => evaluate(input.action, resource, all).effect) const effects = input.resources.map((resource) => evaluate(input.action, resource, all).effect)
const effect: Effect = effects.includes("deny") ? "deny" : effects.includes("ask") ? "ask" : "allow" const effect: Permission.Effect = effects.includes("deny") ? "deny" : effects.includes("ask") ? "ask" : "allow"
return { effect, rules: all } return { effect, rules: all }
}) })

View File

@ -1,12 +0,0 @@
export * as PermissionSchema from "./schema"
import { Permission } from "@opencode-ai/schema/permission"
export const Effect = Permission.Effect
export type Effect = Permission.Effect
export const Rule = Permission.Rule
export type Rule = Permission.Rule
export const Ruleset = Permission.Ruleset
export type Ruleset = Permission.Ruleset

View File

@ -2,9 +2,8 @@ export * as Ripgrep from "./ripgrep"
import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect" import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import path from "path" import { Entry, Match } from "@opencode-ai/schema/filesystem"
import { LayerNode } from "./effect/layer-node" import { LayerNode } from "./effect/layer-node"
import { Entry, Match } from "./filesystem/schema"
import { AppProcess, collectStream, waitForAbort } from "./process" import { AppProcess, collectStream, waitForAbort } from "./process"
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
import { RipgrepBinary } from "./ripgrep/binary" import { RipgrepBinary } from "./ripgrep/binary"

View File

@ -49,12 +49,12 @@ test("Core reuses the canonical shared schemas", async () => {
import("@opencode-ai/core/command"), import("@opencode-ai/core/command"),
import("@opencode-ai/core/integration/connection"), import("@opencode-ai/core/integration/connection"),
import("@opencode-ai/core/credential"), import("@opencode-ai/core/credential"),
import("@opencode-ai/core/filesystem/schema"), import("@opencode-ai/core/filesystem"),
import("@opencode-ai/core/integration"), import("@opencode-ai/core/integration"),
import("@opencode-ai/core/location"), import("@opencode-ai/core/location"),
import("@opencode-ai/llm"), import("@opencode-ai/llm"),
import("@opencode-ai/core/model-request"), import("@opencode-ai/core/model-request"),
import("@opencode-ai/core/permission/schema"), import("@opencode-ai/core/permission"),
import("@opencode-ai/core/project/schema"), import("@opencode-ai/core/project/schema"),
import("@opencode-ai/core/reference"), import("@opencode-ai/core/reference"),
import("@opencode-ai/core/session/input"), import("@opencode-ai/core/session/input"),
@ -80,6 +80,8 @@ test("Core reuses the canonical shared schemas", async () => {
[coreFileSystem.Entry, FileSystem.Entry], [coreFileSystem.Entry, FileSystem.Entry],
[coreFileSystem.Submatch, FileSystem.Submatch], [coreFileSystem.Submatch, FileSystem.Submatch],
[coreFileSystem.Match, FileSystem.Match], [coreFileSystem.Match, FileSystem.Match],
[coreIntegration.ID, Integration.ID],
[coreIntegration.MethodID, Integration.MethodID],
[coreIntegration.When, Integration.When], [coreIntegration.When, Integration.When],
[coreIntegration.TextPrompt, Integration.TextPrompt], [coreIntegration.TextPrompt, Integration.TextPrompt],
[coreIntegration.SelectPrompt, Integration.SelectPrompt], [coreIntegration.SelectPrompt, Integration.SelectPrompt],