fix(server): apply plugin pty environment (#32296)

This commit is contained in:
Shoubhit Dash 2026-06-14 16:47:48 +05:30 committed by GitHub
commit 7ad68f8150
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 135 additions and 3 deletions

View file

@ -3,6 +3,9 @@ import { Context, Config as EffectConfig, Effect, Layer, Queue, Schema } from "e
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
import * as Socket from "effect/unstable/socket/Socket"
import path from "path"
import { pathToFileURL } from "url"
import { mkdir } from "fs/promises"
import { Location } from "@opencode-ai/core/location"
import { Pty } from "@opencode-ai/core/pty"
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
@ -171,4 +174,78 @@ describe("v2 pty HttpApi", () => {
expect(removed.status).toBe(204)
}),
)
;(process.platform === "win32" ? effectIt.live.skip : effectIt.live)(
"applies plugin shell environment before forced PTY values",
() =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true, config: { formatter: false, lsp: false } })
const plugin = path.join(dir, "plugin.ts")
const cwd = path.join(dir, "child")
yield* Effect.promise(() => mkdir(cwd))
yield* Effect.promise(() =>
Bun.write(
plugin,
[
"export default async () => ({",
' "shell.env": (input, output) => {',
' output.env.SHARED = "plugin"',
' output.env.PLUGIN = "plugin"',
' output.env.TERM = "plugin"',
" output.env.HOOK_CWD = input.cwd",
" },",
"})",
"",
].join("\n"),
),
)
yield* Effect.promise(() =>
Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({ plugin: [pathToFileURL(plugin).href], formatter: false, lsp: false }),
),
)
const created = yield* HttpClientRequest.post("/api/pty").pipe(
directoryHeader(dir),
HttpClientRequest.bodyJson({
command: "/bin/sh",
args: [
"-c",
'printf "%s|%s|%s|%s|%s\\n" "$CALLER" "$SHARED" "$PLUGIN" "$TERM" "$HOOK_CWD"; sleep 5',
],
cwd,
env: { CALLER: "caller", SHARED: "caller", TERM: "caller" },
}),
Effect.flatMap(HttpClient.execute),
)
expect(created.status).toBe(200)
const info = (yield* Schema.decodeUnknownEffect(Location.response(Pty.Info))(yield* created.json)).data
const socket = yield* Socket.makeWebSocket(
`${(yield* serverUrl()).replace(/^http/, "ws")}/api/pty/${info.id}/connect?cursor=0&location[directory]=${encodeURIComponent(dir)}`,
{ closeCodeIsError: () => false },
)
const messages = yield* Queue.unbounded<string>()
yield* socket
.runRaw((message) =>
Queue.offer(messages, typeof message === "string" ? message : new TextDecoder().decode(message)),
)
.pipe(Effect.catch(() => Effect.void), Effect.forkScoped)
const write = yield* socket.writer
const takeUntil = (expected: string, seen = ""): Effect.Effect<string, unknown> =>
Effect.gen(function* () {
const next = seen + (yield* Queue.take(messages).pipe(Effect.timeout("5 seconds")))
if (next.includes(expected)) return next
return yield* takeUntil(expected, next)
})
expect(yield* takeUntil(`caller|plugin|plugin|xterm-256color|${cwd}`)).toContain(
`caller|plugin|plugin|xterm-256color|${cwd}`,
)
yield* write(new Socket.CloseEvent(1000, "done")).pipe(Effect.catch(() => Effect.void))
yield* HttpClientRequest.delete(`/api/pty/${info.id}`).pipe(directoryHeader(dir), HttpClient.execute)
}),
)
})