import { ORCHESTRATION_PROTOCOL_VERSION, EnvironmentId, PROVIDER_SEND_TURN_MAX_FILE_BYTES, type ExecutionEnvironmentDescriptor, } from "@t3tools/contracts"; import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import packageJson from "../../package.json" with { type: "json" }; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import { readAgentActivityPublishingActive } from "../cloud/config.ts"; import { resolveServerSelfUpdateCapability } from "../cloud/selfUpdate.ts"; import { resolveServiceLauncherMode } from "../cloud/serviceLauncherClient.ts"; import * as ServerConfig from "../config.ts"; import * as ProcessRunner from "../processRunner.ts"; import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts"; import { detectServerEnvironmentMachineKind } from "./ServerEnvironmentMachine.ts"; export class ServerEnvironmentIdPersistenceError extends Schema.TaggedErrorClass()( "ServerEnvironmentIdPersistenceError", { operation: Schema.Literals(["check", "read", "write", "initialize"]), environmentIdPath: Schema.String, cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { if (this.operation === "initialize") { return `Server environment ID file is missing or empty after initialization at '${this.environmentIdPath}'.`; } return `Server environment ID ${this.operation} failed at '${this.environmentIdPath}'.`; } } export class ServerEnvironment extends Context.Service< ServerEnvironment, { readonly getEnvironmentId: Effect.Effect; readonly getDescriptor: Effect.Effect; } >()("t3/environment/ServerEnvironment") {} export class ServerEnvironmentIdentity extends Context.Service< ServerEnvironmentIdentity, { readonly getEnvironmentId: Effect.Effect; } >()("t3/environment/ServerEnvironment/ServerEnvironmentIdentity") {} function platformOs(platform: NodeJS.Platform): ExecutionEnvironmentDescriptor["platform"]["os"] { switch (platform) { case "darwin": return "darwin"; case "linux": return "linux"; case "win32": return "windows"; default: return "unknown"; } } function platformArch( architecture: NodeJS.Architecture, ): ExecutionEnvironmentDescriptor["platform"]["arch"] { switch (architecture) { case "arm64": return "arm64"; case "x64": return "x64"; default: return "other"; } } const makeIdentity = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const serverConfig = yield* ServerConfig.ServerConfig; const crypto = yield* Crypto.Crypto; const readPersistedEnvironmentId = Effect.gen(function* () { const exists = yield* fileSystem.exists(serverConfig.environmentIdPath).pipe( Effect.mapError( (cause) => new ServerEnvironmentIdPersistenceError({ operation: "check", environmentIdPath: serverConfig.environmentIdPath, cause, }), ), ); if (!exists) { return null; } const raw = yield* fileSystem.readFileString(serverConfig.environmentIdPath).pipe( Effect.map((value) => value.trim()), Effect.mapError( (cause) => new ServerEnvironmentIdPersistenceError({ operation: "read", environmentIdPath: serverConfig.environmentIdPath, cause, }), ), ); return raw.length > 0 ? raw : null; }); const persistEnvironmentId = Effect.fn("ServerEnvironmentIdentity.persistEnvironmentId")( function* (value: string, mode: "create" | "recover") { const destinationPath = mode === "recover" ? `${serverConfig.environmentIdPath}.recovery` : serverConfig.environmentIdPath; const tempPath = yield* fileSystem.makeTempFileScoped({ directory: serverConfig.stateDir, prefix: ".environment-id-", }); yield* fileSystem.writeFileString(tempPath, `${value}\n`); // Publish the completed file without replacing an ID created by another process. yield* fileSystem .link(tempPath, destinationPath) .pipe( Effect.catch((cause) => cause.reason._tag === "AlreadyExists" ? Effect.void : Effect.fail(cause), ), ); if (mode === "recover") { // Keep the recovery ID so delayed initializers also publish the same winner. yield* fileSystem.remove(tempPath); yield* fileSystem.copyFile(destinationPath, tempPath); yield* fileSystem.rename(tempPath, serverConfig.environmentIdPath); } }, Effect.scoped, Effect.mapError( (cause) => new ServerEnvironmentIdPersistenceError({ operation: "write", environmentIdPath: serverConfig.environmentIdPath, cause, }), ), ); const environmentIdRaw = yield* Effect.gen(function* () { const persisted = yield* readPersistedEnvironmentId; if (persisted) { return persisted; } const generated = yield* crypto.randomUUIDv4; yield* persistEnvironmentId(generated, "create"); let winner = yield* readPersistedEnvironmentId; if (winner === null) { yield* persistEnvironmentId(generated, "recover"); winner = yield* readPersistedEnvironmentId; } if (winner === null) { return yield* new ServerEnvironmentIdPersistenceError({ operation: "initialize", environmentIdPath: serverConfig.environmentIdPath, }); } return winner; }); const environmentId = EnvironmentId.make(environmentIdRaw); return ServerEnvironmentIdentity.of({ getEnvironmentId: Effect.succeed(environmentId), }); }); export const make = Effect.gen(function* () { const path = yield* Path.Path; const serverConfig = yield* ServerConfig.ServerConfig; const secrets = yield* ServerSecretStore.ServerSecretStore; const identity = yield* ServerEnvironmentIdentity; const hostPlatform = yield* HostProcessPlatform; const hostArchitecture = yield* HostProcessArchitecture; const environmentId = yield* identity.getEnvironmentId; const cwdBaseName = path.basename(serverConfig.cwd).trim(); const label = yield* resolveServerEnvironmentLabel({ cwdBaseName }); const machine = yield* detectServerEnvironmentMachineKind(); const launcher = yield* resolveServiceLauncherMode(); const serverSelfUpdate = resolveServerSelfUpdateCapability({ desktopManaged: serverConfig.mode === "desktop", launcherManaged: launcher.managed, }); // Static is correct: the control fd is known at bootstrap, and the desktop // app and its bundled server ship in one artifact, so a present fd means // the app speaks the requestDesktopUpdate protocol. WSL backends never get // the fd and correctly do not advertise. const desktopAppUpdate = serverSelfUpdate === "desktop-managed" && serverConfig.desktopTelemetryControlFd !== undefined; const descriptor: ExecutionEnvironmentDescriptor = { environmentId, label, platform: { os: platformOs(hostPlatform), arch: platformArch(hostArchitecture), ...(machine === null ? {} : { machine }), }, serverVersion: packageJson.version, orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION, capabilities: { repositoryIdentity: true, connectionProbe: true, attachmentUploads: true, fileAttachments: { maxUploadBytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES }, pullRequests: true, threadSettlement: true, threadAutoSettlement: true, threadSnooze: true, environmentThemes: true, usageLimitSources: true, usagePriceOverrides: true, threadPinning: true, threadPinReorder: true, threadTitleRegeneration: true, threadVisitedTracking: true, threadPullRequestLinking: true, environmentIcon: true, ...(serverSelfUpdate === null ? {} : { serverSelfUpdate }), // V2 restart recovery uses the environment-owned opt-in. The old // per-update request flag is not wired into the V2 update RPC path. ...(serverSelfUpdate === "boot-service" || desktopAppUpdate ? { serverSelfUpdateProgress: true } : {}), ...(desktopAppUpdate ? { desktopAppUpdate: true } : {}), }, }; return ServerEnvironment.of({ getEnvironmentId: Effect.succeed(environmentId), // The publish opt-in and relay link change at runtime (`t3 connect // publish`, the client settings toggle), so the capability is read per // descriptor request rather than baked in at startup. getDescriptor: readAgentActivityPublishingActive(secrets).pipe( Effect.map((agentActivityPublishing) => ({ ...descriptor, capabilities: { ...descriptor.capabilities, agentActivityPublishing }, })), ), }); }); export const identityLayer = Layer.effect(ServerEnvironmentIdentity, makeIdentity); /** * ServerEnvironment is acquired from persisted filesystem and host-process * state. It intentionally has no fallback Layer.succeed value: callers must * provide the external platform services, a ServerConfig, and the * ServerSecretStore backing the descriptor's publishing capability. */ export const layer = Layer.effect(ServerEnvironment, make).pipe( Layer.provideMerge(identityLayer), Layer.provide(ProcessRunner.layer), );