apps/server/src/device/AgentDeviceShim.ts

// @effect-diagnostics preferSchemaOverJson:off - JSON string literals embed paths safely into generated JavaScript.
import * as HostProcess from "@t3tools/shared/HostProcess";
import { resolveNodeExecutable } from "@t3tools/shared/nodeRuntime";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";

const SHIM_DIR = "device/bin";
const TARGETS_DIR = "device/targets";
const STALE_RUNTIME =
  "This agent-device command is out of date because T3 Code moved or updated its runtime. Call device_open again for a fresh one.";

const quoteShell = (value: string) => "'" + value.replaceAll("'", "'\"'\"'") + "'";

/** agent-device 0.21.31's flags that take a value, so the launcher can find the command past them. */
const VALUE_FLAGS: ReadonlyArray<string> = [
  "--activity",
  "--all",
  "--android-device-allowlist",
  "--app",
  "--appium-version",
  "--artifact",
  "--artifacts-dir",
  "--aws-app-arn",
  "--aws-device-arn",
  "--aws-interaction-mode",
  "--aws-project-arn",
  "--aws-region",
  "-b",
  "--baseline",
  "--bearer-token",
  "--bundle-url",
  "--button",
  "--config",
  "--count",
  "--crop-on",
  "--custom-network",
  "-d",
  "--daemon-auth-token",
  "--daemon-base-url",
  "--daemon-server-mode",
  "--daemon-transport",
  "--delay-ms",
  "--depth",
  "--device",
  "--device-orientation",
  "--dsym",
  "--duration-ms",
  "--fps",
  "--from",
  "--geo-location",
  "--github-actions-artifact",
  "--header",
  "--hold-ms",
  "--host",
  "--in-app",
  "--include",
  "--interval-ms",
  "--ios-simulator-device-set",
  "--ios-xctest-derived-data-path",
  "--ios-xctest-env-dir",
  "--ios-xctestrun-file",
  "--jitter-px",
  "--keyframes",
  "--kind",
  "--language",
  "--launch-args",
  "--launch-console",
  "--launch-url",
  "--lease-backend",
  "--lease-id",
  "--level",
  "--listen-host",
  "--locale",
  "--max-passes",
  "--max-steps",
  "--metro-host",
  "--metro-kind",
  "--metro-port",
  "--network-profile",
  "--on-error",
  "--os-version",
  "--out",
  "--pattern",
  "--pause-ms",
  "--pixel-density",
  "--pixels",
  "--plan-digest",
  "--platform",
  "--pointer-count",
  "--port",
  "--probe-timeout-ms",
  "--project-root",
  "--provider",
  "--provider-app",
  "--provider-appium-version",
  "--provider-build",
  "--provider-custom-network",
  "--provider-device-orientation",
  "--provider-device-type",
  "--provider-geo-location",
  "--provider-language",
  "--provider-locale",
  "--provider-network-profile",
  "--provider-os-version",
  "--provider-project",
  "--provider-session",
  "--provider-session-name",
  "--provider-timezone",
  "--proxy-base-url",
  "--public-base-url",
  "--quality",
  "--record-as",
  "--remote-config",
  "--reporter",
  "--report-junit",
  "--retention-ms",
  "--retries",
  "--run-id",
  "--runtime-file",
  "-s",
  "--scale",
  "--scope",
  "--search-path",
  "--serial",
  "--session",
  "--session-isolation",
  "--session-lock",
  "--settle-quiet",
  "--shard-all",
  "--shard-split",
  "--startup-timeout-ms",
  "--state-dir",
  "--status-host",
  "--steps",
  "--steps-file",
  "--surface",
  "--system",
  "--target",
  "--target-app",
  "--template",
  "--tenant",
  "--threshold",
  "--timeout",
  "--timezone",
  "--udid",
  "--until",
  "--wait",
];

