فهرست منبع

fix(app): terminal issues

Adam 1 ماه پیش
والد
کامیت
e70d2b27de

+ 5 - 4
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)

+ 1 - 1
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

+ 48 - 0
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 })