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 {
Expand Down Expand Up @@ -430,6 +431,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 @@ -446,7 +448,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
29 changes: 28 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 @@ -72,6 +76,28 @@ 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;
yield* serverSettings.updateSettings({ environmentLabel: "" });
const reset = yield* serverEnvironment.getDescriptor;

expect(renamed).toEqual({ ...initial, label: "Studio Mac" });
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 @@ -111,6 +137,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
24 changes: 20 additions & 4 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import packageJson from "../../package.json" with { type: "json" };
import { resolveServerSelfUpdateCapability } from "../cloud/selfUpdate.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 Down Expand Up @@ -65,6 +66,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 @@ -124,14 +126,14 @@ 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 serverSelfUpdate = yield* resolveServerSelfUpdateCapability({
desktopManaged: serverConfig.mode === "desktop",
});

const descriptor: ExecutionEnvironmentDescriptor = {
const defaultDescriptor: ExecutionEnvironmentDescriptor = {
environmentId,
label,
label: defaultLabel,
platform: {
os: platformOs(hostPlatform),
arch: platformArch(hostArchitecture),
Expand All @@ -148,7 +150,21 @@ export const make = Effect.gen(function* () {

return ServerEnvironment.of({
getEnvironmentId: Effect.succeed(environmentId),
getDescriptor: Effect.succeed(descriptor),
getDescriptor: serverSettings.getSettings.pipe(
Effect.map(
(settings): ExecutionEnvironmentDescriptor => ({
...defaultDescriptor,
label: settings.environmentLabel || defaultLabel,
}),
),
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)),
),
),
});
});

Expand Down
14 changes: 9 additions & 5 deletions apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1926,11 +1926,15 @@ const makeWsRpcLayer = (
);
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, environment },
})),
),
),
);

yield* providerRegistry
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 @@ -79,6 +79,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 { Group, GroupSeparator } from "../ui/group";
import { AnimatedHeight } from "../AnimatedHeight";
Expand Down Expand Up @@ -131,6 +132,7 @@ import {
usePrimaryEnvironment,
} from "~/state/environments";
import { useAtomCommand } from "../../state/use-atom-command";
import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings";
import { ConnectionStatusDot } from "../ConnectionStatusDot";
import { ServerUpdateAction } from "../ServerUpdateAction";
import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList";
Expand Down Expand Up @@ -1713,6 +1715,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 @@ -1843,6 +1847,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 @@ -2538,6 +2545,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 @@ -3003,6 +3027,7 @@ export function ConnectionsSettings() {
}
/>
) : null}
{renderEnvironmentLabelRow()}
{desktopBridge ? (
<>
{renderNetworkAccessRow()}
Expand Down Expand Up @@ -3307,6 +3332,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 @@ -12,6 +12,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 two ways to expose your server for remote connections: from the desktop app or from the CLI.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function makeEnvironmentLinks(
listPublicKeysForEnvironment: () => Effect.succeed([]),
listForUser: () => Effect.succeed([]),
getForUser: () => Effect.succeed(null),
updateLabelForUser: () => Effect.void,
revokeForUser: () => Effect.succeed(false),
...overrides,
};
Expand Down
1 change: 1 addition & 0 deletions infra/relay/src/agentActivity/MobileRegistrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ function makeEnvironmentLinks(
listPublicKeysForEnvironment: () => Effect.succeed([]),
listForUser: () => Effect.succeed([]),
getForUser: () => Effect.succeed(null),
updateLabelForUser: () => Effect.void,
revokeForUser: () => Effect.succeed(false),
...overrides,
};
Expand Down
59 changes: 59 additions & 0 deletions infra/relay/src/environments/EnvironmentConnector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ function makeLinks(
environmentPublicKey: environmentKeyPair.publicKey,
...overrides,
}),
updateLabelForUser: () => Effect.void,
revokeForUser: () => Effect.succeed(false),
};
}
Expand Down Expand Up @@ -323,6 +324,64 @@ describe("EnvironmentConnector", () => {
}).pipe(Effect.provide(connectorTestLayer(execute)));
});

it.effect("persists a label reported by a verified environment health response", () => {
const updates: Array<{ userId: string; environmentId: string; label: string }> = [];
const execute = (request: HttpClientRequest.HttpClientRequest) =>
Effect.sync(() => {
const healthRequest = decodeHealthRequestBody(requestBodyText(request));
return HttpClientResponse.fromWeb(
request,
Response.json(
signHealthResponse(
healthRequest,
environmentKeyPair.privateKey,
{},
{
descriptor: {
environmentId: "env-connector-test" as never,
label: "Studio Mac",
platform: { os: "darwin", arch: "arm64" },
serverVersion: "0.0.0-test",
capabilities: { repositoryIdentity: true },
},
},
),
{ status: 200 },
),
);
});
const links = makeLinks();

return Effect.gen(function* () {
const connector = yield* EnvironmentConnector.EnvironmentConnector;
const result = yield* connector.status({
userId: "user_123",
environmentId: "env-connector-test",
});

expect(result.descriptor?.label).toBe("Studio Mac");
expect(updates).toEqual([
{
userId: "user_123",
environmentId: "env-connector-test",
label: "Studio Mac",
},
]);
}).pipe(
Effect.provide(
connectorTestLayer(execute, {
links: {
...links,
updateLabelForUser: (input) =>
Effect.sync(() => {
updates.push(input);
}),
},
}),
),
);
});

it.effect("rejects manual endpoints before sending a health request", () => {
let requestCount = 0;
const execute = () =>
Expand Down
17 changes: 17 additions & 0 deletions infra/relay/src/environments/EnvironmentConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,23 @@ const make = Effect.gen(function* () {
operation: "status",
});
}
if (decoded.descriptor.label !== link.label) {
yield* links
.updateLabelForUser({
userId: input.userId,
environmentId: input.environmentId,
label: decoded.descriptor.label,
})
.pipe(
Effect.tapError((error) =>
Effect.logWarning("managed environment label persistence failed", {
environmentId: input.environmentId,
errorTag: error._tag,
}),
),
Effect.ignore,
);
}
return {
environmentId: link.environmentId,
endpoint,
Expand Down
1 change: 1 addition & 0 deletions infra/relay/src/environments/EnvironmentLinker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ function testLayer(input?: {
listPublicKeysForEnvironment: () => Effect.succeed([]),
listForUser: () => Effect.succeed([]),
getForUser: () => Effect.succeed(null),
updateLabelForUser: () => Effect.void,
revokeForUser: () => Effect.succeed(false),
}),
Layer.succeed(EnvironmentCredentials.EnvironmentCredentials, {
Expand Down
Loading
Loading