fix(core): stat shell workdir through the workspace environment

This commit is contained in:
Kit Langton
2026-08-05 22:27:55 -04:00
parent 6e0066247c
commit a4eeaefd5b
4 changed files with 77 additions and 44 deletions
+1
View File
@@ -134,6 +134,7 @@ export function buildLocationServiceMap(
[LocationMutation.node, LocationMutation.hostedNode],
[FileMutation.node, FileMutation.hostedNode],
[Shell.node, Shell.hostedNode],
[PluginSupervisor.node, PluginSupervisor.hostedNode],
]
: [[Location.node, Location.boundNode(ref)]],
)
+9 -1
View File
@@ -1,7 +1,8 @@
export * as PluginInternal from "./internal"
import type { Plugin } from "@opencode-ai/plugin/effect/plugin"
import { Context, Effect, Scope } from "effect"
import { Context, Effect, Option, Scope } from "effect"
import { WorkspaceEnvironment } from "../workspace/environment"
import { HttpClient } from "effect/unstable/http"
import { Agent } from "../agent"
import { Catalog } from "../catalog"
@@ -95,7 +96,14 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const skill = yield* Skill.Service
const tools = yield* Tool.Service
const wellknown = yield* WellKnown.Service
// Bound only in hosted Location graphs; plugins read it with serviceOption
// to route filesystem checks at the provider instead of the host.
const environment = yield* Effect.serviceOption(WorkspaceEnvironment.Service)
return Context.mergeAll(
Option.match(environment, {
onSome: (value) => Context.make(WorkspaceEnvironment.Service, value),
onNone: () => Context.empty(),
}),
Context.make(Agent.Service, agent),
Context.make(Catalog.Service, catalog),
Context.make(Command.Service, command),
+49 -37
View File
@@ -40,6 +40,7 @@ import { ReadToolFileSystem } from "../tool/read-filesystem"
import { Tool } from "../tool"
import { WebSearch } from "../websearch"
import { WellKnown } from "../wellknown"
import { WorkspaceEnvironment } from "../workspace/environment"
import { PluginInternal } from "./internal"
import { PluginRuntime } from "./runtime"
import { SdkPlugins } from "./sdk"
@@ -282,7 +283,9 @@ const layer = Layer.effect(
})
const updates = Stream.merge(
config.changes().pipe(
Stream.filterEffect((update) => Effect.map(config.entries(), (entries) => isPluginSource(entries, update.path))),
Stream.filterEffect((update) =>
Effect.map(config.entries(), (entries) => isPluginSource(entries, update.path)),
),
Stream.merge(Stream.fromPubSub(configuredChanges)),
),
bus.subscribe([Event.Updated, SdkPlugins.Updated]),
@@ -308,45 +311,54 @@ const layer = Layer.effect(
const nodeLayer = layer as Layer.Layer<Service, never, PluginInternal.Requirements>
const nodeDeps = [
Plugin.node,
SdkPlugins.node,
Agent.node,
Catalog.node,
Command.node,
Config.node,
Credential.node,
Bus.node,
FileMutation.node,
Formatter.node,
FileSystem.node,
FSUtil.node,
Global.node,
httpClient,
Image.node,
Integration.node,
KV.node,
Location.node,
LocationMutation.node,
ModelsDev.node,
Npm.node,
Permission.node,
PluginRuntime.node,
Form.node,
ReadToolFileSystem.node,
Reference.node,
Ripgrep.node,
SessionInstructions.node,
Shell.node,
Skill.node,
Tool.node,
Watcher.node,
WebSearch.node,
WellKnown.node,
] as const
export const node = makeLocationNode({
service: Service,
layer: nodeLayer,
deps: [
Plugin.node,
SdkPlugins.node,
Agent.node,
Catalog.node,
Command.node,
Config.node,
Credential.node,
Bus.node,
FileMutation.node,
Formatter.node,
FileSystem.node,
FSUtil.node,
Global.node,
httpClient,
Image.node,
Integration.node,
KV.node,
Location.node,
LocationMutation.node,
ModelsDev.node,
Npm.node,
Permission.node,
PluginRuntime.node,
Form.node,
ReadToolFileSystem.node,
Reference.node,
Ripgrep.node,
SessionInstructions.node,
Shell.node,
Skill.node,
Tool.node,
Watcher.node,
WebSearch.node,
WellKnown.node,
],
deps: nodeDeps,
})
/** Hosted graphs bind the workspace environment so internal plugins can reach it. */
export const hostedNode = makeLocationNode({
service: Service,
layer: nodeLayer,
deps: [...nodeDeps, WorkspaceEnvironment.node],
})
export { layer }
+18 -6
View File
@@ -4,8 +4,9 @@ import path from "path"
import { ToolFailure } from "@opencode-ai/ai"
import type { Content } from "@opencode-ai/schema/tool"
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
import { Deferred, Effect, Schema, Scope } from "effect"
import { Deferred, Effect, Option, Schema, Scope } from "effect"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { WorkspaceEnvironment } from "../../workspace/environment"
import { LocationMutation } from "../../location-mutation"
import { Permission } from "../../permission"
import { PluginRuntime } from "../../plugin/runtime"
@@ -83,6 +84,21 @@ export const Plugin = {
const runtime = yield* PluginRuntime.Service
const scope = yield* Scope.Scope
const fsUtil = yield* FSUtil.Service
// Hosted Locations bind the workspace environment; the workdir check must
// stat the provider filesystem there, never the host's.
const environment = Option.getOrUndefined(yield* Effect.serviceOption(WorkspaceEnvironment.Service))
const statWorkdir = (canonical: string) =>
environment
? environment.files.stat(canonical).pipe(
Effect.catchTag("WorkspaceEnvironment.NotFoundError", () =>
Effect.fail(new Error(`Working directory does not exist: ${canonical}`)),
),
)
: fsUtil.stat(canonical).pipe(
Effect.catchReason("PlatformError", "NotFound", () =>
Effect.fail(new Error(`Working directory does not exist: ${canonical}`)),
),
)
const mutation = yield* LocationMutation.Service
const shell = yield* Shell.Service
const permission = yield* Permission.Service
@@ -176,11 +192,7 @@ export const Plugin = {
agent: context.agent,
source,
})
const workdir = yield* fsUtil.stat(target.canonical).pipe(
Effect.catchReason("PlatformError", "NotFound", () =>
Effect.fail(new Error(`Working directory does not exist: ${target.canonical}`)),
),
)
const workdir = yield* statWorkdir(target.canonical)
if (workdir.type !== "Directory")
return yield* Effect.fail(new Error(`Working directory is not a directory: ${target.canonical}`))
}),