1import * as Crypto from "effect/Crypto";
2import * as Effect from "effect/Effect";
3import * as Hex from "effect/encoding/Hex";
4import * as Schema from "effect/Schema";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7
8import type { AgentDeviceEndpoint } from "./DeviceHost.ts";
9
10const encodeEndpoint = Schema.encodeEffect(
11 Schema.fromJsonString(
12 Schema.Struct({ daemonBaseUrl: Schema.String, daemonAuthToken: Schema.String }),
13 ),
14);
15
16const key = Effect.fn("AgentDeviceTarget.key")(function* (value: string) {
17 const crypto = yield* Crypto.Crypto;
18 const digest = yield* crypto
19 .digest("SHA-256", new TextEncoder().encode(value))
20 .pipe(Effect.orDie);
21 return Hex.encode(digest).slice(0, 24);
22});
23
24/** A stable file per host lets forwarded endpoints change without retargeting other commands. */
25export const agentDeviceConfigPath = (stateDir: string, hostId: string, path: Path.Path) =>
26 key(hostId).pipe(Effect.map((hash) => path.join(stateDir, "device", "hosts", `${hash}.json`)));
27
28export const agentDeviceSession = (threadId: string, hostId: string, deviceId: string) =>
29 key(JSON.stringify([threadId, hostId, deviceId])).pipe(Effect.map((hash) => `t3-${hash}`));
30
31export const writeAgentDeviceConfig = Effect.fn("AgentDeviceTarget.writeConfig")(function* (
32 file: string,
33 endpoint: AgentDeviceEndpoint,
34) {
35 const fs = yield* FileSystem.FileSystem;
36 const path = yield* Path.Path;
37 yield* fs.makeDirectory(path.dirname(file), { recursive: true });
38 const content = yield* encodeEndpoint({
39 daemonBaseUrl: endpoint.baseUrl,
40 daemonAuthToken: endpoint.token,
41 });
42 if ((yield* fs.readFileString(file).pipe(Effect.orElseSucceed(() => ""))) === content) return;
43 const temporary = yield* fs.makeTempFile({ directory: path.dirname(file), prefix: ".endpoint-" });
44 yield* Effect.gen(function* () {
45 yield* fs.chmod(temporary, 0o600);
46 yield* fs.writeFileString(temporary, content);
47 yield* fs.rename(temporary, file);
48 }).pipe(Effect.ensuring(fs.remove(temporary, { force: true }).pipe(Effect.ignore)));
49});