Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mobile/src/state/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function projectEnvironmentPresentation(
return {
...presentation,
environmentId,
label: presentation.entry.target.label,
label: presentation.serverConfig?.environment.label ?? presentation.entry.target.label,
displayUrl: connectionCatalogDisplayUrl(presentation.entry),
relayManaged: presentation.entry.target._tag === "RelayConnectionTarget",
};
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/cli/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts";
import * as ServerConfig from "../config.ts";
import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
import * as ExternalLauncher from "../process/externalLauncher.ts";
import * as ServerSettings from "../serverSettings.ts";
import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
import { resolveCliCommand } from "./invocation.ts";
Expand Down Expand Up @@ -433,6 +434,7 @@ const runCloudCommand = Effect.fn("cloud.cli.run_cloud_command")(function* <A, E
| Prompt.Environment
| ServerConfig.ServerConfig
| ServerEnvironment.ServerEnvironment
| ServerSettings.ServerSettingsService
>,
options?: {
readonly quietLogs?: boolean;
Expand All @@ -449,7 +451,9 @@ const runCloudCommand = Effect.fn("cloud.cli.run_cloud_command")(function* <A, E
),
RelayClient.layerCloudflared({ baseDir: config.baseDir }),
EnvironmentAuth.runtimeLayer,
ServerEnvironment.layer.pipe(Layer.provide(ServerSecretStore.layer)),
ServerEnvironment.layer.pipe(
Layer.provideMerge(ServerSettings.layer.pipe(Layer.provideMerge(ServerSecretStore.layer))),
),
bootServiceLayer(config),
headlessRelayClientTracingLayer,
).pipe(
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/cloud/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ describe("reconcileDesiredCloudLink", () => {
ServerEnvironment.ServerEnvironment.of({
getEnvironmentId: unusedSecretStoreOperation(),
getDescriptor: unusedSecretStoreOperation(),
getDescriptorForSettings: () => {
throw new Error("unused");
},
}),
),
Effect.provideService(
Expand Down Expand Up @@ -295,6 +298,9 @@ describe("releaseManagedTunnelOnShutdown", () => {
ServerEnvironment.ServerEnvironment.of({
getEnvironmentId: Effect.succeed(EnvironmentId.make("env_123")),
getDescriptor: Effect.die("unused"),
getDescriptorForSettings: () => {
throw new Error("unused");
},
}),
),
Effect.provideService(
Expand Down
38 changes: 34 additions & 4 deletions apps/server/src/environment/ServerEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
RELAY_URL_SECRET,
} from "../cloud/config.ts";
import * as ServerConfig from "../config.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as ServerEnvironment from "./ServerEnvironment.ts";

const isServerEnvironmentIdPersistenceError = Schema.is(
Expand All @@ -22,6 +23,7 @@ const isServerEnvironmentIdPersistenceError = Schema.is(

const makeServerEnvironmentLayer = (baseDir: string) =>
ServerEnvironment.layer.pipe(
Layer.provideMerge(ServerSettings.layerTest()),
Layer.provide(ServerSecretStore.layer),
Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)),
);
Expand Down Expand Up @@ -102,10 +104,11 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-server-environment-publish-test-",
});
const testLayer = Layer.mergeAll(
ServerEnvironment.layer.pipe(Layer.provide(ServerSecretStore.layer)),
ServerSecretStore.layer,
).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));
const testLayer = ServerEnvironment.layer.pipe(
Layer.provideMerge(ServerSettings.layerTest()),
Layer.provideMerge(ServerSecretStore.layer),
Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)),
);

yield* Effect.gen(function* () {
const secrets = yield* ServerSecretStore.ServerSecretStore;
Expand Down Expand Up @@ -142,6 +145,32 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
}),
);

it.effect("applies configured environment label updates without a restart", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-server-environment-label-test-",
});
yield* Effect.gen(function* () {
const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
const serverSettings = yield* ServerSettings.ServerSettingsService;
const initial = yield* serverEnvironment.getDescriptor;

yield* serverSettings.updateSettings({ environmentLabel: "Studio Mac" });
const renamed = yield* serverEnvironment.getDescriptor;
const renamedFromSnapshot = serverEnvironment.getDescriptorForSettings({
environmentLabel: "Studio Mac",
});
yield* serverSettings.updateSettings({ environmentLabel: "" });
const reset = yield* serverEnvironment.getDescriptor;

expect(renamed).toEqual({ ...initial, label: "Studio Mac" });
expect(renamedFromSnapshot).toEqual(renamed);
expect(reset).toEqual(initial);
}).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));
}),
);

