| author | |
| committer | |
| log | 7734c6d71b6dd5df9c9b4856cff8e419b8837fe8 |
| tree | 505220ed3094e21eb14a87e2a68f21a9b70f6466 |
| parent | e022fa430edbf8d448efe0f34303ff290ebb3dc0 |
| signature | Signed by PGP key B5690EEEBB952194 |
11 files changed, 376 insertions(+), 48 deletions(-)
apps/server/src/device/AgentDeviceShim.ts+26-2| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | // @effect-diagnostics preferSchemaOverJson:off - JSON string literals embed paths safely into generated JavaScript. | ||
| 1 | /** | 2 | /** |
| 2 | * A directory holding an `agent-device` launcher that runs the pinned install | 3 | * A directory holding an `agent-device` launcher that runs the pinned install |
| 3 | * with the server's Node. Prepended to provider subprocess PATHs so the agent | 4 | * with the server's Node. Prepended to provider subprocess PATHs so the agent |
| ... | @@ -22,11 +23,34 @@ export const ensureAgentDeviceShim = Effect.fn("AgentDeviceShim.ensure")(functio | ... | @@ -22,11 +23,34 @@ export const ensureAgentDeviceShim = Effect.fn("AgentDeviceShim.ensure")(functio |
| 22 | const shimDir = path.join(input.stateDir, SHIM_DIR); | 23 | const shimDir = path.join(input.stateDir, SHIM_DIR); |
| 23 | yield* fs.makeDirectory(shimDir, { recursive: true }); | 24 | yield* fs.makeDirectory(shimDir, { recursive: true }); |
| 24 | const node = process.execPath; | 25 | const node = process.execPath; |
| 26 | const launcherPath = path.join(shimDir, "agent-device-launcher.mjs"); | ||
| 27 | yield* fs.writeFileString( | ||
| 28 | launcherPath, | ||
| 29 | `import { spawn } from "node:child_process"; | ||
| 30 | const args = process.argv.slice(2); | ||
| 31 | const informational = args.length === 1 && ["help", "--help", "-h", "--version", "version"].includes(args[0]); | ||
| 32 | const hasValue = flag => { const index = args.indexOf(flag); return index >= 0 && !!args[index + 1] && !args[index + 1].startsWith("--"); }; | ||
| 33 | if (!informational && !(hasValue("--config") && hasValue("--session"))) { | ||
| 34 | console.error("Call device_open first and include its --config and --session flags."); | ||
| 35 | process.exit(1); | ||
| 36 | } | ||
| 37 | const env = { ...process.env }; | ||
| 38 | delete env.AGENT_DEVICE_DAEMON_BASE_URL; | ||
| 39 | delete env.AGENT_DEVICE_DAEMON_AUTH_TOKEN; | ||
| 40 | delete env.AGENT_DEVICE_CONFIG; | ||
| 41 | const child = spawn(${JSON.stringify(node)}, [${JSON.stringify(entryPath)}, ...args], { stdio: "inherit", env }); | ||
| 42 | child.on("error", error => { console.error(error.message); process.exitCode = 1; }); | ||
| 43 | child.on("exit", code => { process.exitCode = code ?? 1; }); | ||
| 44 | `, | ||
| 45 | ); | ||
| 25 | if (platform === "win32") { | 46 | if (platform === "win32") { |
| 26 | const script = `@echo off\r\n"${node}" "${entryPath}" %*\r\n`; | 47 | const script = `@echo off\r\n"${node}" "${launcherPath}" %*\r\n`; |
| 27 | yield* fs.writeFileString(path.join(shimDir, "agent-device.cmd"), script); | 48 | yield* fs.writeFileString(path.join(shimDir, "agent-device.cmd"), script); |
| 28 | } else { | 49 | } else { |
| 29 | const script = `#!/bin/sh\nexec "${node}" "${entryPath}" "$@"\n`; | 50 | const command = [node, launcherPath] |
| 51 | .map((value) => "'" + value.replaceAll("'", "'\"'\"'") + "'") | ||
| 52 | .join(" "); | ||
| 53 | const script = `#!/bin/sh\nexec ${command} "$@"\n`; | ||
| 30 | const shimPath = path.join(shimDir, "agent-device"); | 54 | const shimPath = path.join(shimDir, "agent-device"); |
| 31 | yield* fs.writeFileString(shimPath, script); | 55 | yield* fs.writeFileString(shimPath, script); |
| 32 | yield* fs.chmod(shimPath, 0o755); | 56 | yield* fs.chmod(shimPath, 0o755); |
apps/server/src/device/AgentDeviceTarget.test.ts created+89| ... | @@ -0,0 +1,89 @@ | ||
| 1 | // @effect-diagnostics nodeBuiltinImport:off - exercises concurrent real CLI subprocesses. | ||
| 2 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| 3 | import { describe, expect, it } from "@effect/vitest"; | ||
| 4 | import * as NodeChildProcess from "node:child_process"; | ||
| 5 | import * as NodeUtil from "node:util"; | ||
| 6 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 7 | import * as Effect from "effect/Effect"; | ||
| 8 | import * as FileSystem from "effect/FileSystem"; | ||
| 9 | import * as Path from "effect/Path"; | ||
| 10 | import { ensureAgentDeviceShim } from "./AgentDeviceShim.ts"; | ||
| 11 | import { | ||
| 12 | agentDeviceConfigPath, | ||
| 13 | agentDeviceSession, | ||
| 14 | writeAgentDeviceConfig, | ||
| 15 | } from "./AgentDeviceTarget.ts"; | ||
| 16 | |||
| 17 | const exec = NodeUtil.promisify(NodeChildProcess.execFile); | ||
| 18 | |||
| 19 | describe("host-bound agent commands", () => { | ||
| 20 | it.effect("runs two hosts concurrently and only updates the reconnected host", () => | ||
| 21 | Effect.gen(function* () { | ||
| 22 | const fs = yield* FileSystem.FileSystem; | ||
| 23 | const path = yield* Path.Path; | ||
| 24 | const temp = yield* fs.makeTempDirectoryScoped({ prefix: "t3-device-target-" }); | ||
| 25 | const platform = yield* HostProcessPlatform; | ||
| 26 | const dir = path.join( | ||
| 27 | temp, | ||
| 28 | platform === "win32" ? "paths with spaces" : "quotes '\" $HOME `literal`", | ||
| 29 | ); | ||
| 30 | yield* fs.makeDirectory(dir); | ||
| 31 | const entryPath = path.join(dir, "cli.mjs"); | ||
| 32 | yield* fs.writeFileString( | ||
| 33 | entryPath, | ||
| 34 | `import { readFileSync } from 'node:fs'; | ||
| 35 | const args = process.argv.slice(2); | ||
| 36 | console.log(readFileSync(args[args.indexOf('--config') + 1], 'utf8')); | ||
| 37 | if (process.env.AGENT_DEVICE_DAEMON_BASE_URL) process.exit(2);`, | ||
| 38 | ); | ||
| 39 | const shim = yield* ensureAgentDeviceShim({ entryPath, stateDir: dir }); | ||
| 40 | const files = ["mini", "android"].map((host) => agentDeviceConfigPath(dir, host, path)); | ||
| 41 | for (const [index, file] of files.entries()) | ||
| 42 | yield* writeAgentDeviceConfig(file, { | ||
| 43 | baseUrl: `http://127.0.0.1:${1000 + index}`, | ||
| 44 | token: `token-${index}`, | ||
| 45 | entryPath, | ||
| 46 | }); | ||
| 47 | const invoke = (file: string) => | ||
| 48 | exec( | ||
| 49 | platform === "win32" ? process.execPath : path.join(shim, "agent-device"), | ||
| 50 | [ | ||
| 51 | ...(platform === "win32" ? [path.join(shim, "agent-device-launcher.mjs")] : []), | ||
| 52 | "snapshot", | ||
| 53 | "--config", | ||
| 54 | file, | ||
| 55 | "--session", | ||
| 56 | "test-session", | ||
| 57 | ], | ||
| 58 | { env: { ...process.env, AGENT_DEVICE_DAEMON_BASE_URL: "http://wrong-host" } }, | ||
| 59 | ).then((result) => JSON.parse(result.stdout)); | ||
| 60 | expect(yield* Effect.promise(() => Promise.all(files.map(invoke)))).toEqual([ | ||
| 61 | { daemonBaseUrl: "http://127.0.0.1:1000", daemonAuthToken: "token-0" }, | ||
| 62 | { daemonBaseUrl: "http://127.0.0.1:1001", daemonAuthToken: "token-1" }, | ||
| 63 | ]); | ||
| 64 | const second = yield* fs.readFileString(files[1]!); | ||
| 65 | yield* writeAgentDeviceConfig(files[0]!, { | ||
| 66 | baseUrl: "http://127.0.0.1:2000", | ||
| 67 | token: "new", | ||
| 68 | entryPath, | ||
| 69 | }); | ||
| 70 | expect((yield* Effect.promise(() => invoke(files[0]!))).daemonAuthToken).toBe("new"); | ||
| 71 | expect(yield* fs.readFileString(files[1]!)).toBe(second); | ||
| 72 | expect(agentDeviceSession("thread", "mini", "same-id")).not.toBe( | ||
| 73 | agentDeviceSession("thread", "android", "same-id"), | ||
| 74 | ); | ||
| 75 | for (const args of [ | ||
| 76 | ["snapshot"], | ||
| 77 | ["snapshot", "--config", files[0]!], | ||
| 78 | ["snapshot", "--config", "help"], | ||
| 79 | ["snapshot", "--config", files[0]!, "--session"], | ||
| 80 | ]) { | ||
| 81 | yield* Effect.promise(() => | ||
| 82 | expect( | ||
| 83 | exec(process.execPath, [path.join(shim, "agent-device-launcher.mjs"), ...args]), | ||
| 84 | ).rejects.toThrow("Call device_open first"), | ||
| 85 | ); | ||
| 86 | } | ||
| 87 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), | ||
| 88 | ); | ||
| 89 | }); | ||
apps/server/src/device/AgentDeviceTarget.ts created+43| ... | @@ -0,0 +1,43 @@ | ||
| 1 | import * as NodeCrypto from "node:crypto"; | ||
| 2 | import * as Schema from "effect/Schema"; | ||
| 3 | import * as Effect from "effect/Effect"; | ||
| 4 | import * as FileSystem from "effect/FileSystem"; | ||
| 5 | import * as Path from "effect/Path"; | ||
| 6 | |||
| 7 | import type { AgentDeviceEndpoint } from "./DeviceHost.ts"; | ||
| 8 | |||
| 9 | const encodeEndpoint = Schema.encodeEffect( | ||
| 10 | Schema.fromJsonString( | ||
| 11 | Schema.Struct({ daemonBaseUrl: Schema.String, daemonAuthToken: Schema.String }), | ||
| 12 | ), | ||
| 13 | ); | ||
| 14 | |||
| 15 | const key = (value: string) => | ||
| 16 | NodeCrypto.createHash("sha256").update(value).digest("hex").slice(0, 24); | ||
| 17 | |||
| 18 | /** A stable file per host lets forwarded endpoints change without retargeting other commands. */ | ||
| 19 | export const agentDeviceConfigPath = (stateDir: string, hostId: string, path: Path.Path) => | ||
| 20 | path.join(stateDir, "device", "hosts", `${key(hostId)}.json`); | ||
| 21 | |||
| 22 | export const agentDeviceSession = (threadId: string, hostId: string, deviceId: string) => | ||
| 23 | `t3-${key(JSON.stringify([threadId, hostId, deviceId]))}`; | ||
| 24 | |||
| 25 | export const writeAgentDeviceConfig = Effect.fn("AgentDeviceTarget.writeConfig")(function* ( | ||
| 26 | file: string, | ||
| 27 | endpoint: AgentDeviceEndpoint, | ||
| 28 | ) { | ||
| 29 | const fs = yield* FileSystem.FileSystem; | ||
| 30 | const path = yield* Path.Path; | ||
| 31 | yield* fs.makeDirectory(path.dirname(file), { recursive: true }); | ||
| 32 | const content = yield* encodeEndpoint({ | ||
| 33 | daemonBaseUrl: endpoint.baseUrl, | ||
| 34 | daemonAuthToken: endpoint.token, | ||
| 35 | }); | ||
| 36 | if ((yield* fs.readFileString(file).pipe(Effect.orElseSucceed(() => ""))) === content) return; | ||
| 37 | const temporary = yield* fs.makeTempFile({ directory: path.dirname(file), prefix: ".endpoint-" }); | ||
| 38 | yield* Effect.gen(function* () { | ||
| 39 | yield* fs.chmod(temporary, 0o600); | ||
| 40 | yield* fs.writeFileString(temporary, content); | ||
| 41 | yield* fs.rename(temporary, file); | ||
| 42 | }).pipe(Effect.ensuring(fs.remove(temporary, { force: true }).pipe(Effect.ignore))); | ||
| 43 | }); | ||
apps/server/src/device/DeviceService.test.ts+2-2| ... | @@ -16,7 +16,7 @@ import { HttpClient, HttpClientResponse } from "effect/unstable/http"; | ... | @@ -16,7 +16,7 @@ import { HttpClient, HttpClientResponse } from "effect/unstable/http"; |
| 16 | import { ServerSettingsService } from "../serverSettings.ts"; | 16 | import { ServerSettingsService } from "../serverSettings.ts"; |
| 17 | import * as DeviceHost from "./DeviceHost.ts"; | 17 | import * as DeviceHost from "./DeviceHost.ts"; |
| 18 | 18 | ||
| 19 | import { type DeviceService, make, stateStream } from "./DeviceService.ts"; | 19 | import { type DeviceService, makeWithHosts, stateStream } from "./DeviceService.ts"; |
| 20 | 20 | ||
| 21 | const baseState: DeviceServiceState = { | 21 | const baseState: DeviceServiceState = { |
| 22 | hosts: [], | 22 | hosts: [], |
| ... | @@ -108,7 +108,7 @@ const fixture = Effect.fn("fixture")(function* ( | ... | @@ -108,7 +108,7 @@ const fixture = Effect.fn("fixture")(function* ( |
| 108 | starts.push("stop"); | 108 | starts.push("stop"); |
| 109 | }), | 109 | }), |
| 110 | }; | 110 | }; |
| 111 | const service = yield* make.pipe( | 111 | const service = yield* makeWithHosts(new Map([[host.id, host]])).pipe( |
| 112 | Effect.provideService(DeviceHost.DeviceHost, host), | 112 | Effect.provideService(DeviceHost.DeviceHost, host), |
| 113 | Effect.provideService( | 113 | Effect.provideService( |
| 114 | ServerSettingsService, | 114 | ServerSettingsService, |
apps/server/src/device/DeviceService.ts+88-2| ... | @@ -33,6 +33,15 @@ import { | ... | @@ -33,6 +33,15 @@ import { |
| 33 | LOCAL_DEVICE_HOST_ID, | 33 | LOCAL_DEVICE_HOST_ID, |
| 34 | type ThreadId, | 34 | type ThreadId, |
| 35 | } from "@t3tools/contracts"; | 35 | } from "@t3tools/contracts"; |
| 36 | import * as FileSystem from "effect/FileSystem"; | ||
| 37 | import * as Path from "effect/Path"; | ||
| 38 | import { ensureAgentDevice } from "./DeviceToolchain.ts"; | ||
| 39 | import * as ServerConfig from "../config.ts"; | ||
| 40 | import { | ||
| 41 | agentDeviceConfigPath, | ||
| 42 | agentDeviceSession, | ||
| 43 | writeAgentDeviceConfig, | ||
| 44 | } from "./AgentDeviceTarget.ts"; | ||
| 36 | import * as Context from "effect/Context"; | 45 | import * as Context from "effect/Context"; |
| 37 | import * as DateTime from "effect/DateTime"; | 46 | import * as DateTime from "effect/DateTime"; |
| 38 | import * as Duration from "effect/Duration"; | 47 | import * as Duration from "effect/Duration"; |
| ... | @@ -49,6 +58,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab | ... | @@ -49,6 +58,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab |
| 49 | import * as ServerSettings from "../serverSettings.ts"; | 58 | import * as ServerSettings from "../serverSettings.ts"; |
| 50 | 59 | ||
| 51 | import { readDeviceDetail, runDeviceAction } from "./DeviceActions.ts"; | 60 | import { readDeviceDetail, runDeviceAction } from "./DeviceActions.ts"; |
| 61 | import * as ProcessRunner from "../processRunner.ts"; | ||
| 52 | import * as DeviceHost from "./DeviceHost.ts"; | 62 | import * as DeviceHost from "./DeviceHost.ts"; |
| 53 | import * as LocalDeviceHost from "./LocalDeviceHost.ts"; | 63 | import * as LocalDeviceHost from "./LocalDeviceHost.ts"; |
| 54 | 64 | ||
| ... | @@ -94,6 +104,12 @@ export interface DeviceAgentReadiness extends DeviceReadiness { | ... | @@ -94,6 +104,12 @@ export interface DeviceAgentReadiness extends DeviceReadiness { |
| 94 | export class DeviceService extends Context.Service< | 104 | export class DeviceService extends Context.Service< |
| 95 | DeviceService, | 105 | DeviceService, |
| 96 | { | 106 | { |
| 107 | readonly agentCli: Effect.Effect<string, DeviceError>; | ||
| 108 | readonly agentTarget: (input: { | ||
| 109 | threadId: ThreadId; | ||
| 110 | hostId: DeviceHostId; | ||
| 111 | deviceId: DeviceId; | ||
| 112 | }) => Effect.Effect<ReadonlyArray<string>, DeviceError>; | ||
| 97 | readonly state: Effect.Effect<DeviceServiceState>; | 113 | readonly state: Effect.Effect<DeviceServiceState>; |
| 98 | readonly subscribe: Effect.Effect<PubSub.Subscription<DeviceServiceState>, never, Scope.Scope>; | 114 | readonly subscribe: Effect.Effect<PubSub.Subscription<DeviceServiceState>, never, Scope.Scope>; |
| 99 | readonly configure: ( | 115 | readonly configure: ( |
| ... | @@ -138,6 +154,16 @@ const vendorPrefix = (platform: DevicePlatform) => | ... | @@ -138,6 +154,16 @@ const vendorPrefix = (platform: DevicePlatform) => |
| 138 | 154 | ||
| 139 | export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* ( | 155 | export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* ( |
| 140 | hosts: ReadonlyMap<DeviceHostId, DeviceHost.DeviceHost["Service"]>, | 156 | hosts: ReadonlyMap<DeviceHostId, DeviceHost.DeviceHost["Service"]>, |
| 157 | configureAgent: ( | ||
| 158 | hostId: DeviceHostId, | ||
| 159 | ready: DeviceHost.DeviceHostAgentReady, | ||
| 160 | ) => Effect.Effect<string, DeviceError> = (hostId) => | ||
| 161 | Effect.fail( | ||
| 162 | new DeviceHostUnavailableError({ | ||
| 163 | hostId, | ||
| 164 | reason: "Agent configuration is unavailable in this device service.", | ||
| 165 | }), | ||
| 166 | ), | ||
| 141 | ) { | 167 | ) { |
| 142 | const settings = yield* ServerSettings.ServerSettingsService; | 168 | const settings = yield* ServerSettings.ServerSettingsService; |
| 143 | const lifecycleLock = yield* Semaphore.make(1); | 169 | const lifecycleLock = yield* Semaphore.make(1); |
| ... | @@ -733,6 +759,29 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* | ... | @@ -733,6 +759,29 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* |
| 733 | ); | 759 | ); |
| 734 | 760 | ||
| 735 | return DeviceService.of({ | 761 | return DeviceService.of({ |
| 762 | agentCli: Effect.fail( | ||
| 763 | new DeviceHostUnavailableError({ | ||
| 764 | hostId: LOCAL_DEVICE_HOST_ID, | ||
| 765 | reason: "Agent CLI installation is unavailable in this device service.", | ||
| 766 | }), | ||
| 767 | ), | ||
| 768 | agentTarget: (input) => | ||
| 769 | Effect.gen(function* () { | ||
| 770 | const ready = yield* agentReadinessIfSupported(input.hostId); | ||
| 771 | if (!ready) | ||
| 772 | return yield* new DeviceHostUnavailableError({ | ||
| 773 | hostId: input.hostId, | ||
| 774 | reason: | ||
| 775 | "Agent device access requires enabled device support, agent access, and an available simulator platform on this host.", | ||
| 776 | }); | ||
| 777 | const configPath = yield* configureAgent(input.hostId, ready); | ||
| 778 | return [ | ||
| 779 | "--config", | ||
| 780 | configPath, | ||
| 781 | "--session", | ||
| 782 | agentDeviceSession(input.threadId, input.hostId, input.deviceId), | ||
| 783 | ]; | ||
| 784 | }), | ||
| 736 | state: SynchronizedRef.get(stateRef).pipe(Effect.map(({ state }) => state)), | 785 | state: SynchronizedRef.get(stateRef).pipe(Effect.map(({ state }) => state)), |
| 737 | subscribe: PubSub.subscribe(statePubSub), | 786 | subscribe: PubSub.subscribe(statePubSub), |
| 738 | configure, | 787 | configure, |
| ... | @@ -751,9 +800,46 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* | ... | @@ -751,9 +800,46 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* |
| 751 | }); | 800 | }); |
| 752 | }); | 801 | }); |
| 753 | 802 | ||
| 803 | /** @public Service construction is part of the canonical Effect module API. */ | ||
| 754 | export const make = Effect.gen(function* () { | 804 | export const make = Effect.gen(function* () { |
| 755 | const host = yield* DeviceHost.DeviceHost; | 805 | const localHost = yield* DeviceHost.DeviceHost; |
| 756 | return yield* makeWithHosts(new Map([[host.id, host]])); | 806 | const config = yield* ServerConfig.ServerConfig; |
| 807 | const fs = yield* FileSystem.FileSystem; | ||
| 808 | const path = yield* Path.Path; | ||
| 809 | const runner = yield* ProcessRunner.ProcessRunner; | ||
| 810 | const service = yield* makeWithHosts(new Map([[localHost.id, localHost]]), (hostId, ready) => { | ||
| 811 | const file = agentDeviceConfigPath(config.stateDir, hostId, path); | ||
| 812 | return writeAgentDeviceConfig(file, ready.agentDevice).pipe( | ||
| 813 | Effect.provideService(FileSystem.FileSystem, fs), | ||
| 814 | Effect.provideService(Path.Path, path), | ||
| 815 | Effect.mapError( | ||
| 816 | (cause) => | ||
| 817 | new DeviceOperationError({ | ||
| 818 | operation: "configure agent", | ||
| 819 | reason: "settings_failed", | ||
| 820 | cause, | ||
| 821 | }), | ||
| 822 | ), | ||
| 823 | Effect.as(file), | ||
| 824 | ); | ||
| 825 | }); | ||
| 826 | return { | ||
| 827 | ...service, | ||
| 828 | agentCli: ensureAgentDevice(config.baseDir).pipe( | ||
| 829 | Effect.provideService(FileSystem.FileSystem, fs), | ||
| 830 | Effect.provideService(Path.Path, path), | ||
| 831 | Effect.provideService(ProcessRunner.ProcessRunner, runner), | ||
| 832 | Effect.map((tool) => tool.entryPath), | ||
| 833 | Effect.mapError( | ||
| 834 | (error) => | ||
| 835 | new DeviceOperationError({ | ||
| 836 | operation: "install agent CLI", | ||
| 837 | reason: "command_failed", | ||
| 838 | cause: error, | ||
| 839 | }), | ||
| 840 | ), | ||
| 841 | ), | ||
| 842 | }; | ||
| 757 | }); | 843 | }); |
| 758 | 844 | ||
| 759 | export const layer = Layer.effect(DeviceService, make).pipe(Layer.provide(LocalDeviceHost.layer)); | 845 | export const layer = Layer.effect(DeviceService, make).pipe(Layer.provide(LocalDeviceHost.layer)); |
apps/server/src/mcp/McpDeviceToolkit.test.ts+48-1| ... | @@ -1,10 +1,16 @@ | ... | @@ -1,10 +1,16 @@ |
| 1 | import { expect, it } from "@effect/vitest"; | 1 | import { expect, it } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { EnvironmentId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; | 3 | import { |
| 4 | DeviceHostUnavailableError, | ||
| 5 | EnvironmentId, | ||
| 6 | ProviderInstanceId, | ||
| 7 | ThreadId, | ||
| 8 | } from "@t3tools/contracts"; | ||
| 4 | import * as Effect from "effect/Effect"; | 9 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; | 10 | import * as Layer from "effect/Layer"; |
| 6 | import { McpSchema, McpServer } from "effect/unstable/ai"; | 11 | import { McpSchema, McpServer } from "effect/unstable/ai"; |
| 7 | 12 | ||
| 13 | import * as ServerConfig from "../config.ts"; | ||
| 8 | import * as DeviceService from "../device/DeviceService.ts"; | 14 | import * as DeviceService from "../device/DeviceService.ts"; |
| 9 | import * as McpHttpServer from "./McpHttpServer.ts"; | 15 | import * as McpHttpServer from "./McpHttpServer.ts"; |
| 10 | import * as McpInvocationContext from "./McpInvocationContext.ts"; | 16 | import * as McpInvocationContext from "./McpInvocationContext.ts"; |
| ... | @@ -85,11 +91,14 @@ const DeviceServiceMock = Layer.mock(DeviceService.DeviceService)({ | ... | @@ -85,11 +91,14 @@ const DeviceServiceMock = Layer.mock(DeviceService.DeviceService)({ |
| 85 | sessionsForThread: () => Effect.succeed([]), | 91 | sessionsForThread: () => Effect.succeed([]), |
| 86 | screenshot: () => Effect.succeed({ device, png }), | 92 | screenshot: () => Effect.succeed({ device, png }), |
| 87 | close: () => Effect.void, | 93 | close: () => Effect.void, |
| 94 | agentCli: Effect.succeed("/cli"), | ||
| 95 | agentTarget: () => Effect.succeed(["--config", "/host.json", "--session", "thread-device"]), | ||
| 88 | }); | 96 | }); |
| 89 | 97 | ||
| 90 | const TestLayer = McpHttpServer.DeviceToolkitRegistrationLive.pipe( | 98 | const TestLayer = McpHttpServer.DeviceToolkitRegistrationLive.pipe( |
| 91 | Layer.provideMerge(McpServer.McpServer.layer), | 99 | Layer.provideMerge(McpServer.McpServer.layer), |
| 92 | Layer.provideMerge(DeviceServiceMock), | 100 | Layer.provideMerge(DeviceServiceMock), |
| 101 | Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-device-toolkit-test-" })), | ||
| 93 | Layer.provide(NodeServices.layer), | 102 | Layer.provide(NodeServices.layer), |
| 94 | ); | 103 | ); |
| 95 | 104 | ||
| ... | @@ -126,3 +135,41 @@ it.effect("registers the device tools and returns the screenshot as image conten | ... | @@ -126,3 +135,41 @@ it.effect("registers the device tools and returns the screenshot as image conten |
| 126 | }), | 135 | }), |
| 127 | ).pipe(Effect.provide(TestLayer)), | 136 | ).pipe(Effect.provide(TestLayer)), |
| 128 | ); | 137 | ); |
| 138 | |||
| 139 | it.effect("rejects unavailable agent access before booting or opening a device", () => { | ||
| 140 | const unavailable = Layer.mock(DeviceService.DeviceService)({ | ||
| 141 | list: Effect.succeed(state), | ||
| 142 | agentTarget: () => | ||
| 143 | Effect.fail( | ||
| 144 | new DeviceHostUnavailableError({ hostId: "local", reason: "Agent access is disabled." }), | ||
| 145 | ), | ||
| 146 | open: () => Effect.die("Must not boot or register a device when agent access fails"), | ||
| 147 | }); | ||
| 148 | return Effect.gen(function* () { | ||
| 149 | const server = yield* McpServer.McpServer; | ||
| 150 | const result = yield* server | ||
| 151 | .callTool({ name: "device_open", arguments: { platform: "ios" } }) | ||
| 152 | .pipe( | ||
| 153 | Effect.provideService(McpInvocationContext.McpInvocationContext, invocation(["device"])), | ||
| 154 | Effect.provideService(McpSchema.McpServerClient, client), | ||
| 155 | ); | ||
| 156 | expect(result.isError).toBe(true); | ||
| 157 | expect(result.content).toEqual( | ||
| 158 | expect.arrayContaining([ | ||
| 159 | expect.objectContaining({ | ||
| 160 | type: "text", | ||
| 161 | text: expect.stringContaining("Agent access is disabled."), | ||
| 162 | }), | ||
| 163 | ]), | ||
| 164 | ); | ||
| 165 | }).pipe( | ||
| 166 | Effect.scoped, | ||
| 167 | Effect.provide( | ||
| 168 | McpHttpServer.DeviceToolkitRegistrationLive.pipe( | ||
| 169 | Layer.provideMerge(McpServer.McpServer.layer), | ||
| 170 | Layer.provide(unavailable), | ||
| 171 | Layer.provide(NodeServices.layer), | ||
| 172 | ), | ||
| 173 | ), | ||
| 174 | ); | ||
| 175 | }); |
apps/server/src/mcp/toolkits/device/handlers.test.ts+13| ... | @@ -30,6 +30,19 @@ describe("device tool helpers", () => { | ... | @@ -30,6 +30,19 @@ describe("device tool helpers", () => { |
| 30 | expect(text).toContain("XCTest runner"); | 30 | expect(text).toContain("XCTest runner"); |
| 31 | }); | 31 | }); |
| 32 | 32 | ||
| 33 | it("uses the absolute launcher in every quick-start command", () => { | ||
| 34 | const text = agentDeviceQuickStart( | ||
| 35 | device, | ||
| 36 | ["--session", "thread-1", "--config", "/tmp/host.json"], | ||
| 37 | "/tmp/t3 tools/agent-device", | ||
| 38 | ); | ||
| 39 | expect(text).toContain( | ||
| 40 | "'/tmp/t3 tools/agent-device' snapshot -i --session thread-1 --config /tmp/host.json", | ||
| 41 | ); | ||
| 42 | expect(text).not.toContain(" agent-device "); | ||
| 43 | expect(text).not.toContain("is on PATH"); | ||
| 44 | }); | ||
| 45 | |||
| 33 | it("reads PNG dimensions from the IHDR chunk", () => { | 46 | it("reads PNG dimensions from the IHDR chunk", () => { |
| 34 | const png = new Uint8Array(24); | 47 | const png = new Uint8Array(24); |
| 35 | new DataView(png.buffer).setUint32(0, 0x89504e47); | 48 | new DataView(png.buffer).setUint32(0, 0x89504e47); |
apps/server/src/mcp/toolkits/device/handlers.ts+54-12| ... | @@ -8,6 +8,10 @@ import { | ... | @@ -8,6 +8,10 @@ import { |
| 8 | LOCAL_DEVICE_HOST_ID, | 8 | LOCAL_DEVICE_HOST_ID, |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import * as Effect from "effect/Effect"; | 10 | import * as Effect from "effect/Effect"; |
| 11 | import * as Path from "effect/Path"; | ||
| 12 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| 13 | import { ServerConfig } from "../../../config.ts"; | ||
| 14 | import { ensureAgentDeviceShim } from "../../../device/AgentDeviceShim.ts"; | ||
| 11 | 15 | ||
| 12 | import * as DeviceService from "../../../device/DeviceService.ts"; | 16 | import * as DeviceService from "../../../device/DeviceService.ts"; |
| 13 | import * as McpInvocationContext from "../../McpInvocationContext.ts"; | 17 | import * as McpInvocationContext from "../../McpInvocationContext.ts"; |
| ... | @@ -26,24 +30,37 @@ export function agentDeviceTargetArgs(device: DeviceSummary): ReadonlyArray<stri | ... | @@ -26,24 +30,37 @@ export function agentDeviceTargetArgs(device: DeviceSummary): ReadonlyArray<stri |
| 26 | * rather than in the always-on prompt block; threads that never open a device | 30 | * rather than in the always-on prompt block; threads that never open a device |
| 27 | * never pay for it. | 31 | * never pay for it. |
| 28 | */ | 32 | */ |
| 29 | export function agentDeviceQuickStart(device: DeviceSummary): string { | 33 | export function agentDeviceQuickStart( |
| 30 | const target = agentDeviceTargetArgs(device).join(" "); | 34 | device: DeviceSummary, |
| 35 | targetArgs = agentDeviceTargetArgs(device), | ||
| 36 | command = "agent-device", | ||
| 37 | ): string { | ||
| 38 | const executable = /^[a-zA-Z0-9_./:-]+$/.test(command) | ||
| 39 | ? command | ||
| 40 | : "'" + command.replaceAll("'", "'\"'\"'") + "'"; | ||
| 41 | const target = targetArgs | ||
| 42 | .map((arg) => | ||
| 43 | /^[a-zA-Z0-9_./:-]+$/.test(arg) ? arg : "'" + arg.replaceAll("'", "'\"'\"'") + "'", | ||
| 44 | ) | ||
| 45 | .join(" "); | ||
| 31 | const platformNotes = | 46 | const platformNotes = |
| 32 | device.platform === "ios" | 47 | device.platform === "ios" |
| 33 | ? "First use builds an XCTest runner and can take a couple of minutes; later commands are fast." | 48 | ? "First use builds an XCTest runner and can take a couple of minutes; later commands are fast." |
| 34 | : "The Android snapshot helper installs itself on first use."; | 49 | : "The Android snapshot helper installs itself on first use."; |
| 35 | return [ | 50 | return [ |
| 36 | `The user is watching ${device.name} (${device.version}) in the Device panel.`, | 51 | `The user is watching ${device.name} (${device.version}) in the Device panel.`, |
| 37 | `Drive it with the agent-device CLI, which is on PATH and already connected to this environment. Always pass ${target}.`, | 52 | `Drive it with ${executable}. Use this exact executable path; login shells may reset PATH. Always pass ${target}.`, |
| 38 | "Typical loop:", | 53 | "Typical loop:", |
| 39 | ` agent-device open <bundle-or-package-id> ${target} # or: open <app> <deep-link-url>`, | 54 | ` ${executable} open <bundle-or-package-id> ${target} # or: open <app> <deep-link-url>`, |
| 40 | ` agent-device snapshot -i ${target} # accessibility tree with @eN refs`, | 55 | ` ${executable} snapshot -i ${target} # accessibility tree with @eN refs`, |
| 41 | ` agent-device click @e3 ${target}`, | 56 | ` ${executable} click @e3 ${target}`, |
| 42 | ` agent-device fill @e5 "text" ${target}`, | 57 | ` ${executable} fill @e5 "text" ${target}`, |
| 43 | ` agent-device screenshot /tmp/shot.png ${target} # or call device_screenshot`, | 58 | ` ${executable} screenshot /tmp/shot.png ${target} # or call device_screenshot`, |
| 44 | ` agent-device install <app> <path-to-.app-or-.apk> ${target}`, | 59 | ` ${executable} install <app> <path-to-.app-or-.apk> ${target}`, |
| 45 | "Prefer snapshot refs over coordinates. Run `agent-device help` for workflow guides and `agent-device <command> --help` for flags.", | 60 | `Prefer snapshot refs over coordinates. Run ${executable} help for workflow guides and ${executable} <command> --help for flags.`, |
| 46 | "Do not call simctl, adb, xcrun, or serve-sim directly while these tools are attached; use agent-device.", | 61 | "Do not call simctl, adb, xcrun, or serve-sim directly while these tools are attached; use agent-device.", |
| 62 | "For remote hosts, arrange builds, app installation, and any Metro reverse forwarding yourself. T3 provides discovery, streaming, and control only.", | ||
| 63 | "Keep the returned --config and --session flags on every command. Other hosts can be used concurrently; opening one does not switch these commands.", | ||
| 47 | platformNotes, | 64 | platformNotes, |
| 48 | ].join("\n"); | 65 | ].join("\n"); |
| 49 | } | 66 | } |
| ... | @@ -136,6 +153,12 @@ const handlers = { | ... | @@ -136,6 +153,12 @@ const handlers = { |
| 136 | }); | 153 | }); |
| 137 | } | 154 | } |
| 138 | const target = yield* pickDevice(state.devices, input); | 155 | const target = yield* pickDevice(state.devices, input); |
| 156 | // Resolve consent and agent connectivity before booting or registering a session. | ||
| 157 | const agentArgs = yield* devices.agentTarget({ | ||
| 158 | threadId: scope.threadId, | ||
| 159 | hostId: target.hostId, | ||
| 160 | deviceId: target.id, | ||
| 161 | }); | ||
| 139 | const session = yield* devices.open({ | 162 | const session = yield* devices.open({ |
| 140 | threadId: scope.threadId, | 163 | threadId: scope.threadId, |
| 141 | hostId: target.hostId, | 164 | hostId: target.hostId, |
| ... | @@ -147,10 +170,29 @@ const handlers = { | ... | @@ -147,10 +170,29 @@ const handlers = { |
| 147 | after.devices.find( | 170 | after.devices.find( |
| 148 | (candidate) => candidate.hostId === session.hostId && candidate.id === session.deviceId, | 171 | (candidate) => candidate.hostId === session.hostId && candidate.id === session.deviceId, |
| 149 | ) ?? target; | 172 | ) ?? target; |
| 173 | const targetArgs = [...agentDeviceTargetArgs(device), ...agentArgs]; | ||
| 174 | const config = yield* ServerConfig; | ||
| 175 | const path = yield* Path.Path; | ||
| 176 | const platform = yield* HostProcessPlatform; | ||
| 177 | const shimDir = yield* ensureAgentDeviceShim({ | ||
| 178 | entryPath: yield* devices.agentCli, | ||
| 179 | stateDir: config.stateDir, | ||
| 180 | }).pipe( | ||
| 181 | Effect.mapError( | ||
| 182 | () => | ||
| 183 | new DeviceToolUnavailableError({ | ||
| 184 | reason: "Could not prepare the agent-device launcher.", | ||
| 185 | }), | ||
| 186 | ), | ||
| 187 | ); | ||
| 188 | const command = path.join( | ||
| 189 | shimDir, | ||
| 190 | platform === "win32" ? "agent-device.cmd" : "agent-device", | ||
| 191 | ); | ||
| 150 | return { | 192 | return { |
| 151 | device, | 193 | device, |
| 152 | agentDevice: { command: "agent-device", targetArgs: agentDeviceTargetArgs(device) }, | 194 | agentDevice: { command, targetArgs }, |
| 153 | quickStart: agentDeviceQuickStart(device), | 195 | quickStart: agentDeviceQuickStart(device, targetArgs, command), |
| 154 | }; | 196 | }; |
| 155 | }).pipe(Effect.mapError(toolError)), | 197 | }).pipe(Effect.mapError(toolError)), |
| 156 | device_screenshot: (input) => | 198 | device_screenshot: (input) => |
apps/server/src/mcp/toolkits/device/tools.ts+4-1| ... | @@ -8,6 +8,9 @@ import { | ... | @@ -8,6 +8,9 @@ import { |
| 8 | DeviceToolTargetInput, | 8 | DeviceToolTargetInput, |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import * as Schema from "effect/Schema"; | 10 | import * as Schema from "effect/Schema"; |
| 11 | import * as FileSystem from "effect/FileSystem"; | ||
| 12 | import * as Path from "effect/Path"; | ||
| 13 | import { ServerConfig } from "../../../config.ts"; | ||
| 11 | import { Tool, Toolkit } from "effect/unstable/ai"; | 14 | import { Tool, Toolkit } from "effect/unstable/ai"; |
| 12 | 15 | ||
| 13 | import * as McpInvocationContext from "../../McpInvocationContext.ts"; | 16 | import * as McpInvocationContext from "../../McpInvocationContext.ts"; |
| ... | @@ -48,7 +51,7 @@ const DeviceOpenTool = Tool.make("device_open", { | ... | @@ -48,7 +51,7 @@ const DeviceOpenTool = Tool.make("device_open", { |
| 48 | parameters: DeviceToolOpenInput, | 51 | parameters: DeviceToolOpenInput, |
| 49 | success: DeviceToolOpenResult, | 52 | success: DeviceToolOpenResult, |
| 50 | failure: DeviceToolError, | 53 | failure: DeviceToolError, |
| 51 | dependencies, | 54 | dependencies: [...dependencies, FileSystem.FileSystem, Path.Path, ServerConfig], |
| 52 | }) | 55 | }) |
| 53 | .annotate(Tool.Title, "Open device") | 56 | .annotate(Tool.Title, "Open device") |
| 54 | .annotate(Tool.Readonly, false) | 57 | .annotate(Tool.Readonly, false) |
apps/server/src/provider/CodexDeveloperInstructions.ts+1-1| ... | @@ -16,7 +16,7 @@ const T3_CODE_DEVICE_TOOL_INSTRUCTIONS = ` | ... | @@ -16,7 +16,7 @@ const T3_CODE_DEVICE_TOOL_INSTRUCTIONS = ` |
| 16 | 16 | ||
| 17 | ## T3 Code devices | 17 | ## T3 Code devices |
| 18 | 18 | ||
| 19 | The \`t3-code\` MCP server also exposes \`device_*\` tools for iOS Simulators and Android Emulators on this environment. For mobile verification, call \`device_list\`, then \`device_open\` so the user can watch the device in their Device panel; its result explains how to drive the device. Driving happens through the \`agent-device\` CLI, which is on PATH and already connected: prefer \`agent-device snapshot -i\` refs over coordinates, and use \`device_screenshot\` when you need to see the screen. Do not call simctl, adb, xcrun, or serve-sim directly while these tools are present. If \`device_list\` reports a platform as unavailable, say so instead of trying another route. | 19 | The \`t3-code\` MCP server also exposes \`device_*\` tools for iOS Simulators and Android Emulators on this environment. For mobile verification, call \`device_list\`, then \`device_open\` so the user can watch the device in their Device panel; its result explains how to drive the device. Driving happens through the \`agent-device\` CLI, which is on PATH. Keep the host config and session flags returned by \`device_open\` on every command so concurrent devices stay independent: prefer \`agent-device snapshot -i\` refs over coordinates, and use \`device_screenshot\` when you need to see the screen. Do not call simctl, adb, xcrun, or serve-sim directly while these tools are present. If \`device_list\` reports a platform as unavailable, say so instead of trying another route. |
| 20 | `; | 20 | `; |
| 21 | 21 | ||
| 22 | export interface T3CodeToolAvailability { | 22 | export interface T3CodeToolAvailability { |
apps/server/src/provider/Layers/ProviderService.ts+8-27| ... | @@ -54,8 +54,8 @@ import * as Stream from "effect/Stream"; | ... | @@ -54,8 +54,8 @@ import * as Stream from "effect/Stream"; |
| 54 | import { appendUserInputAttachmentPaths } from "../userInputAttachments.ts"; | 54 | import { appendUserInputAttachmentPaths } from "../userInputAttachments.ts"; |
| 55 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; | 55 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; |
| 56 | import * as ServerConfig from "../../config.ts"; | 56 | import * as ServerConfig from "../../config.ts"; |
| 57 | import { ensureAgentDeviceShim } from "../../device/AgentDeviceShim.ts"; | ||
| 58 | import * as DeviceService from "../../device/DeviceService.ts"; | 57 | import * as DeviceService from "../../device/DeviceService.ts"; |
| 58 | import { ensureAgentDeviceShim } from "../../device/AgentDeviceShim.ts"; | ||
| 59 | import type * as McpInvocationContext from "../../mcp/McpInvocationContext.ts"; | 59 | import type * as McpInvocationContext from "../../mcp/McpInvocationContext.ts"; |
| 60 | import { | 60 | import { |
| 61 | increment, | 61 | increment, |
| ... | @@ -254,8 +254,6 @@ export interface ProviderServiceLiveOptions { | ... | @@ -254,8 +254,6 @@ export interface ProviderServiceLiveOptions { |
| 254 | * test see whether a credential was requested at all. | 254 | * test see whether a credential was requested at all. |
| 255 | */ | 255 | */ |
| 256 | readonly issueMcpCredential?: typeof McpSessionRegistry.issueActiveMcpCredential; | 256 | readonly issueMcpCredential?: typeof McpSessionRegistry.issueActiveMcpCredential; |
| 257 | /** Overrides the device host lookup used to build the agent-device environment. */ | ||
| 258 | readonly deviceReadiness?: DeviceService.DeviceService["Service"]["agentReadinessIfSupported"]; | ||
| 259 | } | 257 | } |
| 260 | 258 | ||
| 261 | interface TurnAnalyticsMetadata { | 259 | interface TurnAnalyticsMetadata { |
| ... | @@ -484,14 +482,6 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( | ... | @@ -484,14 +482,6 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( |
| 484 | ); | 482 | ); |
| 485 | const issueMcpCredential = | 483 | const issueMcpCredential = |
| 486 | options?.issueMcpCredential ?? McpSessionRegistry.issueActiveMcpCredential; | 484 | options?.issueMcpCredential ?? McpSessionRegistry.issueActiveMcpCredential; |
| 487 | const deviceReadiness = | ||
| 488 | options?.deviceReadiness ?? | ||
| 489 | (() => | ||
| 490 | Effect.serviceOption(DeviceService.DeviceService).pipe( | ||
| 491 | Effect.flatMap((service) => | ||
| 492 | Option.isSome(service) ? service.value.agentReadinessIfSupported() : Effect.succeed(null), | ||
| 493 | ), | ||
| 494 | )); | ||
| 495 | const fileSystem = yield* FileSystem.FileSystem; | 485 | const fileSystem = yield* FileSystem.FileSystem; |
| 496 | const pathService = yield* Path.Path; | 486 | const pathService = yield* Path.Path; |
| 497 | const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>(); | 487 | const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>(); |
| ... | @@ -914,26 +904,19 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( | ... | @@ -914,26 +904,19 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( |
| 914 | return capabilities; | 904 | return capabilities; |
| 915 | }); | 905 | }); |
| 916 | 906 | ||
| 917 | /** | 907 | /** Install only the local CLI here. device_open supplies a separate config for each host. */ |
| 918 | * Starting a session with device access also brings the device host up, so | ||
| 919 | * the `agent-device` CLI is on the provider's PATH from its first turn. The | ||
| 920 | * environment is fixed at spawn time, so a host started later by | ||
| 921 | * `device_open` could not reach an already-running agent. Tools install once | ||
| 922 | * and the host is idempotent, so this is cheap after the first session; | ||
| 923 | * a host that fails to start withholds only the CLI, not the MCP tools. | ||
| 924 | */ | ||
| 925 | const hostPlatform = yield* HostProcessPlatform; | 908 | const hostPlatform = yield* HostProcessPlatform; |
| 926 | const agentDeviceEnvironment = Effect.gen(function* () { | 909 | const agentDeviceEnvironment = Effect.gen(function* () { |
| 927 | const readiness = yield* deviceReadiness().pipe( | 910 | const devices = yield* Effect.serviceOption(DeviceService.DeviceService); |
| 911 | if (Option.isNone(devices)) return undefined; | ||
| 912 | const entryPath = yield* devices.value.agentCli.pipe( | ||
| 928 | Effect.catch((cause) => | 913 | Effect.catch((cause) => |
| 929 | Effect.logWarning("Device host unavailable; starting session without agent-device", { | 914 | Effect.logWarning("Agent device CLI unavailable", { cause }).pipe(Effect.as(null)), |
| 930 | cause, | ||
| 931 | }).pipe(Effect.as(null)), | ||
| 932 | ), | 915 | ), |
| 933 | ); | 916 | ); |
| 934 | if (!readiness) return undefined; | 917 | if (!entryPath) return undefined; |
| 935 | const shimDir = yield* ensureAgentDeviceShim({ | 918 | const shimDir = yield* ensureAgentDeviceShim({ |
| 936 | entryPath: readiness.agentDevice.entryPath, | 919 | entryPath, |
| 937 | stateDir: serverConfig.stateDir, | 920 | stateDir: serverConfig.stateDir, |
| 938 | }).pipe( | 921 | }).pipe( |
| 939 | Effect.provideService(FileSystem.FileSystem, fileSystem), | 922 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| ... | @@ -944,8 +927,6 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( | ... | @@ -944,8 +927,6 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( |
| 944 | return { | 927 | return { |
| 945 | PATH: shimDir, | 928 | PATH: shimDir, |
| 946 | PATH_SEPARATOR: hostPlatform === "win32" ? ";" : ":", | 929 | PATH_SEPARATOR: hostPlatform === "win32" ? ";" : ":", |
| 947 | AGENT_DEVICE_DAEMON_BASE_URL: readiness.agentDevice.baseUrl, | ||
| 948 | AGENT_DEVICE_DAEMON_AUTH_TOKEN: readiness.agentDevice.token, | ||
| 949 | AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1", | 930 | AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1", |
| 950 | } satisfies Record<string, string>; | 931 | } satisfies Record<string, string>; |
| 951 | }); | 932 | }); |