// Help gets no bound flags (agent-device reads --config even for help); only args before `--` are flags.
// agent-device 0.21.31 refuses some bound flags per command; connect/disconnect/auth/daemon would
// replace T3's connection; device status reads this machine's claims. It reports a sandboxed shell and a
// dropped tunnel alike, so check first, at --daemon-base-url if given, as agent-device prefers it.
const launcherSource = (node: string, entryPath: string, boundArgs: ReadonlyArray<string>) =>
  `import { spawn } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { connect } from "node:net";
const entry = ${JSON.stringify(entryPath)};
const bound = ${JSON.stringify(boundArgs)};
const refused = new Map([["cdp", ["--platform", "--udid", "--config", "--session"]], ["connection", ["--platform", "--udid"]]]);
const managed = ["connect", "disconnect", "auth", "daemon"];
const args = process.argv.slice(2);
const end = args.indexOf("--");
const options = end < 0 ? args : args.slice(0, end);
const valued = new Set(${JSON.stringify(VALUE_FLAGS)});
let name = "";
for (let index = 0; index < args.length; index++) {
  const arg = args[index];
  if (arg === "--") { name = args[index + 1] ?? ""; break; }
  if (!arg.startsWith("-")) { name = arg; break; }
  if (!arg.includes("=") && valued.has(arg)) index++;
}
const informational = args.length === 0 || ["help", "--help", "-h", "--version", "version"].includes(args[0]) || options.includes("--help") || options.includes("-h");
if (!informational && managed.includes(name)) {
  console.error("T3 Code manages this command's connection to the device, so \`" + name + "\` is not available through it.");
  process.exit(1);
}
if (!informational && name === "device") {
  console.error("\`device status\` reads this machine's device claims, not those on the device's host, so it is not available through this command. Quote the DEVICE_IN_USE error instead.");
  process.exit(1);
}
const skip = refused.get(name) ?? [];
if (!informational) for (let index = 0; index + 1 < bound.length; index += 2) if (!skip.includes(bound[index]) && !options.some(arg => arg === bound[index] || arg.startsWith(bound[index] + "="))) options.push(bound[index], bound[index + 1]);
const command = end < 0 ? options : [...options, ...args.slice(end)];
const hasValue = flag => { const index = options.indexOf(flag); return options.some(arg => arg.startsWith(flag + "=") && arg.length > flag.length + 1) || (index >= 0 && !!options[index + 1] && !options[index + 1].startsWith("--")); };
if (!informational && !skip.includes("--session") && !(hasValue("--config") && hasValue("--session"))) {
  console.error("Call device_open first and run the agent-device command it returns.");
  process.exit(1);
}
if (!existsSync(entry)) {
  console.error(${JSON.stringify(STALE_RUNTIME)});
  process.exit(1);
}
const valueOf = flag => { const inline = options.find(arg => arg.startsWith(flag + "=")); if (inline) return inline.slice(flag.length + 1); const index = options.indexOf(flag); return index >= 0 ? options[index + 1] : undefined; };
const endpoint = () => { try { const url = new URL(valueOf("--daemon-base-url") ?? JSON.parse(readFileSync(valueOf("--config"), "utf8")).daemonBaseUrl); return ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) && url.port ? url : undefined; } catch { return undefined; } };
const local = informational || refused.has(name) ? undefined : endpoint();
if (local) {
  const failure = await new Promise(resolve => {
    const socket = connect({ host: local.hostname.replace(/^\\[|\\]$/g, ""), port: Number(local.port) });
    socket.setTimeout(3000, () => { socket.destroy(); resolve("ETIMEDOUT"); });
    socket.once("connect", () => { socket.destroy(); resolve(undefined); });
    socket.once("error", error => resolve(error.code ?? "ERROR"));
  });
  if (failure === "EPERM" || failure === "EACCES") {
    console.error("This shell cannot reach T3 Code's device connection at " + local.host + " (connect " + failure + "): a network sandbox is blocking localhost. Run this command again with network access, outside the sandbox.");
    process.exit(1);
  }
  if (failure) {
    console.error("T3 Code's device connection at " + local.host + " is not reachable from this shell (connect " + failure + "). Call device_open again to reconnect; if that succeeds and this repeats, this shell runs in a network sandbox, so run the command with network access, outside the sandbox.");
    process.exit(1);
  }
}
const env = { ...process.env, ELECTRON_RUN_AS_NODE: "1" };
delete env.AGENT_DEVICE_DAEMON_BASE_URL;
delete env.AGENT_DEVICE_DAEMON_AUTH_TOKEN;
delete env.AGENT_DEVICE_CONFIG;
const child = spawn(${JSON.stringify(node)}, [entry, ...command], { stdio: "inherit", env });
child.on("error", error => { console.error(error.message); process.exitCode = 1; });
child.on("exit", code => { process.exitCode = code ?? 1; });
`;