it.effect("structures persisted environment id filesystem failures", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down Expand Up @@ -181,6 +210,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
}).pipe(
Effect.provide(
ServerEnvironment.layer.pipe(
Layer.provideMerge(ServerSettings.layerTest()),
Layer.provide(emptySecretStoreLayer),
Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), failingFileSystemLayer)),
),
Expand Down
39 changes: 31 additions & 8 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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 * as ServerSettings from "../serverSettings.ts";
import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";

export class ServerEnvironmentIdPersistenceError extends Schema.TaggedErrorClass<ServerEnvironmentIdPersistenceError>()(
Expand All @@ -35,6 +36,9 @@ export class ServerEnvironment extends Context.Service<
{
readonly getEnvironmentId: Effect.Effect<EnvironmentId>;
readonly getDescriptor: Effect.Effect<ExecutionEnvironmentDescriptor>;
readonly getDescriptorForSettings: (settings: {
readonly environmentLabel: string;
}) => ExecutionEnvironmentDescriptor;
}
>()("t3/environment/ServerEnvironment") {}

Expand Down Expand Up @@ -69,6 +73,7 @@ export const make = Effect.gen(function* () {
const path = yield* Path.Path;
const serverConfig = yield* ServerConfig.ServerConfig;
const secrets = yield* ServerSecretStore.ServerSecretStore;
const serverSettings = yield* ServerSettings.ServerSettingsService;
const crypto = yield* Crypto.Crypto;
const hostPlatform = yield* HostProcessPlatform;
const hostArchitecture = yield* HostProcessArchitecture;
Expand Down Expand Up @@ -128,16 +133,16 @@ export const make = Effect.gen(function* () {

const environmentId = EnvironmentId.make(environmentIdRaw);
const cwdBaseName = path.basename(serverConfig.cwd).trim();
const label = yield* resolveServerEnvironmentLabel({ cwdBaseName });
const defaultLabel = yield* resolveServerEnvironmentLabel({ cwdBaseName });
const launcher = yield* resolveServiceLauncherMode();
const serverSelfUpdate = resolveServerSelfUpdateCapability({
desktopManaged: serverConfig.mode === "desktop",
launcherManaged: launcher.managed,
});

const descriptor: ExecutionEnvironmentDescriptor = {
const defaultDescriptor: ExecutionEnvironmentDescriptor = {
environmentId,
label,
label: defaultLabel,
platform: {
os: platformOs(hostPlatform),
arch: platformArch(hostArchitecture),
Expand All @@ -156,18 +161,36 @@ export const make = Effect.gen(function* () {
...(serverSelfUpdate === "boot-service" ? { serverSelfUpdateProgress: true } : {}),
},
};
const getDescriptorForSettings = (settings: { readonly environmentLabel: string }) => ({
...defaultDescriptor,
label: settings.environmentLabel || defaultLabel,
capabilities: { ...defaultDescriptor.capabilities, agentActivityPublishing: false },
});

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 },
})),
getDescriptor: serverSettings.getSettings.pipe(
Effect.map(getDescriptorForSettings),
Effect.catch((error) =>
Effect.logWarning("Failed to read the configured environment label.", {
settingsPath: error.settingsPath,
operation: error.operation,
cause: error.cause,
}).pipe(Effect.as(defaultDescriptor)),
),
Effect.flatMap((descriptor) =>
readAgentActivityPublishingActive(secrets).pipe(
Effect.map((agentActivityPublishing) => ({
...descriptor,
capabilities: { ...descriptor.capabilities, agentActivityPublishing },
})),
),
),
),
getDescriptorForSettings,
});
});

Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/mcp/McpSessionRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const fakeHttpServer = makeFakeHttpServer("127.0.0.1");
const fakeEnvironment = ServerEnvironment.ServerEnvironment.of({
getEnvironmentId: Effect.succeed(environmentId),
getDescriptor: Effect.die("unused"),
getDescriptorForSettings: () => {
throw new Error("unused");
},
});

