Skip to content
Open
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 @@ -44,6 +44,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 @@ -431,6 +432,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 @@ -447,7 +449,9 @@ const runCloudCommand = Effect.fn("cloud.cli.run_cloud_command")(function* <A, E
),
RelayClient.layerCloudflared({ baseDir: config.baseDir }),
EnvironmentAuth.runtimeLayer,
ServerEnvironment.layer,
ServerEnvironment.layer.pipe(
Layer.provideMerge(ServerSettings.layer.pipe(Layer.provide(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
33 changes: 32 additions & 1 deletion apps/server/src/environment/ServerEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import * as PlatformError from "effect/PlatformError";
import * as Schema from "effect/Schema";

import * as ServerConfig from "../config.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as ServerEnvironment from "./ServerEnvironment.ts";

const isServerEnvironmentIdPersistenceError = Schema.is(
ServerEnvironment.ServerEnvironmentIdPersistenceError,
);

const makeServerEnvironmentLayer = (baseDir: string) =>
ServerEnvironment.layer.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));
ServerEnvironment.layer.pipe(
Layer.provideMerge(ServerSettings.layerTest()),
Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)),
);

const makeServerConfig = Effect.fn(function* (baseDir: string) {
const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, undefined);
Expand Down Expand Up @@ -73,6 +77,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 @@ -112,6 +142,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
}).pipe(
Effect.provide(
ServerEnvironment.layer.pipe(
Layer.provideMerge(ServerSettings.layerTest()),
Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), failingFileSystemLayer)),
),
),
Expand Down
27 changes: 23 additions & 4 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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 @@ -33,6 +34,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 @@ -66,6 +70,7 @@ export const make = Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const serverConfig = yield* ServerConfig.ServerConfig;
const serverSettings = yield* ServerSettings.ServerSettingsService;
const crypto = yield* Crypto.Crypto;
const hostPlatform = yield* HostProcessPlatform;
const hostArchitecture = yield* HostProcessArchitecture;
Expand Down Expand Up @@ -125,16 +130,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 @@ -152,10 +157,24 @@ export const make = Effect.gen(function* () {
...(serverSelfUpdate === "boot-service" ? { serverSelfUpdateProgress: true } : {}),
},
};
const getDescriptorForSettings = (settings: { readonly environmentLabel: string }) => ({
...defaultDescriptor,
label: settings.environmentLabel || defaultLabel,
});

return ServerEnvironment.of({
getEnvironmentId: Effect.succeed(environmentId),
getDescriptor: Effect.succeed(descriptor),
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)),
),
),
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 @@ -906,6 +906,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 @@ -4583,6 +4589,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
6 changes: 4 additions & 2 deletions apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2096,11 +2096,13 @@ 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 },
payload: {
settings: ServerSettings.redactServerSettingsForClient(settings),
environment: serverEnvironment.getDescriptorForSettings(settings),
},
})),
);

Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/components/settings/ConnectionsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { Switch } from "../ui/switch";
import { stackedThreadToast, toastManager } from "../ui/toast";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import { Button } from "../ui/button";
import { DraftInput } from "../ui/draft-input";
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";
import { AnimatedHeight } from "../AnimatedHeight";
import { Textarea } from "../ui/textarea";
Expand Down Expand Up @@ -126,6 +127,7 @@ import {
} from "~/state/environments";
import { useAtomCommand } from "../../state/use-atom-command";
import { serverEnvironment } from "~/state/server";
import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings";
import { ConnectionStatusDot } from "../ConnectionStatusDot";
import { ServerUpdateAction, ServerUpdateProgress } from "../ServerUpdateAction";
import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList";
Expand Down Expand Up @@ -1728,6 +1730,8 @@ export function ConnectionsSettings() {
const desktopBridge = window.desktopBridge;
const { environments } = useEnvironments();
const primaryEnvironment = usePrimaryEnvironment();
const environmentLabel = usePrimarySettings((settings) => settings.environmentLabel);
const updatePrimarySettings = useUpdatePrimarySettings();
const connectPairing = useAtomCommand(connectPairingAtom, { reportFailure: false });
const connectSshEnvironment = useAtomCommand(connectSshEnvironmentAtom, {
reportFailure: false,
Expand Down Expand Up @@ -1861,6 +1865,9 @@ export function ConnectionsSettings() {
);
const canManageLocalBackend = currentSessionScopes?.includes(AuthAccessWriteScope) ?? false;
const canManageRelay = currentSessionScopes?.includes(AuthRelayWriteScope) ?? false;
const canEditEnvironmentLabel =
primaryEnvironmentId !== null &&
(currentSessionScopes?.includes(AuthOrchestrationOperateScope) ?? false);
const authAccessChanges = useEnvironmentQuery(
canManageLocalBackend && primaryEnvironmentId !== null
? authEnvironment.accessChanges({
Expand Down Expand Up @@ -2556,6 +2563,23 @@ export function ConnectionsSettings() {
aria-label="Enable network access"
/>
);
const renderEnvironmentLabelRow = () => (
<SettingsRow
title="Host name"
description="Shown in T3 Connect and used as the default name for new manual connections. Clear it to use this computer’s name."
control={
<DraftInput
className="w-full sm:w-64"
value={environmentLabel}
onCommit={(next) => updatePrimarySettings({ environmentLabel: next })}
placeholder="Use this computer’s name"
aria-label="Host name"
disabled={!canEditEnvironmentLabel}
spellCheck={false}
Comment thread
cursor[bot] marked this conversation as resolved.
/>
}
/>
);
const renderEndpointRows = (presentation: AccessSectionPresentation) =>
isAdvertisedEndpointListExpanded
? visibleDesktopNetworkAdvertisedEndpoints.map((endpoint) => {
Expand Down Expand Up @@ -3041,6 +3065,7 @@ export function ConnectionsSettings() {
}
/>
) : null}
{renderEnvironmentLabelRow()}
{desktopBridge ? (
<>
{renderNetworkAccessRow()}
Expand Down Expand Up @@ -3345,6 +3370,7 @@ export function ConnectionsSettings() {
</>
) : (
<SettingsSection title="This environment">
{renderEnvironmentLabelRow()}
<SettingsRow
title="Administrative access"
description="Pairing links and client-session management require the access:write scope for this backend."
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/state/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ 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
9 changes: 9 additions & 0 deletions docs/user/remote-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ That gives you:
- transport security at the network layer
- less exposure than opening the server to the public internet

## Naming This Host

In the web or desktop app, open **Settings** → **Connections** and edit **Host name** under
**This environment**. T3 Code uses that name in T3 Connect and as the default label when another
client adds the environment through a manual pairing connection.

Clear the field to return to the name detected from the host operating system. This setting changes
the environment name shown by T3 Code; it does not change the machine's operating-system hostname.

## Enabling Network Access

There are three ways to reach your server from another device: expose the desktop app's backend,
Expand Down
Loading
Loading