authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-15 15:09:25-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-15 15:09:25-07:00
log2c16c1d264173d86fc354e9a4c0c78cd42e514b7
tree19b05adfd3232313e670b96b774b774d32a3cadc
parente6ae764f47d678b96f4a1b2c8c76eedf1a342e26
signature Signed by PGP key B5690EEEBB952194

fix(mobile): block incompatible server connections (#11974)


20 files changed, 234 insertions(+), 22 deletions(-)

apps/mobile/src/features/connection/ConnectionStatusDot.tsx+1
......@@ -35,6 +35,7 @@ function statusDotTone(state: ConnectionStatusDotState): {
3535 haloColor: "rgba(245,158,11,0.5)",
3636 };
3737 case "offline":
38 case "unsupported":
3839 case "error":
3940 return {
4041 dotColor: "#ef4444",
apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx+6-2
......@@ -16,6 +16,8 @@ function noticeTitle(phase: EnvironmentConnectionPhase, environmentLabel: string
1616 return `Connecting to ${environmentLabel}...`;
1717 case "reconnecting":
1818 return `Reconnecting to ${environmentLabel}...`;
19 case "unsupported":
20 return "Client not supported";
1921 case "error":
2022 return `${environmentLabel} is unavailable`;
2123 case "available":
......@@ -31,7 +33,7 @@ function noticeDetail(
3133 error: string | null,
3234): string {
3335 if (error) {
34 return `The app will keep retrying automatically. ${error}`;
36 return phase === "reconnecting" ? `The app will keep retrying automatically. ${error}` : error;
3537 }
3638
3739 switch (phase) {
......@@ -40,6 +42,8 @@ function noticeDetail(
4042 case "connecting":
4143 case "reconnecting":
4244 return `The ${resourceName} will load as soon as the environment is ready.`;
45 case "unsupported":
46 return "Use compatible versions of the app and server to connect.";
4347 case "available":
4448 case "error":
4549 return `Reconnect the environment to load the ${resourceName}.`;
......@@ -95,7 +99,7 @@ export function EnvironmentConnectionNotice(props: {
9599 ) : null}
96100 </Text>
97101
98 {props.connection.phase !== "offline" ? (
102 {props.connection.phase !== "offline" && props.connection.phase !== "unsupported" ? (
99103 <Pressable
100104 accessibilityRole="button"
101105 className="mt-1 rounded-full bg-subtle px-4 py-2.5 active:opacity-70"
apps/mobile/src/features/connection/connectionTone.ts+6
......@@ -21,6 +21,12 @@ export function connectionTone(state: RemoteClientConnectionState): StatusTone {
2121 pillClassName: "bg-primary/10",
2222 textClassName: "text-foreground-secondary",
2323 };
24 case "unsupported":
25 return {
26 label: "Client not supported",
27 pillClassName: "bg-danger",
28 textClassName: "text-danger-foreground",
29 };
2430 case "error":
2531 return {
2632 label: "Connection failed",
apps/mobile/src/features/home/HomeScreen.tsx+6-2
......@@ -169,11 +169,15 @@ function deriveEmptyState(props: {
169169 if (
170170 (catalogState.connectionState === "available" ||
171171 catalogState.connectionState === "offline" ||
172 catalogState.connectionState === "error") &&
172 catalogState.connectionState === "error" ||
173 catalogState.connectionState === "unsupported") &&
173174 !catalogState.hasLoadedShellSnapshot
174175 ) {
175176 return {
176 title: "Environment unavailable",
177 title:
178 catalogState.connectionState === "unsupported"
179 ? "Client not supported"
180 : "Environment unavailable",
177181 detail:
178182 catalogState.connectionError ??
179183 "The saved environment is offline. Check the URL or start the environment, then retry.",
apps/mobile/src/features/threads/floating-working-status.ts+2
......@@ -51,6 +51,8 @@ export function connectionFloatingStatus(input: {
5151 };
5252 case "offline":
5353 return unavailable("You are offline");
54 case "unsupported":
55 return unavailable("Client not supported");
5456 case "error":
5557 return unavailable(
5658 input.connectionError
apps/mobile/src/state/asset-url-state.ts+2-1
......@@ -21,7 +21,8 @@ export function deriveAssetUrlState(input: {
2121 if (
2222 input.connectionPhase === "offline" ||
2323 input.connectionPhase === "reconnecting" ||
24 input.connectionPhase === "error"
24 input.connectionPhase === "error" ||
25 input.connectionPhase === "unsupported"
2526 ) {
2627 return { _tag: "Failure", reason: "disconnected" };
2728 }
apps/mobile/src/state/workspaceModel.ts+3
......@@ -65,6 +65,9 @@ function overallConnectionState(
6565 if (environments.some((environment) => environment.connectionState === "connecting")) {
6666 return "connecting";
6767 }
68 if (environments.some((environment) => environment.connectionState === "unsupported")) {
69 return "unsupported";
70 }
6871 if (environments.some((environment) => environment.connectionState === "error")) {
6972 return "error";
7073 }
apps/server/src/environment/ServerEnvironment.test.ts+2
......@@ -1,4 +1,5 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
2import { ORCHESTRATION_PROTOCOL_VERSION } from "@t3tools/contracts";
23import { expect, it } from "@effect/vitest";
34import * as Crypto from "effect/Crypto";
45import * as Deferred from "effect/Deferred";
......@@ -163,6 +164,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
163164 }).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));
164165
165166 expect(first.environmentId).toBe(second.environmentId);
167 expect(first.orchestrationProtocolVersion).toBe(ORCHESTRATION_PROTOCOL_VERSION);
166168 expect(second.capabilities.repositoryIdentity).toBe(true);
167169 expect(second.capabilities.connectionProbe).toBe(true);
168170 expect(second.capabilities.attachmentUploads).toBe(true);
apps/server/src/environment/ServerEnvironment.ts+2
......@@ -1,5 +1,6 @@
11import {
22 EnvironmentId,
3 ORCHESTRATION_PROTOCOL_VERSION,
34 PROVIDER_SEND_TURN_MAX_FILE_BYTES,
45 type ExecutionEnvironmentDescriptor,
56} from "@t3tools/contracts";
......@@ -212,6 +213,7 @@ export const make = Effect.gen(function* () {
212213 ...(machine === null ? {} : { machine }),
213214 },
214215 serverVersion: packageJson.version,
216 orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION,
215217 capabilities: {
216218 repositoryIdentity: true,
217219 connectionProbe: true,
apps/web/src/components/ConnectionStatusDot.tsx+1
......@@ -11,6 +11,7 @@ export function connectionPhaseDotClassName(phase: EnvironmentConnectionPhase):
1111 case "connecting":
1212 case "reconnecting":
1313 return "bg-warning";
14 case "unsupported":
1415 case "error":
1516 return "bg-destructive";
1617 default:
apps/web/src/components/ProviderUpdateLaunchNotification.environments.ts+1
......@@ -29,6 +29,7 @@ function normalizeConnectionState(phase: string | undefined): EnvironmentUpdateC
2929 case "connecting":
3030 case "reconnecting":
3131 return "connecting";
32 case "unsupported":
3233 case "error":
3334 return "error";
3435 case "offline":
apps/web/src/components/cloud/cloudEnvironmentConnectionPresentation.ts+3-1
......@@ -36,9 +36,11 @@ export function presentSavedCloudEnvironmentConnection(
3636 statusText: connectionStatusText(connection),
3737 tone: "connecting",
3838 };
39 case "unsupported":
3940 case "error":
4041 return {
41 buttonLabel: "Connection failed",
42 buttonLabel:
43 connection.phase === "unsupported" ? "Client not supported" : "Connection failed",
4244 statusText: connectionStatusText(connection),
4345 tone: "error",
4446 };
apps/web/src/components/settings/ConnectionsSettings.tsx+2
......@@ -1456,6 +1456,8 @@ function savedBackendStatus(environment: EnvironmentPresentation): {
14561456 text: connection.error ? `Reconnecting: ${connection.error}` : "Reconnecting",
14571457 tone: "error",
14581458 };
1459 case "unsupported":
1460 return { text: "Client not supported", tone: "error" };
14591461 case "error":
14601462 return {
14611463 text: connection.error ? `Connection failed: ${connection.error}` : "Connection failed",
packages/client-runtime/src/connection/compatibility.test.ts created+54
......@@ -0,0 +1,54 @@
1import {
2 EnvironmentId,
3 ORCHESTRATION_PROTOCOL_VERSION,
4 type ExecutionEnvironmentDescriptor,
5} from "@t3tools/contracts";
6import { describe, expect, it } from "vite-plus/test";
7
8import {
9 appendOrchestrationProtocol,
10 orchestrationProtocolCompatibilityError,
11} from "./compatibility.ts";
12
13const descriptor = (orchestrationProtocolVersion?: number): ExecutionEnvironmentDescriptor => ({
14 environmentId: EnvironmentId.make("environment-remote"),
15 label: "Build Mac",
16 platform: { os: "darwin", arch: "arm64" },
17 serverVersion: "9.0.0",
18 ...(orchestrationProtocolVersion === undefined ? {} : { orchestrationProtocolVersion }),
19 capabilities: { repositoryIdentity: true },
20});
21
22describe("orchestration protocol compatibility", () => {
23 it("accepts the current protocol and announces it without disturbing socket credentials", () => {
24 expect(
25 orchestrationProtocolCompatibilityError(descriptor(ORCHESTRATION_PROTOCOL_VERSION)),
26 ).toBeNull();
27
28 const socketUrl = new URL(
29 appendOrchestrationProtocol("wss://host.test/ws?wsTicket=secret&connectionMethod=relay"),
30 );
31 expect(socketUrl.searchParams.get("orchestrationProtocol")).toBe(
32 String(ORCHESTRATION_PROTOCOL_VERSION),
33 );
34 expect(socketUrl.searchParams.get("wsTicket")).toBe("secret");
35 expect(socketUrl.searchParams.get("connectionMethod")).toBe("relay");
36 });
37
38 it("treats missing metadata as protocol 1", () => {
39 const error = orchestrationProtocolCompatibilityError(descriptor());
40 if (Number(ORCHESTRATION_PROTOCOL_VERSION) === 1) {
41 expect(error).toBeNull();
42 } else {
43 expect(error).toMatchObject({ reason: "unsupported" });
44 }
45 });
46
47 it("blocks a different protocol before connecting", () => {
48 const error = orchestrationProtocolCompatibilityError(
49 descriptor(ORCHESTRATION_PROTOCOL_VERSION + 1),
50 );
51 expect(error).toMatchObject({ reason: "unsupported" });
52 expect(error?.message).toContain("This client is not supported");
53 });
54});
packages/client-runtime/src/connection/compatibility.ts created+30
......@@ -0,0 +1,30 @@
1import {
2 ORCHESTRATION_PROTOCOL_QUERY_PARAM,
3 ORCHESTRATION_PROTOCOL_VERSION,
4 type ExecutionEnvironmentDescriptor,
5} from "@t3tools/contracts";
6
7import { ConnectionBlockedError } from "./model.ts";
8
9export function orchestrationProtocolCompatibilityError(
10 descriptor: ExecutionEnvironmentDescriptor,
11): ConnectionBlockedError | null {
12 // Servers shipped before negotiation use the original wire protocol.
13 const serverProtocolVersion = descriptor.orchestrationProtocolVersion ?? 1;
14 if (serverProtocolVersion === ORCHESTRATION_PROTOCOL_VERSION) {
15 return null;
16 }
17 return new ConnectionBlockedError({
18 reason: "unsupported",
19 detail:
20 serverProtocolVersion > ORCHESTRATION_PROTOCOL_VERSION
21 ? `This client is not supported by this server. Update your app or use a compatible release to connect to ${descriptor.label}.`
22 : `This client requires a newer server. Update T3 Code on ${descriptor.label} to connect.`,
23 });
24}
25
26export function appendOrchestrationProtocol(socketUrl: string): string {
27 const url = new URL(socketUrl);
28 url.searchParams.set(ORCHESTRATION_PROTOCOL_QUERY_PARAM, String(ORCHESTRATION_PROTOCOL_VERSION));
29 return url.toString();
30}
packages/client-runtime/src/connection/presentation.test.ts+16
......@@ -5,6 +5,7 @@ import * as Option from "effect/Option";
55import { BearerConnectionProfile, type ConnectionCatalogEntry } from "./catalog.ts";
66import {
77 BearerConnectionTarget,
8 ConnectionBlockedError,
89 ConnectionTransientError,
910 type SupervisorConnectionState,
1011} from "./model.ts";
......@@ -51,6 +52,21 @@ function supervisorState(overrides: Partial<SupervisorConnectionState>): Supervi
5152}
5253
5354describe("connection presentation", () => {
55 it("labels a blocked protocol as unsupported", () => {
56 const connection = presentConnectionState(
57 supervisorState({
58 phase: "blocked",
59 lastFailure: new ConnectionBlockedError({
60 reason: "unsupported",
61 detail: "Update your app.",
62 }),
63 }),
64 );
65 expect(connection.phase).toBe("unsupported");
66 expect(connection.error).toBe("Update your app.");
67 expect(connectionStatusText(connection)).toBe("Client not supported");
68 });
69
5470 it("preserves profile display information without exposing credentials", () => {
5571 expect(connectionCatalogDisplayUrl(ENTRY)).toBe("https://environment.example.test");
5672 });
packages/client-runtime/src/connection/presentation.ts+5-2
......@@ -10,7 +10,8 @@ export type EnvironmentConnectionPhase =
1010 | "connecting"
1111 | "reconnecting"
1212 | "connected"
13 | "error";
13 | "error"
14 | "unsupported";
1415
1516export interface EnvironmentConnectionPresentation {
1617 readonly phase: EnvironmentConnectionPhase;
......@@ -48,7 +49,7 @@ export function presentConnectionState(
4849 };
4950 case "blocked":
5051 return {
51 phase: "error",
52 phase: state.lastFailure?.reason === "unsupported" ? "unsupported" : "error",
5253 error: state.lastFailure?.message ?? null,
5354 traceId: state.lastFailure?.traceId ?? null,
5455 };
......@@ -69,6 +70,8 @@ export function connectionStatusText(connection: EnvironmentConnectionPresentati
6970 : "Reconnecting...";
7071 case "connected":
7172 return "Connected";
73 case "unsupported":
74 return "Client not supported";
7275 case "error":
7376 return connection.error
7477 ? `Connection failed. Reason: ${connection.error}`
packages/client-runtime/src/connection/resolver.test.ts+45-4
......@@ -1,4 +1,8 @@
1import { EnvironmentId, type DesktopSshEnvironmentTarget } from "@t3tools/contracts";
1import {
2 EnvironmentId,
3 ORCHESTRATION_PROTOCOL_VERSION,
4 type DesktopSshEnvironmentTarget,
5} from "@t3tools/contracts";
26import { RelayClientTracer } from "@t3tools/shared/relayTracing";
37import { describe, expect, it } from "@effect/vitest";
48import * as Effect from "effect/Effect";
......@@ -28,6 +32,7 @@ import {
2832 type ConnectionTarget,
2933} from "./model.ts";
3034import * as ConnectionProfileStore from "./profileStore.ts";
35import { remoteHttpClientLayer } from "../rpc/http.ts";
3136import {
3237 GitHubRoutingPermissions,
3338 gitHubRoutingConnectionKey,
......@@ -75,6 +80,7 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o
7580 readonly authorizeDpop?: RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization["Service"]["authorizeDpop"];
7681 readonly primaryBearerToken?: string;
7782 readonly prepareSsh?: ClientCapabilities.SshEnvironmentGateway["Service"]["prepare"];
83 readonly descriptorProtocolVersion?: number | null | undefined;
7884}) => {
7985 const profiles = new Map(
8086 (options?.profiles ?? []).map((profile) => [profile.connectionId, profile]),
......@@ -140,6 +146,21 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o
140146 });
141147
142148 const dependencies = Layer.mergeAll(
149 remoteHttpClientLayer((() =>
150 Promise.resolve(
151 Response.json({
152 environmentId: ENVIRONMENT_ID,
153 label: "Compatible environment",
154 platform: { os: "linux", arch: "x64" },
155 serverVersion: "0.0.0-test",
156 ...(options?.descriptorProtocolVersion === undefined
157 ? { orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION }
158 : options.descriptorProtocolVersion === null
159 ? {}
160 : { orchestrationProtocolVersion: options.descriptorProtocolVersion }),
161 capabilities: { repositoryIdentity: true },
162 }),
163 )) satisfies typeof fetch),
143164 Layer.succeed(
144165 ConnectionProfileStore.ConnectionProfileStore,
145166 options?.profileStore ?? profileStore,
......@@ -166,6 +187,26 @@ const makeDependencies = Effect.fn("TestConnectionResolver.makeDependencies")((o
166187});
167188
168189describe("ConnectionResolver", () => {
190 it.effect("blocks an incompatible host during discovery before opening orchestration RPC", () =>
191 Effect.gen(function* () {
192 const brokerLayer = yield* makeDependencies({
193 descriptorProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION + 1,
194 });
195 const broker = yield* ConnectionResolver.ConnectionResolver.pipe(Effect.provide(brokerLayer));
196 const target = new PrimaryConnectionTarget({
197 environmentId: ENVIRONMENT_ID,
198 label: "Primary",
199 httpBaseUrl: "http://127.0.0.1:3777",
200 wsBaseUrl: "ws://127.0.0.1:3777",
201 });
202
203 const error = yield* Effect.flip(broker.prepare(catalogEntry(target)));
204
205 expect(error).toMatchObject({ reason: "unsupported" });
206 expect(error.message).toContain("This client is not supported");
207 }),
208 );
209
169210 it.effect("prepares a primary environment without remote capabilities", () =>
170211 Effect.gen(function* () {
171212 const brokerLayer = yield* makeDependencies();
......@@ -182,7 +223,7 @@ describe("ConnectionResolver", () => {
182223 label: "Primary",
183224 httpBaseUrl: "http://127.0.0.1:3777",
184225 socketUrl:
185 "ws://127.0.0.1:3777/ws?clientSurface=web&clientDeviceType=desktop&connectionMethod=direct",
226 "ws://127.0.0.1:3777/ws?clientSurface=web&clientDeviceType=desktop&connectionMethod=direct&orchestrationProtocol=1",
186227 httpAuthorization: null,
187228 target,
188229 });
......@@ -220,7 +261,7 @@ describe("ConnectionResolver", () => {
220261 });
221262
222263 expect(yield* broker.prepare(catalogEntry(target))).toMatchObject({
223 socketUrl: "ws://127.0.0.1:3777/ws?wsTicket=desktop",
264 socketUrl: "ws://127.0.0.1:3777/ws?wsTicket=desktop&orchestrationProtocol=1",
224265 httpAuthorization: { _tag: "Bearer", token: "desktop-bearer" },
225266 target,
226267 });
......@@ -284,7 +325,7 @@ describe("ConnectionResolver", () => {
284325 environmentId: ENVIRONMENT_ID,
285326 label: "Authorized relay environment",
286327 httpBaseUrl: ENDPOINT.httpBaseUrl,
287 socketUrl: "wss://authorized.example.test/ws?wsTicket=dpop",
328 socketUrl: `wss://authorized.example.test/ws?wsTicket=dpop&orchestrationProtocol=${ORCHESTRATION_PROTOCOL_VERSION}`,
288329 httpAuthorization: {
289330 _tag: "Dpop",
290331 accessToken: "dpop-access-token",
packages/client-runtime/src/connection/resolver.ts+41-10
......@@ -2,6 +2,7 @@ import type { AuthClientPresentationMetadata } from "@t3tools/contracts";
22import { withRelayClientTracing } from "@t3tools/shared/relayTracing";
33import * as Context from "effect/Context";
44import * as Effect from "effect/Effect";
5import * as HttpClient from "effect/unstable/http/HttpClient";
56import * as Layer from "effect/Layer";
67import * as Option from "effect/Option";
78import * as Schema from "effect/Schema";
......@@ -16,7 +17,12 @@ import {
1617 SshConnectionProfile,
1718} from "./catalog.ts";
1819import * as ConnectionCredentialStore from "./credentialStore.ts";
19import { credentialMissingError, environmentMismatchError, profileMissingError } from "./errors.ts";
20import {
21 credentialMissingError,
22 environmentMismatchError,
23 mapRemoteEnvironmentError,
24 profileMissingError,
25} from "./errors.ts";
2026import {
2127 GitHubRoutingPermissions,
2228 gitHubRoutingConnectionKey,
......@@ -31,6 +37,11 @@ import type {
3137} from "./model.ts";
3238import { ConnectionBlockedError, type ConnectionAttemptError } from "./model.ts";
3339import * as ConnectionProfileStore from "./profileStore.ts";
40import {
41 appendOrchestrationProtocol,
42 orchestrationProtocolCompatibilityError,
43} from "./compatibility.ts";
44import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts";
3445
3546export class ConnectionResolver extends Context.Service<
3647 ConnectionResolver,
......@@ -234,6 +245,7 @@ export const make = Effect.gen(function* () {
234245 const bearer = yield* makeBearerBroker();
235246 const relay = yield* makeRelayBroker();
236247 const ssh = yield* makeSshBroker();
248 const httpClient = yield* HttpClient.HttpClient;
237249
238250 const prepare = Effect.fn("clientRuntime.connection.broker.prepare")(function* (
239251 entry: ConnectionCatalogEntry,
......@@ -243,16 +255,35 @@ export const make = Effect.gen(function* () {
243255 "connection.environment.id": target.environmentId,
244256 "connection.target.kind": target._tag,
245257 });
246 switch (target._tag) {
247 case "PrimaryConnectionTarget":
248 return yield* primary(target);
249 case "BearerConnectionTarget":
250 return yield* bearer({ ...entry, target });
251 case "RelayConnectionTarget":
252 return yield* relay(target);
253 case "SshConnectionTarget":
254 return yield* ssh({ ...entry, target });
258 const prepared = yield* (() => {
259 switch (target._tag) {
260 case "PrimaryConnectionTarget":
261 return primary(target);
262 case "BearerConnectionTarget":
263 return bearer({ ...entry, target });
264 case "RelayConnectionTarget":
265 return relay(target);
266 case "SshConnectionTarget":
267 return ssh({ ...entry, target });
268 }
269 })();
270 const descriptor = yield* fetchRemoteEnvironmentDescriptor({
271 httpBaseUrl: prepared.httpBaseUrl,
272 }).pipe(
273 Effect.mapError(mapRemoteEnvironmentError),
274 Effect.provideService(HttpClient.HttpClient, httpClient),
275 );
276 if (descriptor.environmentId !== target.environmentId) {
277 return yield* environmentMismatchError({
278 expected: target.environmentId,
279 actual: descriptor.environmentId,
280 });
281 }
282 const compatibilityError = orchestrationProtocolCompatibilityError(descriptor);
283 if (compatibilityError !== null) {
284 return yield* compatibilityError;
255285 }
286 return { ...prepared, socketUrl: appendOrchestrationProtocol(prepared.socketUrl) };
256287 });
257288
258289 return ConnectionResolver.of({ prepare });
packages/contracts/src/environment.ts+6
......@@ -9,6 +9,10 @@ import {
99 TrimmedNonEmptyString,
1010} from "./baseSchemas.ts";
1111
12/** Wire version for orchestration snapshots, streams, commands, and RPC payloads. */
13export const ORCHESTRATION_PROTOCOL_VERSION = 1;
14export const ORCHESTRATION_PROTOCOL_QUERY_PARAM = "orchestrationProtocol";
15
1216export const ExecutionEnvironmentPlatformOs = Schema.Literals([
1317 "darwin",
1418 "linux",
......@@ -175,6 +179,8 @@ export const ExecutionEnvironmentDescriptor = Schema.Struct({
175179 label: TrimmedNonEmptyString,
176180 platform: ExecutionEnvironmentPlatform,
177181 serverVersion: TrimmedNonEmptyString,
182 /** Missing metadata denotes protocol 1. Bump this for breaking wire changes. */
183 orchestrationProtocolVersion: Schema.optionalKey(Schema.Int),
178184 capabilities: ExecutionEnvironmentCapabilities,
179185});
180186export type ExecutionEnvironmentDescriptor = typeof ExecutionEnvironmentDescriptor.Type;