const makeRegistry = (now: () => number, httpServer = fakeHttpServer) =>
Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/relay/AgentAwarenessRelay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,10 @@ describe.sequential("signRelayAgentActivityPublishProof", () => {
Layer.succeed(ServerEnvironment.ServerEnvironment, {
getEnvironmentId: Effect.succeed(environmentId),
getDescriptor: Effect.succeed(descriptor),
getDescriptorForSettings: (settings) => ({
...descriptor,
label: settings.environmentLabel || descriptor.label,
}),
}),
Layer.succeed(OrchestrationEngineService, orchestrationEngine),
Layer.succeed(ProjectionSnapshotQuery, snapshotQuery),
Expand Down Expand Up @@ -661,6 +665,10 @@ describe.sequential("signRelayAgentActivityPublishProof", () => {
Layer.succeed(ServerEnvironment.ServerEnvironment, {
getEnvironmentId: Effect.succeed(environmentId),
getDescriptor: Effect.succeed(descriptor),
getDescriptorForSettings: (settings) => ({
...descriptor,
label: settings.environmentLabel || descriptor.label,
}),
}),
Layer.succeed(OrchestrationEngineService, {
readEvents: () => Stream.empty,
Expand Down
43 changes: 43 additions & 0 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,12 @@ const buildAppUnderTest = (options?: {
getEnvironmentId: Effect.succeed(testEnvironmentDescriptor.environmentId),
getDescriptor: Effect.succeed(testEnvironmentDescriptor),
...options?.layers?.serverEnvironment,
getDescriptorForSettings:
options?.layers?.serverEnvironment?.getDescriptorForSettings ??
((settings) => ({
...testEnvironmentDescriptor,
label: settings.environmentLabel || testEnvironmentDescriptor.label,
})),
}),
),
Layer.provide(
Expand Down Expand Up @@ -4589,6 +4595,43 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
);

it.effect("uses the emitted settings snapshot for environment label updates", () =>
Effect.gen(function* () {
const updatedSettings = {
...DEFAULT_SERVER_SETTINGS,
environmentLabel: "Studio Mac",
};
yield* buildAppUnderTest({
layers: {
serverSettings: {
streamChanges: Stream.succeed(updatedSettings),
},
serverEnvironment: {
getDescriptor: Effect.succeed(testEnvironmentDescriptor),
getDescriptorForSettings: (settings) => ({
...testEnvironmentDescriptor,
label: settings.environmentLabel || testEnvironmentDescriptor.label,
}),
},
},
});

const wsUrl = yield* getWsServerUrl("/ws");
const events = yield* Effect.scoped(
withWsRpcClient(wsUrl, (client) =>
client[WS_METHODS.subscribeServerConfig]({}).pipe(Stream.take(2), Stream.runCollect),
),
);

const update = Array.from(events)[1];
assert.equal(update?.type, "settingsUpdated");
if (update?.type === "settingsUpdated") {
assert.equal(update.payload.settings.environmentLabel, "Studio Mac");
assert.equal(update.payload.environment?.label, "Studio Mac");
}
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
);

it.effect("routes websocket resource telemetry through the subscription", () =>
Effect.gen(function* () {
yield* buildAppUnderTest();
Expand Down
21 changes: 15 additions & 6 deletions apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2199,12 +2199,21 @@ const makeWsRpcLayer = (
Stream.debounce(Duration.millis(PROVIDER_STATUS_DEBOUNCE_MS)),
);
const settingsUpdates = serverSettings.streamChanges.pipe(
Stream.map((settings) => ServerSettings.redactServerSettingsForClient(settings)),
Stream.map((settings) => ({
version: 1 as const,
type: "settingsUpdated" as const,
payload: { settings },
})),
Stream.mapEffect((settings) =>
serverEnvironment.getDescriptor.pipe(
Effect.map((environment) => ({
version: 1 as const,
type: "settingsUpdated" as const,
payload: {
settings: ServerSettings.redactServerSettingsForClient(settings),
environment: {
...serverEnvironment.getDescriptorForSettings(settings),
capabilities: environment.capabilities,
},
},
})),
),
),
);

yield* providerRegistry
Expand Down
Loading
Loading