diff --git a/packages/plugins/sandbox-providers/daytona/package.json b/packages/plugins/sandbox-providers/daytona/package.json index 666300e93c..ddc5acd934 100644 --- a/packages/plugins/sandbox-providers/daytona/package.json +++ b/packages/plugins/sandbox-providers/daytona/package.json @@ -42,7 +42,7 @@ ], "scripts": { "prebuild": "pnpm -C ../../../.. --filter @paperclipai/plugin-sdk ensure-build-deps", - "build": "rm -rf dist && tsc && mkdir -p dist/scripts && cp -R src/scripts/. dist/scripts/", + "build": "rm -rf dist && tsc", "clean": "rm -rf dist", "typecheck": "pnpm -C ../../../.. --filter @paperclipai/plugin-sdk ensure-build-deps && tsc --noEmit", "test": "vitest run --config vitest.config.ts", diff --git a/packages/plugins/sandbox-providers/daytona/src/login-pty.test.ts b/packages/plugins/sandbox-providers/daytona/src/login-pty.test.ts index 398106dae3..518cd8715f 100644 --- a/packages/plugins/sandbox-providers/daytona/src/login-pty.test.ts +++ b/packages/plugins/sandbox-providers/daytona/src/login-pty.test.ts @@ -1,8 +1,4 @@ -import { spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { composeLaunchLine, createDaytonaLoginHomeFs, @@ -15,7 +11,6 @@ import { type DaytonaPtyHandle, type DaytonaPtyProcess, type DaytonaSandboxExec, - type LoginHomeInspection, type LoginPtyLaunchDescriptor, } from "./login-pty.js"; @@ -23,70 +18,21 @@ import { const ENTER = "\r"; const HOME = "/tmp/paperclip-adapter-login/11111111-2222-4333-8444-555555555555"; -const LOGIN_UID = 1000; const CLAUDE: LoginPtyLaunchDescriptor = { loginCommandKey: "claude", sessionHome: HOME }; const CODEX: LoginPtyLaunchDescriptor = { loginCommandKey: "codex", sessionHome: HOME }; -/** A directory entry in the virtual filesystem. */ -type FakeEntry = Omit; - -const ABSENT: LoginHomeInspection = { - exists: false, - isSymlink: false, - isDirectory: false, - mode: "", - ownerUid: null, -}; - /** - * A fake login-home filesystem. It models one virtual path. `createDirectory` - * fails when the path exists and otherwise stores `createAs` (a clean 0700 - * directory owned by the login user by default). A test seeds a pre-existing - * entry, forces a bad created entry, or swaps the entry through `onCreatePty`, so - * a test drives each rejection and the pre-launch re-check. + * A fake login-home filesystem. It records each path the caller asks to create + * and never fails, matching the `mkdir -p` semantics of the real command: a + * repeat create for the same path succeeds the same as a fresh one. */ -function createFakeHomeFs(config?: { - loginUid?: number; - seed?: FakeEntry; - createAs?: FakeEntry; - createShouldFail?: boolean; -}): DaytonaLoginHomeFs & { - created: Array<{ path: string; mode: string }>; - inspectCount: number; - setEntry: (entry: FakeEntry | null) => void; -} { - const loginUid = config?.loginUid ?? LOGIN_UID; - const cleanDir: FakeEntry = { - isSymlink: false, - isDirectory: true, - mode: "700", - ownerUid: loginUid, - }; - let entry: FakeEntry | null = config?.seed ?? null; - const created: Array<{ path: string; mode: string }> = []; - const state = { inspectCount: 0 }; +function createFakeHomeFs(): DaytonaLoginHomeFs & { created: string[] } { + const created: string[] = []; return { created, - get inspectCount() { - return state.inspectCount; - }, - setEntry(next: FakeEntry | null): void { - entry = next; - }, - async loginUserId(): Promise { - return loginUid; - }, - async inspect(): Promise { - state.inspectCount += 1; - return entry ? { exists: true, ...entry } : ABSENT; - }, - async createDirectory(path: string, mode: string): Promise { - if (config?.createShouldFail || entry) { - throw new Error("LOGIN_PTY_HOME_REJECTED"); - } - created.push({ path, mode }); - entry = config?.createAs ?? cleanDir; + async createDirectory(path: string): Promise { + created.push(path); }, }; } @@ -154,11 +100,9 @@ function createFakePtyHandle( /** * A fake Daytona process. It opens one fake PTY handle and records the create - * options, so a test asserts the terminal size and the launch line. `onCreatePty` - * runs when the session opens the terminal, so a test swaps a symlink in after - * the directory creation but before the launch input. + * options, so a test asserts the terminal size and the launch line. */ -function createFakeProcess(onCreatePty?: () => void): DaytonaPtyProcess & { +function createFakeProcess(): DaytonaPtyProcess & { handle: ReturnType | null; createOptions: DaytonaPtyCreateOptions | null; createCount: number; @@ -181,7 +125,6 @@ function createFakeProcess(onCreatePty?: () => void): DaytonaPtyProcess & { async createPty(options: DaytonaPtyCreateOptions): Promise { state.createCount += 1; state.createOptions = options; - onCreatePty?.(); const handle = createFakePtyHandle(options.onData); state.handle = handle; return handle; @@ -217,15 +160,14 @@ describe("composeLaunchLine", () => { }); describe("openDaytonaLoginPtySession — session home", () => { - it("creates the exact UUID directory as a new 0700 directory owned by the login user", async () => { + it("creates the session home directory", async () => { const process = createFakeProcess(); const fs = createFakeHomeFs(); await openDaytonaLoginPtySession(process, fs, CLAUDE); - // The provider created the exact directory as a new 0700 directory. - expect(fs.created).toEqual([{ path: HOME, mode: "700" }]); - // The terminal opened and the launch line ran. + // The provider created the exact directory and opened the terminal. + expect(fs.created).toEqual([HOME]); expect(process.createCount).toBe(1); expect(process.handle?.inputs[0]).toBe("exec claude setup-token" + ENTER); }); @@ -243,94 +185,18 @@ describe("openDaytonaLoginPtySession — session home", () => { expect((process.handle?.inputs[0]?.match(/CODEX_HOME=/g) ?? []).length).toBe(1); }); - it("rejects a pre-existing target before it creates the directory", async () => { + it("opens the login session when the session home already exists", async () => { const process = createFakeProcess(); - const fs = createFakeHomeFs({ - seed: { isSymlink: false, isDirectory: true, mode: "700", ownerUid: LOGIN_UID }, - }); - - await expect(openDaytonaLoginPtySession(process, fs, CLAUDE)).rejects.toThrow( - "LOGIN_PTY_HOME_REJECTED", - ); - // The provider never created the directory and never opened the terminal. - expect(fs.created).toEqual([]); - expect(process.createCount).toBe(0); - }); - - it("rejects a pre-existing symlink at the target", async () => { - const process = createFakeProcess(); - const fs = createFakeHomeFs({ - seed: { isSymlink: true, isDirectory: false, mode: "777", ownerUid: LOGIN_UID }, - }); - - await expect(openDaytonaLoginPtySession(process, fs, CLAUDE)).rejects.toThrow( - "LOGIN_PTY_HOME_REJECTED", - ); - expect(process.createCount).toBe(0); - }); - - it("rejects a created target that is a symlink", async () => { - const process = createFakeProcess(); - const fs = createFakeHomeFs({ - createAs: { isSymlink: true, isDirectory: false, mode: "700", ownerUid: LOGIN_UID }, - }); - - await expect(openDaytonaLoginPtySession(process, fs, CLAUDE)).rejects.toThrow( - "LOGIN_PTY_HOME_REJECTED", - ); - expect(process.createCount).toBe(0); - }); - - it("rejects a created target that is not a directory", async () => { - const process = createFakeProcess(); - const fs = createFakeHomeFs({ - createAs: { isSymlink: false, isDirectory: false, mode: "700", ownerUid: LOGIN_UID }, - }); - - await expect(openDaytonaLoginPtySession(process, fs, CLAUDE)).rejects.toThrow( - "LOGIN_PTY_HOME_REJECTED", - ); - expect(process.createCount).toBe(0); - }); - - it("rejects a created directory with a wrong owner", async () => { - const process = createFakeProcess(); - const fs = createFakeHomeFs({ - createAs: { isSymlink: false, isDirectory: true, mode: "700", ownerUid: LOGIN_UID + 1 }, - }); - - await expect(openDaytonaLoginPtySession(process, fs, CLAUDE)).rejects.toThrow( - "LOGIN_PTY_HOME_REJECTED", - ); - expect(process.createCount).toBe(0); - }); - - it("rejects a created directory with a wrong mode", async () => { - const process = createFakeProcess(); - const fs = createFakeHomeFs({ - createAs: { isSymlink: false, isDirectory: true, mode: "755", ownerUid: LOGIN_UID }, - }); - - await expect(openDaytonaLoginPtySession(process, fs, CLAUDE)).rejects.toThrow( - "LOGIN_PTY_HOME_REJECTED", - ); - expect(process.createCount).toBe(0); - }); - - it("rejects a symlink swapped in after creation, before the launch input", async () => { - // The directory validates cleanly, then a symlink replaces it while the - // terminal opens. The no-symlink re-check before the launch input fails. const fs = createFakeHomeFs(); - const process = createFakeProcess(() => { - fs.setEntry({ isSymlink: true, isDirectory: false, mode: "777", ownerUid: LOGIN_UID }); - }); - await expect(openDaytonaLoginPtySession(process, fs, CLAUDE)).rejects.toThrow( - "LOGIN_PTY_HOME_REJECTED", - ); - // The terminal opened, but the launch input never ran. - expect(process.createCount).toBe(1); - expect(process.handle?.inputs).toEqual([]); + // A second login for the same home hits an existing directory. `mkdir -p` + // succeeds on an existing directory, so the second session opens the same + // as the first. + await openDaytonaLoginPtySession(process, fs, CLAUDE); + await openDaytonaLoginPtySession(process, fs, CLAUDE); + + expect(fs.created).toEqual([HOME, HOME]); + expect(process.createCount).toBe(2); }); it("rejects a descriptor with a command key outside the closed set", async () => { @@ -464,37 +330,9 @@ describe("openDaytonaLoginPtySession — session mechanics", () => { }); }); -describe("openDaytonaLoginPtySession — ancestor symlink", () => { - it("starts no pseudo-terminal when the home filesystem check fails closed", async () => { - // A symlinked login-root ancestor makes the descriptor-relative inspection fail - // closed. The session opener must reject before it opens the terminal, so no - // pseudo-terminal starts and the login command never runs. - const process = createFakeProcess(); - let created = false; - const fs: DaytonaLoginHomeFs = { - async loginUserId(): Promise { - return LOGIN_UID; - }, - async inspect(): Promise { - throw new Error("LOGIN_PTY_HOME_REJECTED"); - }, - async createDirectory(): Promise { - created = true; - }, - }; - - await expect(openDaytonaLoginPtySession(process, fs, CODEX)).rejects.toThrow( - "LOGIN_PTY_HOME_REJECTED", - ); - // The provider never created the directory and never opened the terminal. - expect(created).toBe(false); - expect(process.createCount).toBe(0); - }); -}); - describe("createDaytonaLoginHomeFs — login-profile preamble", () => { // A capturing exec surface. It records each command and returns a fixed result, - // so a test reads the exact command string the helper runs. + // so a test reads the exact command string the create runs. function createCapturingExec(result: DaytonaExecResult): DaytonaSandboxExec & { commands: string[]; } { @@ -503,125 +341,39 @@ describe("createDaytonaLoginHomeFs — login-profile preamble", () => { commands, async executeCommand(command: string): Promise { commands.push(command); - // `id -u` resolves the login user id. Return a fixed uid so the caller - // continues to the node helper command under test. - if (command.startsWith("id -u")) { - return { exitCode: 0, result: "1000" }; - } return result; }, }; } - // The command sources /etc/profile before it runs node. The Daytona image may - // expose node only through a login profile, so node must run after the profile - // source. This assertion proves the helper runtime and the login PTY capability - // stay consistent. - function expectProfileBeforeNode(command: string | undefined): void { - expect(command).toBeDefined(); - const profileIndex = command!.indexOf("/etc/profile"); - const nodeIndex = command!.indexOf("node -e"); - expect(profileIndex).toBeGreaterThanOrEqual(0); - expect(nodeIndex).toBeGreaterThanOrEqual(0); - expect(profileIndex).toBeLessThan(nodeIndex); - } - - it("sources the login profiles before it runs the node inspect helper", async () => { - const exec = createCapturingExec({ exitCode: 0, result: "ABSENT" }); - const fs = createDaytonaLoginHomeFs(exec); - await fs.inspect(HOME); - expectProfileBeforeNode(exec.commands.find((command) => command.includes("node -e"))); - }); - - it("sources the login profiles before it runs the node create helper", async () => { + it("sources the login profiles before it creates the session home directory", async () => { + // The command sources /etc/profile before it runs `mkdir -p`, so the create + // command stays consistent with the profile chain the rest of the sandbox + // exec commands use. const exec = createCapturingExec({ exitCode: 0, result: "" }); const fs = createDaytonaLoginHomeFs(exec); - await fs.createDirectory(HOME, "700"); - expectProfileBeforeNode(exec.commands.find((command) => command.includes("node -e"))); - }); -}); -// The real login-home filesystem runs a node helper on the sandbox. The helper -// walks the path with `/proc/self/fd`, which is Linux only. A non-Linux host skips -// these tests. The fake exec runs the helper locally, so the descriptor-relative -// walk is tested against a real filesystem. The login sandbox is Linux. -const describeLinux = process.platform === "linux" ? describe : describe.skip; + await fs.createDirectory(HOME); -describeLinux("createDaytonaLoginHomeFs — descriptor-relative walk", () => { - let root: string; - - beforeAll(() => { - root = mkdtempSync(path.join(tmpdir(), "daytona-login-home-")); - }); - afterAll(() => { - rmSync(root, { recursive: true, force: true }); + const command = exec.commands.find((entry) => entry.includes("mkdir -p")); + expect(command).toBeDefined(); + const profileIndex = command!.indexOf("/etc/profile"); + const mkdirIndex = command!.indexOf("mkdir -p"); + expect(profileIndex).toBeGreaterThanOrEqual(0); + expect(mkdirIndex).toBeGreaterThan(profileIndex); }); - // A local exec surface. It runs the composed command with `/bin/sh -c`, so the - // real node helper runs against the local filesystem. - function createLocalExec(): DaytonaSandboxExec { - return { - async executeCommand(command: string): Promise { - const result = spawnSync("/bin/sh", ["-c", command], { encoding: "utf8" }); - return { exitCode: result.status ?? undefined, result: result.stdout ?? "" }; - }, - }; - } + it("rejects the session and opens no pseudo-terminal when the create command exits non-zero", async () => { + // The sandbox `mkdir -p` command fails (for example, a read-only mount). + // The session must fail closed before it opens a pseudo-terminal. + const exec = createCapturingExec({ exitCode: 1, result: "" }); + const fs = createDaytonaLoginHomeFs(exec); + const process = createFakeProcess(); - const UUID = "11111111-2222-4333-8444-555555555555"; - - it("reports a not-yet-created target as absent", async () => { - const fs = createDaytonaLoginHomeFs(createLocalExec()); - const home = path.join(root, "absent-root", UUID); - const inspection = await fs.inspect(home); - expect(inspection.exists).toBe(false); - }); - - it("creates the login root and the session home relative to a trusted descriptor", async () => { - const fs = createDaytonaLoginHomeFs(createLocalExec()); - const home = path.join(root, "clean-root", UUID); - await fs.createDirectory(home, "700"); - const inspection = await fs.inspect(home); - expect(inspection.exists).toBe(true); - expect(inspection.isSymlink).toBe(false); - expect(inspection.isDirectory).toBe(true); - expect(inspection.mode).toBe("700"); - expect(inspection.ownerUid).toBe(process.getuid ? process.getuid() : inspection.ownerUid); - }); - - it("rejects a pre-existing session home target", async () => { - const fs = createDaytonaLoginHomeFs(createLocalExec()); - const home = path.join(root, "exists-root", UUID); - await fs.createDirectory(home, "700"); - // A second create of the same target fails, because the target exists. - await expect(fs.createDirectory(home, "700")).rejects.toThrow("LOGIN_PTY_HOME_REJECTED"); - }); - - it("fails the inspection closed when the login root is a symlink", async () => { - // Pre-place a symlink at the login-root ancestor. The descriptor-relative walk - // opens the root with a no-follow open, so the inspection fails closed. - const fs = createDaytonaLoginHomeFs(createLocalExec()); - const attacker = path.join(root, "inspect-attacker"); - mkdirSync(attacker, { mode: 0o700 }); - const rootLink = path.join(root, "inspect-symlink-root"); - symlinkSync(attacker, rootLink); - const home = path.join(rootLink, UUID); - await expect(fs.inspect(home)).rejects.toThrow("LOGIN_PTY_HOME_REJECTED"); - }); - - it("fails the creation closed when the login root is a pre-placed symlink", async () => { - // Pre-place a symlink at the login-root ancestor that points at an attacker - // tree. The creation opens the root with a no-follow open after the tolerant - // `mkdir`, so it fails closed and creates no directory in the attacker tree. - const fs = createDaytonaLoginHomeFs(createLocalExec()); - const attacker = path.join(root, "create-attacker"); - mkdirSync(attacker, { mode: 0o700 }); - const rootLink = path.join(root, "create-symlink-root"); - symlinkSync(attacker, rootLink); - const home = path.join(rootLink, UUID); - await expect(fs.createDirectory(home, "700")).rejects.toThrow("LOGIN_PTY_HOME_REJECTED"); - // The creation placed no session home inside the attacker tree. - expect(existsSync(path.join(attacker, UUID))).toBe(false); + await expect(openDaytonaLoginPtySession(process, fs, CLAUDE)).rejects.toThrow( + "LOGIN_PTY_HOME_REJECTED", + ); + expect(process.createCount).toBe(0); }); }); diff --git a/packages/plugins/sandbox-providers/daytona/src/login-pty.ts b/packages/plugins/sandbox-providers/daytona/src/login-pty.ts index 803cafc729..e3430b6b1c 100644 --- a/packages/plugins/sandbox-providers/daytona/src/login-pty.ts +++ b/packages/plugins/sandbox-providers/daytona/src/login-pty.ts @@ -15,17 +15,12 @@ // home cannot add a second shell token or a second command. // // Session home: the module revalidates the descriptor and the home shape, then -// creates the exact UUID directory as a NEW directory with mode 0700, owned by the -// login user. The inspection and the creation open the filesystem root, then walk -// each path component with a no-follow, directory-only open, so a symlink at any -// ancestor (the login root or a directory above it) fails closed. A single -// composite `stat` or `mkdir` follows a symlink at an ancestor; the per-component -// walk does not. The creation creates the login root if the root is absent and the -// UUID directory as a NEW directory, both relative to an open trusted descriptor. -// The module rejects a pre-existing target, a symlink, a non-directory, a wrong -// owner, and a wrong mode. It uses no recursive delete. It re-checks the directory -// with a no-symlink walk before it opens the terminal and again before it sends the -// launch input, so a symlink swapped in after creation fails before the launch. +// creates the session home directory with one `mkdir -p` command. The command +// runs inside the sandbox, so it holds no authority over a host file and no +// authority to call the Paperclip API. The sandbox contract in +// `SANDBOX-REQUIREMENTS.md` treats a check that only inspects state inside the +// sandbox as a non-boundary control, so this module keeps no owner check, no +// mode check, and no link-type check for the session home. // // Dependency boundary: this provider plugin ships standalone (the workspace // excludes `packages/plugins/sandbox-providers/**`). So the module imports no @@ -49,8 +44,6 @@ // or OSC 8 handling; the login parser owns that handling. import { randomUUID } from "node:crypto"; -import { readFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; import { sendPtyInputInChunks } from "./pty-chunked-input.js"; @@ -85,9 +78,6 @@ const LOGIN_COMMAND_BY_KEY: Readonly> = { codex: "codex login --device-auth", }; -/** The octal mode string for a new session home directory. */ -const LOGIN_HOME_MODE = "700"; - /** The fixed root for a login session home. */ const LOGIN_SESSION_HOME_ROOT = "/tmp/paperclip-adapter-login"; @@ -220,40 +210,16 @@ export interface DaytonaSandboxExec { ): Promise; } -/** One no-follow inspection of a filesystem path. */ -export interface LoginHomeInspection { - /** True when the path exists (as any type). */ - exists: boolean; - /** True when the path itself is a symbolic link. */ - isSymlink: boolean; - /** True when the path itself is a directory. */ - isDirectory: boolean; - /** The octal permission string, for example `700`. Empty when the path is absent. */ - mode: string; - /** The owner user id, or null when the path is absent. */ - ownerUid: number | null; -} - /** - * The narrow filesystem surface the session home operations need. The production - * surface runs commands on the sandbox; a unit test injects a fake. The inspection - * and the creation walk each path component with a no-follow open, so a symlink at - * any ancestor fails closed and a symlink at the target reports the link. + * The narrow filesystem surface the session home operation needs. The production + * surface runs a command on the sandbox; a unit test injects a fake. */ export interface DaytonaLoginHomeFs { - /** Resolves the user id of the login user that runs the pseudo-terminal. */ - loginUserId(): Promise; /** - * Inspects `path` without following a symlink at the target or at any ancestor. - * It rejects (throws) when an ancestor is a symlink or a non-directory. + * Creates `path` as a directory, plus a missing parent directory. It does not + * fail when the directory already exists. */ - inspect(path: string): Promise; - /** - * Creates `path` as a NEW directory with the octal `mode`. It creates the login - * root ancestor if the root is absent. It fails when the target path exists or - * when an ancestor is a symlink. It follows no composite pathname. - */ - createDirectory(path: string, mode: string): Promise; + createDirectory(path: string): Promise; } /** The options for the Daytona login PTY session. */ @@ -276,11 +242,6 @@ const LOGIN_PTY_ROWS = 30; */ const PTY_COMMAND_TERMINATOR = "\r"; -/** Normalizes an octal permission string to its numeric value for comparison. */ -function octalMode(value: string): number { - return Number.parseInt(value, 8); -} - /** * Revalidates the launch descriptor. It fails closed when the command key is * outside the closed set or the session home shape is wrong. A unit test that @@ -296,59 +257,12 @@ function assertDescriptor(descriptor: LoginPtyLaunchDescriptor): void { } } -/** - * Creates and validates the exact session home directory. It rejects a - * pre-existing target (including a symlink), creates the directory as a NEW - * directory with mode 0700, then verifies the directory type, the no-symlink - * state, the mode, and the login-user ownership. It uses no recursive delete. - */ -async function prepareSessionHome(fs: DaytonaLoginHomeFs, sessionHome: string): Promise { - const loginUid = await fs.loginUserId(); - - // Reject a pre-existing target. A no-follow inspection reports a symlink, a - // file, or a directory as present, so any pre-existing target fails here. - const before = await fs.inspect(sessionHome); - if (before.exists) { - throw new Error(LOGIN_PTY_HOME_REJECTED); - } - - // Create the exact directory as a NEW directory with mode 0700. The create - // fails when the path exists, so the create never follows a symlink. - await fs.createDirectory(sessionHome, LOGIN_HOME_MODE); - - // Verify the created directory. Reject a symlink, a non-directory, a wrong - // owner, and a wrong mode. Do not delete on a rejection; the sandbox is - // ephemeral and the provider uses no recursive delete. - const after = await fs.inspect(sessionHome); - if ( - !after.exists || - after.isSymlink || - !after.isDirectory || - after.ownerUid !== loginUid || - octalMode(after.mode) !== octalMode(LOGIN_HOME_MODE) - ) { - throw new Error(LOGIN_PTY_HOME_REJECTED); - } -} - -/** - * Re-checks the directory with a no-symlink check. It rejects a symlink and a - * non-directory, so a symlink swapped in after creation fails before the launch. - */ -async function assertHomeStillSafe(fs: DaytonaLoginHomeFs, sessionHome: string): Promise { - const now = await fs.inspect(sessionHome); - if (!now.exists || now.isSymlink || !now.isDirectory) { - throw new Error(LOGIN_PTY_HOME_REJECTED); - } -} - /** * Opens a Daytona PTY session for `descriptor` and returns it as a * {@link LoginPtySession}. The function revalidates the descriptor and the home - * shape, creates and validates the session home, opens a real pseudo-terminal, - * re-checks the directory before the launch, composes the safe launch line, and - * sends it. The session streams the raw terminal output, delivers delayed input, - * and stops the child. + * shape, creates the session home, opens a real pseudo-terminal, composes the + * safe launch line, and sends it. The session streams the raw terminal output, + * delivers delayed input, and stops the child. * * The function decodes the terminal bytes as a UTF-8 stream, so a multibyte * character that splits across two output chunks stays whole. It buffers the @@ -361,10 +275,9 @@ export async function openDaytonaLoginPtySession( options?: DaytonaLoginPtyOptions, ): Promise { assertDescriptor(descriptor); - // Create and validate the session home before the terminal opens. - await prepareSessionHome(fs, descriptor.sessionHome); - // Re-check the directory with a no-symlink check before `createPty`. - await assertHomeStillSafe(fs, descriptor.sessionHome); + // Create the session home. `mkdir -p` succeeds when the directory already + // exists, so a repeat login for the same home needs no extra check. + await fs.createDirectory(descriptor.sessionHome); const decoder = new TextDecoder("utf-8"); let listener: ((chunk: string) => void) | null = null; @@ -387,9 +300,6 @@ export async function openDaytonaLoginPtySession( }); await handle.waitForConnection(); - // Re-check the directory with a no-symlink check immediately before the launch - // input, so a symlink swapped in after creation fails before `sendInput`. - await assertHomeStillSafe(fs, descriptor.sessionHome); // Replace the interactive shell with the login command, so the pseudo-terminal // runs the command directly. The runner then writes the delayed browser code // to the command, not to a shell. @@ -434,63 +344,15 @@ export async function openDaytonaLoginPtySession( } /** - * The inspection helper source. The provider reads it from its own script file and - * runs it on the sandbox as ` && node -e