summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-22 06:17:59 -0600
committerAdam <[email protected]>2026-02-22 06:17:59 -0600
commite70d2b27de3aaed5a19b9ca2c6749ed7fce3ef93 (patch)
treec06e2f661454b3ab1397a9cdae42f3affa29744d
parentb16f7b426c5be6feea419dca87aad2032f25462b (diff)
downloadopencode-e70d2b27de3aaed5a19b9ca2c6749ed7fce3ef93.tar.gz
opencode-e70d2b27de3aaed5a19b9ca2c6749ed7fce3ef93.zip
fix(app): terminal issues
-rw-r--r--packages/opencode/src/pty/index.ts9
-rw-r--r--packages/opencode/src/server/routes/pty.ts2
-rw-r--r--packages/opencode/test/pty/pty-output-isolation.test.ts48
3 files changed, 54 insertions, 5 deletions
diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts
index 33083485b..fdb46c817 100644
--- a/packages/opencode/src/pty/index.ts
+++ b/packages/opencode/src/pty/index.ts
@@ -39,8 +39,9 @@ export namespace Pty {
return next
}
- const token = (ws: Socket) => {
- const data = ws.data
+ const token = (ws: unknown) => {
+ if (!ws || typeof ws !== "object") return ws
+ const data = (ws as { data?: unknown }).data
if (data === undefined) return
if (data === null) return
if (typeof data !== "object") return data
@@ -317,7 +318,7 @@ export namespace Pty {
}
}
- export function connect(id: string, ws: Socket, cursor?: number) {
+ export function connect(id: string, ws: Socket, cursor?: number, identity?: unknown) {
const session = state().get(id)
if (!session) {
ws.close()
@@ -337,7 +338,7 @@ export namespace Pty {
}
owners.set(ws, id)
- session.subscribers.set(ws, { id: socketId, token: token(ws) })
+ session.subscribers.set(ws, { id: socketId, token: token(identity ?? ws) })
const cleanup = () => {
session.subscribers.delete(ws)
diff --git a/packages/opencode/src/server/routes/pty.ts b/packages/opencode/src/server/routes/pty.ts
index 368c9612b..640cfa333 100644
--- a/packages/opencode/src/server/routes/pty.ts
+++ b/packages/opencode/src/server/routes/pty.ts
@@ -182,7 +182,7 @@ export const PtyRoutes = lazy(() =>
ws.close()
return
}
- handler = Pty.connect(id, socket, cursor)
+ handler = Pty.connect(id, socket, cursor, ws)
},
onMessage(event) {
if (typeof event.data !== "string") return
diff --git a/packages/opencode/test/pty/pty-output-isolation.test.ts b/packages/opencode/test/pty/pty-output-isolation.test.ts
index 07e86ea97..2c9cc5d92 100644
--- a/packages/opencode/test/pty/pty-output-isolation.test.ts
+++ b/packages/opencode/test/pty/pty-output-isolation.test.ts
@@ -98,6 +98,54 @@ describe("pty", () => {
})
})
+ test("does not leak when identity token is only on websocket wrapper", async () => {
+ await using dir = await tmpdir({ git: true })
+
+ await Instance.provide({
+ directory: dir.path,
+ fn: async () => {
+ const a = await Pty.create({ command: "cat", title: "a" })
+ try {
+ const outA: string[] = []
+ const outB: string[] = []
+ const text = (data: string | Uint8Array | ArrayBuffer) => {
+ if (typeof data === "string") return data
+ if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data)).toString("utf8")
+ return Buffer.from(data).toString("utf8")
+ }
+
+ const raw: Parameters<typeof Pty.connect>[1] = {
+ readyState: 1,
+ send: (data) => {
+ outA.push(text(data))
+ },
+ close: () => {
+ // no-op
+ },
+ }
+
+ const wrap = { data: { events: { connection: "a" } } }
+
+ Pty.connect(a.id, raw, undefined, wrap)
+ outA.length = 0
+
+ // Simulate Bun reusing the raw socket object before the next onOpen,
+ // while the connection token only exists on the wrapper socket.
+ raw.send = (data) => {
+ outB.push(text(data))
+ }
+
+ Pty.write(a.id, "AAA\n")
+ await Bun.sleep(100)
+
+ expect(outB.join("")).not.toContain("AAA")
+ } finally {
+ await Pty.remove(a.id)
+ }
+ },
+ })
+ })
+
test("does not leak output when socket data mutates in-place", async () => {
await using dir = await tmpdir({ git: true })