Skip to content
Source
Sign inSign up

ServerEnvironment.ts

317 lines11.3 KB

1import {
2 EnvironmentId,
3 ORCHESTRATION_PROTOCOL_VERSION,
4 PROVIDER_SEND_TURN_MAX_FILE_BYTES,
5 type ExecutionEnvironmentDescriptor,
6} from "@t3tools/contracts";
7import * as HostProcess from "@t3tools/shared/HostProcess";
8import * as Context from "effect/Context";
9import * as SecureRandom from "effect/crypto/SecureRandom";
10import * as Effect from "effect/Effect";
11import * as FileSystem from "effect/FileSystem";
12import * as Layer from "effect/Layer";
13import * as Path from "effect/Path";
14import * as Schema from "effect/Schema";
15​
16import packageJson from "../../package.json" with { type: "json" };
17import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
18import { resolveServerInstallation } from "../cli/invocation.ts";
19import { readAgentActivityPublishingActive } from "../cloud/config.ts";
20import { resolveServerSelfUpdateCapability } from "../cloud/selfUpdate.ts";
21import { resolveServiceLauncherMode } from "../cloud/serviceLauncherClient.ts";
22import * as ServerConfig from "../config.ts";
23import { databaseMigrationHead } from "../persistence/databaseMigrationHead.ts";
24import * as ProcessRunner from "../processRunner.ts";
25import { isSpeechPlatformSupported } from "../speech/platform.ts";
26import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";
27import { detectServerEnvironmentMachineKind } from "./ServerEnvironmentMachine.ts";
28​
29export class ServerEnvironmentIdPersistenceError extends Schema.TaggedError<ServerEnvironmentIdPersistenceError>()(
30 "ServerEnvironmentIdPersistenceError",
31 {
32 operation: Schema.Literals(["check", "read", "write", "initialize"]),
33 environmentIdPath: Schema.String,
34 cause: Schema.optional(Schema.Defect()),
35 },
36) {
37 override get message(): string {
38 if (this.operation === "initialize") {
39 return `Server environment ID file is missing or empty after initialization at '${this.environmentIdPath}'.`;
40 }
41 return `Server environment ID ${this.operation} failed at '${this.environmentIdPath}'.`;
42 }
44​
45export class ServerEnvironment extends Context.Service<
46 ServerEnvironment,
47 {
48 readonly getEnvironmentId: Effect.Effect<EnvironmentId>;
49 readonly getDescriptor: Effect.Effect<ExecutionEnvironmentDescriptor>;
50 }
51>()("t3/environment/ServerEnvironment") {}
52​
53export class ServerEnvironmentIdentity extends Context.Service<
54 ServerEnvironmentIdentity,
55 {
56 readonly getEnvironmentId: Effect.Effect<EnvironmentId>;
57 }
58>()("t3/environment/ServerEnvironment/ServerEnvironmentIdentity") {}
59​
60function platformOs(platform: NodeJS.Platform): ExecutionEnvironmentDescriptor["platform"]["os"] {
61 switch (platform) {
62 case "darwin":
63 return "darwin";
64 case "linux":
65 return "linux";
66 case "win32":
67 return "windows";
68 default:
69 return "unknown";
70 }
72​
73function platformArch(
74 architecture: NodeJS.Architecture,
75): ExecutionEnvironmentDescriptor["platform"]["arch"] {
76 switch (architecture) {
77 case "arm64":
78 return "arm64";
79 case "x64":
80 return "x64";
81 default:
82 return "other";
83 }
85​
86const makeIdentity = Effect.gen(function* () {
87 const fileSystem = yield* FileSystem.FileSystem;
88 const serverConfig = yield* ServerConfig.ServerConfig;
89 const random = yield* SecureRandom.SecureRandom;
90​
91 const readPersistedEnvironmentId = Effect.gen(function* () {
92 const exists = yield* fileSystem.exists(serverConfig.environmentIdPath).pipe(
93 Effect.mapError(
94 (cause) =>
95 new ServerEnvironmentIdPersistenceError({
96 operation: "check",
97 environmentIdPath: serverConfig.environmentIdPath,
98 cause,
99 }),
100 ),
101 );
102 if (!exists) {
103 return null;
104 }
105​
106 const raw = yield* fileSystem.readFileString(serverConfig.environmentIdPath).pipe(
107 Effect.map((value) => value.trim()),
108 Effect.mapError(
109 (cause) =>
110 new ServerEnvironmentIdPersistenceError({
111 operation: "read",
112 environmentIdPath: serverConfig.environmentIdPath,
113 cause,
114 }),
115 ),
116 );
117​
118 return raw.length > 0 ? raw : null;
119 });
120​
121 const persistEnvironmentId = Effect.fn("ServerEnvironmentIdentity.persistEnvironmentId")(
122 function* (value: string, mode: "create" | "recover") {
123 const destinationPath =
124 mode === "recover"
125 ? `${serverConfig.environmentIdPath}.recovery`
126 : serverConfig.environmentIdPath;
127 const tempPath = yield* fileSystem.makeTempFileScoped({
128 directory: serverConfig.stateDir,
129 prefix: ".environment-id-",
130 });
131 yield* fileSystem.writeFileString(tempPath, `${value}\n`);
132 // Publish the completed file without replacing an ID created by another process.
133 yield* fileSystem.link(tempPath, destinationPath).pipe(
134 Effect.catchIf(
135 (cause) => cause.reason._tag === "AlreadyExists",
136 () => Effect.void,
137 ),
138 );
139 if (mode === "recover") {
140 // Keep the recovery ID so delayed initializers also publish the same winner.
141 yield* fileSystem.remove(tempPath);
142 yield* fileSystem.copyFile(destinationPath, tempPath);
143 yield* fileSystem.rename(tempPath, serverConfig.environmentIdPath);
144 }
145 },
146 Effect.scoped,
147 Effect.mapError(
148 (cause) =>
149 new ServerEnvironmentIdPersistenceError({
150 operation: "write",
151 environmentIdPath: serverConfig.environmentIdPath,
152 cause,
153 }),
154 ),
155 );
156​
157 const environmentIdRaw = yield* Effect.gen(function* () {
158 const persisted = yield* readPersistedEnvironmentId;
159 if (persisted) {
160 return persisted;
161 }
162​
163 const generated = yield* random.uuidV4();
164 yield* persistEnvironmentId(generated, "create");
165 let winner = yield* readPersistedEnvironmentId;
166 if (winner === null) {
167 yield* persistEnvironmentId(generated, "recover");
168 winner = yield* readPersistedEnvironmentId;
169 }
170 if (winner === null) {
171 return yield* new ServerEnvironmentIdPersistenceError({
172 operation: "initialize",
173 environmentIdPath: serverConfig.environmentIdPath,
174 });
175 }
176 return winner;
177 });
178​
179 const environmentId = EnvironmentId.make(environmentIdRaw);
180 return ServerEnvironmentIdentity.of({
181 getEnvironmentId: Effect.succeed(environmentId),
182 });
183});
184​
185/** @public */
186export const make = Effect.gen(function* () {
187 const path = yield* Path.Path;
188 const serverConfig = yield* ServerConfig.ServerConfig;
189 const secrets = yield* ServerSecretStore.ServerSecretStore;
190 const identity = yield* ServerEnvironmentIdentity;
191 const hostPlatform = yield* HostProcess.Platform;
192 const hostArchitecture = yield* HostProcess.Architecture;
193 const hostLibc = hostPlatform === "linux" ? yield* HostProcess.LinuxLibc : undefined;
194 const environmentId = yield* identity.getEnvironmentId;
195 const cwdBaseName = path.basename(serverConfig.cwd).trim();
196 const label = yield* resolveServerEnvironmentLabel({ cwdBaseName });
197 const machine = yield* detectServerEnvironmentMachineKind();
198 const launcher = yield* resolveServiceLauncherMode();
199 const serverSelfUpdate = resolveServerSelfUpdateCapability({
200 desktopManaged: serverConfig.mode === "desktop",
201 launcherManaged: launcher.managed,
202 });
203 const serverInstallation = serverSelfUpdate === null ? yield* resolveServerInstallation : null;
204 // The desktop reports whether it can install updates; requests travel over its control fd.
205 const desktopAppUpdate =
206 serverSelfUpdate === "desktop-managed" &&
207 serverConfig.desktopAppUpdate === true &&
208 serverConfig.desktopTelemetryControlFd !== undefined;
209​
210 const databaseMigration = databaseMigrationHead(serverConfig.dbPath);
211 const descriptor: ExecutionEnvironmentDescriptor = {
212 environmentId,
213 label,
214 platform: {
215 os: platformOs(hostPlatform),
216 arch: platformArch(hostArchitecture),
217 ...(machine === null ? {} : { machine }),
218 },
219 serverVersion: packageJson.version,
220 orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION,
221 ...(databaseMigration === undefined ? {} : { databaseMigration }),
222 capabilities: {
223 repositoryIdentity: true,
224 connectionProbe: true,
225 attachmentUploads: true,
226 questionAttachments: true,
227 fileAttachments: { maxUploadBytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES },
228 pullRequests: true,
229 pullRequestChecks: true,
230 sideChats: true,
231 vcsBranchStatus: true,
232 inlineMessageContext: true,
233 requiredWorktreeBootstrap: true,
234 threadSettlement: true,
235 threadAutoSettlement: true,
236 storageCleanup: true,
237 storageCleanupRun: true,
238 databaseStorage: true,
239 otlpEndpointCheck: true,
240 secretSourceCheck: true,
241 projectWorktreeCleanup: true,
242 settledWorktreeCleanup: true,
243 worktreesDirectory: true,
244 ...(hostPlatform === "darwin" ? { fastWorktreeCheckout: true } : {}),
245 threadAutoSettlementScope: true,
246 threadRestartContinuation: true,
247 projectSettingsOverrides: true,
248 projectProviderInstanceOverrides: true,
249 threadSnooze: true,
250 threadForceStop: true,
251 environmentThemes: true,
252 usageLimitSources: true,
253 providerWorkspaceSnapshotsOnDemand: true,
254 modelCatalogRefresh: true,
255 usagePriceOverrides: true,
256 usageModelAliases: true,
257 threadPinning: true,
258 threadPinReorder: true,
259 threadActiveReorder: true,
260 threadAutoSettleOptOut: true,
261 threadProjectFromScratch: true,
262 threadTitleRegeneration: true,
263 commandOutputStreaming: true,
264 threadVisitedTracking: true,
265 threadPullRequests: true,
266 threadPullRequestWatch: true,
267 pullRequestStackActions: true,
268 threadPullRequestLinking: true,
269 serverResolvedCommandContext: true,
270 threadStop: true,
271 ...(hostPlatform === "win32" ? {} : { localServerStop: true }),
272 environmentIcon: true,
273 projectCloneTracking: true,
274 projectOrganization: true,
275 ignoredFileListing: true,
276 triage: true,
277 oneTimeSchedules: true,
278 freshContext: true,
279 scheduledTaskThreadModes: true,
280 npmReleaseLookup: true,
281 voiceTranscription: isSpeechPlatformSupported(hostPlatform, hostArchitecture, hostLibc),
282 cloudVoiceTranscription: true,
283 ...(serverSelfUpdate === null ? {} : { serverSelfUpdate }),
284 ...(serverInstallation === null ? {} : { serverInstallation }),
285 ...(serverSelfUpdate === "boot-service" || desktopAppUpdate
286 ? { serverSelfUpdateProgress: true }
287 : {}),
288 ...(desktopAppUpdate ? { desktopAppUpdate: true, desktopAppChannelSwitch: true } : {}),
289 serverBrowser: true,
290 peerLinks: true,
291 mcpModeLimitHeader: true,
292 linkFence: true,
293 mcpApprovalCodes: true,
294 mcpHostedApproval: true,
295 shellThreadFamilies: true,
296 shellOlderSettled: true,
297 },
298 };
299​
300 return ServerEnvironment.of({
301 getEnvironmentId: Effect.succeed(environmentId),
302 // Publishing opt-in and relay links change at runtime; read the capability per request.
303 getDescriptor: readAgentActivityPublishingActive(secrets).pipe(
304 Effect.map((agentActivityPublishing) => ({
305 ...descriptor,
306 capabilities: { ...descriptor.capabilities, agentActivityPublishing },
307 })),
308 ),
309 });
310});
311​
312export const layerIdentity = Layer.effect(ServerEnvironmentIdentity, makeIdentity);
313​
314export const layer = Layer.effect(ServerEnvironment, make).pipe(
315 Layer.provideMerge(layerIdentity),
316 Layer.provide(ProcessRunner.layer),
317);