/** Writes a launcher into `dir` and returns the executable agents run. */
const writeLauncher = Effect.fn("AgentDeviceShim.writeLauncher")(function* (input: {
  readonly dir: string;
  readonly entryPath: string;
  readonly boundArgs: ReadonlyArray<string>;
}) {
  const fs = yield* FileSystem.FileSystem;
  const path = yield* Path.Path;
  const platform = yield* HostProcess.Platform;
  const node = yield* resolveNodeExecutable("Device automation");
  yield* fs.makeDirectory(input.dir, { recursive: true });
  const launcherPath = path.join(input.dir, "agent-device-launcher.mjs");
  yield* fs.writeFileString(launcherPath, launcherSource(node, input.entryPath, input.boundArgs));
  if (platform === "win32") {
    const script = `@echo off\r\nsetlocal EnableExtensions DisableDelayedExpansion\r\nif not exist "${node}" (echo ${STALE_RUNTIME} 1>&2 & exit /b 1)\r\nset "ELECTRON_RUN_AS_NODE=1"\r\n"${node}" "${launcherPath}" %*\r\n`;
    const scriptPath = path.join(input.dir, "agent-device.cmd");
    yield* fs.writeFileString(scriptPath, script);
    return scriptPath;
  }
  const script = `#!/bin/sh\n[ -x ${quoteShell(node)} ] || { echo ${quoteShell(STALE_RUNTIME)} >&2; exit 1; }\nexport ELECTRON_RUN_AS_NODE=1\nexec ${quoteShell(node)} ${quoteShell(launcherPath)} "$@"\n`;
  const scriptPath = path.join(input.dir, "agent-device");
  yield* fs.writeFileString(scriptPath, script);
  yield* fs.chmod(scriptPath, 0o755);
  return scriptPath;
});

/** The unbound launcher: every call needs the device's target flags. */
export const ensureAgentDeviceShim = Effect.fn("AgentDeviceShim.ensure")(function* (input: {
  readonly entryPath: string;
  readonly stateDir: string;
}) {
  const path = yield* Path.Path;
  const shimDir = path.join(input.stateDir, SHIM_DIR);
  yield* writeLauncher({ dir: shimDir, entryPath: input.entryPath, boundArgs: [] });
  return shimDir;
});

/**
 * A launcher for one agent-device session with its target flags built in, so
 * agents pass none, and a shell that does not word-split a variable cannot mangle them.
 */
export const bindAgentDeviceLauncher = Effect.fn("AgentDeviceShim.bind")(function* (input: {
  readonly entryPath: string;
  readonly stateDir: string;
  readonly session: string;
  readonly targetArgs: ReadonlyArray<string>;
}) {
  const path = yield* Path.Path;
  return yield* writeLauncher({
    dir: path.join(input.stateDir, TARGETS_DIR, input.session),
    entryPath: input.entryPath,
    boundArgs: input.targetArgs,
  });
});

/** Removes the bound launchers of agent-device sessions not in `live`. */
export const pruneAgentDeviceLaunchers = Effect.fn("AgentDeviceShim.prune")(function* (input: {
  readonly stateDir: string;
  readonly live: ReadonlySet<string>;
}) {
  const fs = yield* FileSystem.FileSystem;
  const path = yield* Path.Path;
  const dir = path.join(input.stateDir, TARGETS_DIR);
  const sessions = yield* fs.readDirectory(dir).pipe(Effect.orElseSucceed(() => []));
  yield* Effect.forEach(
    sessions.filter((session) => !input.live.has(session)),
    (session) => fs.remove(path.join(dir, session), { recursive: true, force: true }),
    { discard: true },
  ).pipe(Effect.ignore);
});

export type AgentDeviceCloseOutcome = "closed" | "no-session" | "failed" | "timed-out";

/** What a launcher's `close` did; only a failure or timeout can leave the device claimed. */
export const agentDeviceCloseOutcome = (result: {
  readonly code: number | null;
  readonly stderr: string;
  readonly timedOut: boolean;
}): AgentDeviceCloseOutcome =>
  result.timedOut
    ? "timed-out"
    : result.code === 0
      ? "closed"
      : result.stderr.includes("SESSION_NOT_FOUND")
        ? "no-session"
        : "failed";