ds-harness-remote 0.4.26 → 0.4.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +6 -6
  2. package/dist/client-runtime.d.ts +11 -0
  3. package/dist/client-runtime.d.ts.map +1 -1
  4. package/dist/client-runtime.js +62 -0
  5. package/dist/client-runtime.js.map +1 -1
  6. package/dist/client.github.js +18 -51
  7. package/dist/client.js +18 -51
  8. package/dist/client.js.map +1 -1
  9. package/dist/codex/domain.d.ts.map +1 -1
  10. package/dist/codex/domain.js +22 -0
  11. package/dist/codex/domain.js.map +1 -1
  12. package/dist/config.d.ts +6 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +4 -1
  15. package/dist/config.js.map +1 -1
  16. package/dist/control-runtime.d.ts.map +1 -1
  17. package/dist/control-runtime.js +16 -1
  18. package/dist/control-runtime.js.map +1 -1
  19. package/dist/harness-version.d.ts.map +1 -1
  20. package/dist/harness-version.js +16 -3
  21. package/dist/harness-version.js.map +1 -1
  22. package/dist/index.js +154 -31
  23. package/dist/index.js.map +3 -3
  24. package/dist/loopback-host.d.ts +2 -2
  25. package/dist/loopback-host.d.ts.map +1 -1
  26. package/dist/loopback-host.js +41 -16
  27. package/dist/loopback-host.js.map +1 -1
  28. package/dist/server-api.d.ts +2 -0
  29. package/dist/server-api.d.ts.map +1 -1
  30. package/dist/server-api.js +5 -0
  31. package/dist/server-api.js.map +1 -1
  32. package/dist/server-connection.d.ts.map +1 -1
  33. package/dist/server-connection.js +8 -0
  34. package/dist/server-connection.js.map +1 -1
  35. package/dist/service.d.ts +4 -1
  36. package/dist/service.d.ts.map +1 -1
  37. package/dist/service.js +19 -5
  38. package/dist/service.js.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/dsh-plugin.json +1 -1
  42. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4646,22 +4646,22 @@ function headerPairs(headers) {
4646
4646
  return Object.entries(headers).flatMap(([name2, value]) => value === void 0 ? [] : (Array.isArray(value) ? value : [value]).map((item) => [name2, item]));
4647
4647
  }
4648
4648
  var LoopbackHost = class {
4649
- handles = /* @__PURE__ */ new Map();
4650
- ports;
4651
- closed = false;
4652
- timer;
4653
- constructor(ports) {
4654
- this.ports = [...ports];
4649
+ constructor(getPorts, onClose) {
4650
+ this.onClose = onClose;
4651
+ this.ports = [...getPorts()];
4655
4652
  this.timer = setInterval(() => {
4656
4653
  for (const [id4, handle] of this.handles) if (Date.now() - handle.touched > 6e4) this.close(id4);
4657
4654
  }, 1e4);
4658
4655
  this.timer.unref();
4659
4656
  }
4657
+ handles = /* @__PURE__ */ new Map();
4658
+ ports;
4659
+ closed = false;
4660
+ timer;
4660
4661
  setPorts(ports) {
4661
4662
  this.ports = [...ports];
4662
- }
4663
- hasPorts() {
4664
- return this.ports.length > 0;
4663
+ const allowed = new Set(ports);
4664
+ for (const [id4, handle] of this.handles) if (!allowed.has(handle.port)) this.close(id4);
4665
4665
  }
4666
4666
  async call(input2) {
4667
4667
  if (this.closed) throw new RpcError("TRANSPORT_CLOSED", "Preview connection closed.");
@@ -4678,10 +4678,13 @@ var LoopbackHost = class {
4678
4678
  );
4679
4679
  if (this.handles.has(value.id)) throw new RpcError("REQUEST_CONFLICT", "Preview handle is already in use.");
4680
4680
  if (this.handles.size >= LOOPBACK_MAX_CONNECTIONS) throw new RpcError("RATE_LIMITED", "Too many active preview requests.");
4681
+ let openedHandle;
4681
4682
  try {
4682
- return value.op === "http.open" ? await this.openHttp(value) : await this.openWs(value);
4683
+ const opening = value.op === "http.open" ? this.openHttp(value) : this.openWs(value);
4684
+ openedHandle = this.handles.get(value.id);
4685
+ return await opening;
4683
4686
  } catch {
4684
- this.close(value.id);
4687
+ this.close(value.id, openedHandle);
4685
4688
  throw new RpcError("LOOPBACK_UNAVAILABLE", "The allowed loopback service did not respond. Check that it is running on the Host.");
4686
4689
  }
4687
4690
  }
@@ -4712,6 +4715,7 @@ var LoopbackHost = class {
4712
4715
  response.once("close", ready);
4713
4716
  response.once("error", fail4);
4714
4717
  });
4718
+ if (this.handles.get(value.id) !== handle) throw new Error("closed");
4715
4719
  chunk = response.read(Math.min(response.readableLength || LOOPBACK_CHUNK_BYTES, LOOPBACK_CHUNK_BYTES));
4716
4720
  if (chunk === null && response.readableLength > 0) chunk = response.read(Math.min(response.readableLength, LOOPBACK_CHUNK_BYTES));
4717
4721
  }
@@ -4719,10 +4723,10 @@ var LoopbackHost = class {
4719
4723
  if (done && !response.complete) throw new Error("truncated");
4720
4724
  handle.total += chunk?.length ?? 0;
4721
4725
  if (handle.total > 64 * 1024 * 1024) throw new Error("size");
4722
- if (done) this.close(value.id);
4726
+ if (done) this.close(value.id, handle);
4723
4727
  return { data: chunk?.toString("base64") ?? "", done };
4724
4728
  } catch {
4725
- this.close(value.id);
4729
+ this.close(value.id, handle);
4726
4730
  throw new RpcError("LOOPBACK_READ_FAILED", "Preview response ended or exceeded its limit; reload to retry.");
4727
4731
  } finally {
4728
4732
  handle.reading = false;
@@ -4762,7 +4766,7 @@ var LoopbackHost = class {
4762
4766
  }
4763
4767
  handle.bytes -= bytes;
4764
4768
  const closed = handle.closed && handle.messages.length === 0;
4765
- if (closed) this.close(value.id);
4769
+ if (closed) this.close(value.id, handle);
4766
4770
  return { messages, closed };
4767
4771
  } finally {
4768
4772
  handle.reading = false;
@@ -4772,12 +4776,15 @@ var LoopbackHost = class {
4772
4776
  throw new RpcError("INVALID_MESSAGE", "Preview handle kind does not match the operation.");
4773
4777
  }
4774
4778
  closeAll() {
4779
+ if (this.closed) return;
4775
4780
  this.closed = true;
4776
4781
  clearInterval(this.timer);
4777
4782
  for (const id4 of this.handles.keys()) this.close(id4);
4783
+ this.onClose?.();
4778
4784
  }
4779
- close(id4) {
4785
+ close(id4, expected) {
4780
4786
  const handle = this.handles.get(id4);
4787
+ if (expected !== void 0 && handle !== expected) return;
4781
4788
  this.handles.delete(id4);
4782
4789
  if (handle?.kind === "http") {
4783
4790
  handle.response?.destroy();
@@ -4785,6 +4792,8 @@ var LoopbackHost = class {
4785
4792
  }
4786
4793
  if (handle?.kind === "ws") {
4787
4794
  handle.closed = true;
4795
+ handle.messages = [];
4796
+ handle.bytes = 0;
4788
4797
  handle.wake?.();
4789
4798
  handle.socket.terminate();
4790
4799
  }
@@ -4794,6 +4803,7 @@ var LoopbackHost = class {
4794
4803
  if ((body?.length ?? 0) > LOOPBACK_MAX_BODY_BYTES) throw new Error("body limit");
4795
4804
  return new Promise((resolve4, reject) => {
4796
4805
  const headers = Object.fromEntries(cleanHeaders(value.headers));
4806
+ let handle;
4797
4807
  const request = httpRequest({
4798
4808
  hostname: "127.0.0.1",
4799
4809
  port: value.port,
@@ -4803,8 +4813,7 @@ var LoopbackHost = class {
4803
4813
  agent: false,
4804
4814
  maxHeaderSize: 32 * 1024
4805
4815
  }, (response) => {
4806
- const handle = this.handles.get(value.id);
4807
- if (handle?.kind !== "http") {
4816
+ if (this.handles.get(value.id) !== handle) {
4808
4817
  response.destroy();
4809
4818
  return;
4810
4819
  }
@@ -4821,7 +4830,8 @@ var LoopbackHost = class {
4821
4830
  clearTimeout(timer);
4822
4831
  reject(new Error("upstream"));
4823
4832
  });
4824
- this.handles.set(value.id, { kind: "http", request, reading: false, total: 0, touched: Date.now() });
4833
+ handle = { kind: "http", port: value.port, request, reading: false, total: 0, touched: Date.now() };
4834
+ this.handles.set(value.id, handle);
4825
4835
  request.end(body);
4826
4836
  });
4827
4837
  }
@@ -4834,13 +4844,14 @@ var LoopbackHost = class {
4834
4844
  maxPayload: LOOPBACK_MAX_WS_BYTES,
4835
4845
  perMessageDeflate: false
4836
4846
  });
4837
- const handle = { kind: "ws", socket, messages: [], bytes: 0, reading: false, closed: false, touched: Date.now() };
4847
+ const handle = { kind: "ws", port: value.port, socket, messages: [], bytes: 0, reading: false, closed: false, touched: Date.now() };
4838
4848
  this.handles.set(value.id, handle);
4839
4849
  socket.once("open", () => resolve4({ protocol: socket.protocol }));
4840
4850
  socket.on("message", (data2, binary) => {
4851
+ if (handle.closed) return;
4841
4852
  const message = { data: Buffer.from(data2).toString("base64"), binary };
4842
4853
  if (handle.bytes + message.data.length > 1024 * 1024 || handle.messages.length >= 256) {
4843
- this.close(value.id);
4854
+ this.close(value.id, handle);
4844
4855
  return;
4845
4856
  }
4846
4857
  handle.messages.push(message);
@@ -14745,7 +14756,7 @@ function uuidV7(now = Date.now()) {
14745
14756
  }
14746
14757
 
14747
14758
  // src/harness-version.ts
14748
- import { readFile } from "node:fs/promises";
14759
+ import { readFile, realpath as realpathEntrypoint } from "node:fs/promises";
14749
14760
  import { createRequire } from "node:module";
14750
14761
  import { dirname, isAbsolute, join } from "node:path";
14751
14762
  var LEGACY_PLACEHOLDER_VERSION = "0.0.1";
@@ -14780,7 +14791,13 @@ async function readHarnessManifestVersion(manifestPath) {
14780
14791
  }
14781
14792
  async function readHarnessDistributionVersion(entrypoint = process.argv[1]) {
14782
14793
  if (entrypoint === void 0 || !isAbsolute(entrypoint)) return void 0;
14783
- let directory = dirname(entrypoint);
14794
+ let resolvedEntrypoint = entrypoint;
14795
+ try {
14796
+ resolvedEntrypoint = await realpathEntrypoint(entrypoint);
14797
+ } catch {
14798
+ }
14799
+ if (!isAbsolute(resolvedEntrypoint)) return void 0;
14800
+ let directory = dirname(resolvedEntrypoint);
14784
14801
  for (let depth = 0; depth < 8; depth += 1) {
14785
14802
  const version = await readHarnessManifestVersion(join(directory, "package.json"));
14786
14803
  if (version !== void 0) return version;
@@ -14789,7 +14806,7 @@ async function readHarnessDistributionVersion(entrypoint = process.argv[1]) {
14789
14806
  directory = parent;
14790
14807
  }
14791
14808
  try {
14792
- return await readHarnessManifestVersion(createRequire(entrypoint).resolve(`${HARNESS_PACKAGE_NAME}/package.json`));
14809
+ return await readHarnessManifestVersion(createRequire(resolvedEntrypoint).resolve(`${HARNESS_PACKAGE_NAME}/package.json`));
14793
14810
  } catch {
14794
14811
  return void 0;
14795
14812
  }
@@ -18286,6 +18303,7 @@ var entryConfigSchema = s.object({
18286
18303
  serverUrl: s.string(),
18287
18304
  deviceName: s.string(),
18288
18305
  terminal: s.object({ enabled: s.boolean() }),
18306
+ hostControl: s.object({ enabled: s.boolean() }),
18289
18307
  loopback: s.object({ ports: s.array(s.number()) }),
18290
18308
  forceRelay: s.boolean(),
18291
18309
  logLevel: s.union(["debug", "info", "warn", "error"]),
@@ -18322,6 +18340,7 @@ var configSchema = external_exports.object({
18322
18340
  serverUrl: external_exports.string().url().optional(),
18323
18341
  deviceName: external_exports.string().trim().min(1).max(80).optional(),
18324
18342
  terminal: external_exports.object({ enabled: external_exports.boolean().optional() }).strict().optional(),
18343
+ hostControl: external_exports.object({ enabled: external_exports.boolean().optional() }).strict().optional(),
18325
18344
  loopback: external_exports.object({ ports: external_exports.array(external_exports.number().int().min(1024).max(65535)).max(16).optional() }).strict().optional(),
18326
18345
  forceRelay: external_exports.boolean().optional(),
18327
18346
  logLevel: external_exports.enum(["debug", "info", "warn", "error"]).optional(),
@@ -18347,7 +18366,8 @@ function resolveConfig(input2 = {}, env = process.env) {
18347
18366
  role: parsed.role ?? "host",
18348
18367
  ...serverUrl === void 0 ? {} : { serverUrl },
18349
18368
  deviceName: parsed.deviceName ?? hostname(),
18350
- terminal: { enabled: parsed.terminal?.enabled ?? env.DSH_REMOTE_TERMINAL_ENABLED === "true" },
18369
+ hostControl: { enabled: parsed.hostControl?.enabled ?? true },
18370
+ terminal: { enabled: parsed.terminal?.enabled ?? (env.DSH_REMOTE_TERMINAL_ENABLED === void 0 || env.DSH_REMOTE_TERMINAL_ENABLED === "true") },
18351
18371
  loopback: { ports: [...new Set(parsed.loopback?.ports ?? [])] },
18352
18372
  forceRelay: parsed.forceRelay ?? false,
18353
18373
  logLevel: parsed.logLevel ?? "info",
@@ -18387,7 +18407,7 @@ function normalizeServerUrl(value) {
18387
18407
  }
18388
18408
 
18389
18409
  // src/version.ts
18390
- var PLUGIN_VERSION = "0.4.26";
18410
+ var PLUGIN_VERSION = "0.4.27";
18391
18411
 
18392
18412
  // src/server-api.ts
18393
18413
  var TERMINAL_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/u;
@@ -18416,6 +18436,11 @@ var HostServerApi = class {
18416
18436
  ...this.credentials.account === void 0 ? {} : { account: this.credentials.account }
18417
18437
  };
18418
18438
  }
18439
+ /** Check the persisted device credential without issuing or refreshing one. */
18440
+ async hasStoredAuthorization() {
18441
+ const identity = this.requireIdentity();
18442
+ return await this.store.load(this.baseUrl, identity.deviceId) !== void 0;
18443
+ }
18419
18444
  async clearAuthorization() {
18420
18445
  this.credentials = void 0;
18421
18446
  this.credentialsPromise = void 0;
@@ -20061,6 +20086,13 @@ var REMOTE_COMMAND_LIST_MIN_VERSION = [0, 3, 16];
20061
20086
  var REMOTE_FILE_VIEWER_MIN_VERSION = [0, 3, 17];
20062
20087
  var DIRECT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 12e3;
20063
20088
  var DIRECT_LAN_PROGRESS_DISPLAY_MS = 1400;
20089
+ var HOST_AUTHORIZATION_ERRORS = /* @__PURE__ */ new Set([
20090
+ "ACCOUNT_AUTH_REQUIRED",
20091
+ "AUTH_INVALID",
20092
+ "DEVICE_OWNERSHIP_REQUIRED",
20093
+ "DEVICE_REVOKED",
20094
+ "TOKEN_EXPIRED"
20095
+ ]);
20064
20096
  var ClientModeRuntime = class {
20065
20097
  constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory) {
20066
20098
  this.config = config;
@@ -20093,6 +20125,27 @@ var ClientModeRuntime = class {
20093
20125
  deviceId: shortId(this.identity.deviceId),
20094
20126
  fingerprint: this.identity.fingerprint
20095
20127
  });
20128
+ if (this.config.hostControl?.enabled !== false && this.host !== void 0 && this.server.hasStoredAuthorization !== void 0) {
20129
+ try {
20130
+ if (await this.server.hasStoredAuthorization() && (this.host.hasStoredAuthorization === void 0 || !await this.host.hasStoredAuthorization())) {
20131
+ await this.authorizeHostByDefault();
20132
+ }
20133
+ } catch (error) {
20134
+ this.logger.warn("automatic Host authorization failed", { code: safeErrorCode(error) });
20135
+ }
20136
+ }
20137
+ }
20138
+ async authorizeHostByDefault() {
20139
+ try {
20140
+ if (this.host === void 0) return;
20141
+ if (this.config.hostControl?.enabled === false) return;
20142
+ if (this.host.hostStatus().authorized) return;
20143
+ if (this.host.hasStoredAuthorization !== void 0 && await this.host.hasStoredAuthorization()) return;
20144
+ const credentials = await this.server.authenticate(this.requireIdentity());
20145
+ await this.host.authorizeHostAsOwned(credentials.accessToken, credentials.account);
20146
+ } catch (error) {
20147
+ this.logger.warn("automatic Host authorization failed", { code: safeErrorCode(error) });
20148
+ }
20096
20149
  }
20097
20150
  registerControl(connection, webServer) {
20098
20151
  return registerControlRoute(connection, (endpoint, payload, signal) => this.handleControl(endpoint, payload, signal), webServer);
@@ -20152,6 +20205,7 @@ var ClientModeRuntime = class {
20152
20205
  };
20153
20206
  }
20154
20207
  async devices() {
20208
+ this.assertHostAuthorizationForDeviceDiscovery();
20155
20209
  this.requireIdentity();
20156
20210
  const serverDevices = await this.server.listDevices();
20157
20211
  const remoteDevices = serverDevices.filter((device) => device.deviceId !== this.host?.hostStatus().deviceId);
@@ -20161,6 +20215,20 @@ var ClientModeRuntime = class {
20161
20215
  return { ...device, ...presence };
20162
20216
  }));
20163
20217
  }
20218
+ /**
20219
+ * Device discovery is exposed through the local app control route. When this
20220
+ * installation also runs a Host, keep that route closed after the Host's
20221
+ * Server credential has become terminally invalid. The Client credential can
20222
+ * remain usable for a short time after a revoke, so checking only
20223
+ * `ClientServerApi.listDevices()` would otherwise leak the device directory
20224
+ * from a Host that the user has already been told to re-authorize.
20225
+ */
20226
+ assertHostAuthorizationForDeviceDiscovery() {
20227
+ const status2 = this.host?.hostStatus();
20228
+ if (status2 === void 0 || status2.error === void 0 || !HOST_AUTHORIZATION_ERRORS.has(status2.error)) return;
20229
+ const message = status2.error === "DEVICE_REVOKED" ? "The local Host was revoked on the Server. Sign out and authorize this Host again." : "The local Host authorization is no longer valid. Sign out and authorize this Host again.";
20230
+ throw new ClientModeError(status2.error, message);
20231
+ }
20164
20232
  async authorizeClientWithAccount(email, password) {
20165
20233
  let authorization;
20166
20234
  try {
@@ -20171,6 +20239,7 @@ var ClientModeRuntime = class {
20171
20239
  this.server.bindIdentity(this.identity);
20172
20240
  authorization = await this.server.authorizeWithAccount(this.identity, email, password);
20173
20241
  }
20242
+ await this.authorizeHostByDefault();
20174
20243
  this.logger.info("Client account authorized");
20175
20244
  return authorization;
20176
20245
  }
@@ -20185,6 +20254,7 @@ var ClientModeRuntime = class {
20185
20254
  return this.identity;
20186
20255
  });
20187
20256
  if (result.status === "complete") this.logger.info("Client account authorized with QR login");
20257
+ if (result.status === "complete") await this.authorizeHostByDefault();
20188
20258
  return result;
20189
20259
  }
20190
20260
  async clearClientAuthorization() {
@@ -21518,6 +21588,16 @@ var PluginControlRuntime = class {
21518
21588
  if (endpoint === "settings.acp.add") return ok3(await this.addAcp(payload));
21519
21589
  if (endpoint === "settings.acp.remove") return ok3(await this.removeAcp(payload));
21520
21590
  if (endpoint === "settings.logout") return ok3(await this.logout());
21591
+ if (endpoint === "host.authorization.set" && this.client !== void 0) {
21592
+ const value = record5(payload);
21593
+ if (typeof value.enabled !== "boolean") throw new ClientModeError("INVALID_MESSAGE", "Host authorization state is required.");
21594
+ const status2 = await this.client.setHostAuthorization(value.enabled);
21595
+ if (this.settings !== void 0) {
21596
+ const current = resolveConfig(this.settings.get());
21597
+ await this.settings.replace(editableConfig({ ...current, hostControl: { enabled: value.enabled } }));
21598
+ }
21599
+ return ok3(status2);
21600
+ }
21521
21601
  if (endpoint === "host.reconnect") {
21522
21602
  if (this.host === void 0) throw new ClientModeError("METHOD_NOT_ALLOWED", "This plugin is not running as a Host.");
21523
21603
  this.host.reconnectHost();
@@ -21575,6 +21655,9 @@ var PluginControlRuntime = class {
21575
21655
  }
21576
21656
  authorization = await api.authorizeWithAccount(identity, value.email, value.password);
21577
21657
  }
21658
+ if (value.role === "client" && resolveConfig(this.settings.get()).hostControl?.enabled !== false) {
21659
+ await this.client?.authorizeHostByDefault();
21660
+ }
21578
21661
  await this.settings.replace(editableConfig(next));
21579
21662
  return {
21580
21663
  status: "authorized",
@@ -21621,7 +21704,7 @@ var PluginControlRuntime = class {
21621
21704
  const current = editableConfig(resolveConfig(this.settings.get()));
21622
21705
  const next = resolveConfig({
21623
21706
  ...current,
21624
- terminal: { enabled: value.terminalEnabled === void 0 ? current.terminal?.enabled ?? false : value.terminalEnabled },
21707
+ terminal: { enabled: value.terminalEnabled === void 0 ? current.terminal?.enabled ?? true : value.terminalEnabled },
21625
21708
  loopback: { ports: value.ports === void 0 ? current.loopback?.ports ?? [] : value.ports }
21626
21709
  });
21627
21710
  await this.settings.replace(editableConfig(next));
@@ -21769,6 +21852,7 @@ function editableConfig(config) {
21769
21852
  role: config.role,
21770
21853
  ...config.serverUrl === void 0 ? {} : { serverUrl: config.serverUrl },
21771
21854
  terminal: config.terminal,
21855
+ hostControl: config.hostControl ?? { enabled: true },
21772
21856
  loopback: config.loopback,
21773
21857
  forceRelay: config.forceRelay,
21774
21858
  logLevel: config.logLevel,
@@ -22549,6 +22633,13 @@ var HostServerConnection = class {
22549
22633
  if (TERMINAL_AUTH_ERRORS.has(code)) {
22550
22634
  this.logger.warn(code === "CONNECTION_REPLACED" ? "Another instance is using this Host identity. Stop it or use a separate DSH_HOME; automatic reconnect is paused." : code === "SERVER_CREDENTIALS_BUSY" ? "Credential refresh is locked. Stop other instances; after a crash, stop all instances before removing server-credentials.json.refresh-lock and authorizing again." : "Host authorization failed. Run /remote login or authorize this Host again in Remote settings.");
22551
22635
  }
22636
+ if (code === "DEVICE_REVOKED") {
22637
+ try {
22638
+ await this.api.clearAuthorization();
22639
+ } catch (clearError) {
22640
+ this.logger.error("failed to clear revoked Host authorization", { code: errorCode2(clearError) });
22641
+ }
22642
+ }
22552
22643
  if (TERMINAL_AUTH_ERRORS.has(code) || !this.config.reconnect.enabled) return;
22553
22644
  }
22554
22645
  if (this.stopped) return;
@@ -24666,6 +24757,7 @@ function isRecord10(value) {
24666
24757
 
24667
24758
  // src/codex/domain.ts
24668
24759
  import { randomUUID as randomUUID2 } from "node:crypto";
24760
+ import { accessSync, constants, existsSync as existsSync2, readFileSync } from "node:fs";
24669
24761
  import { readdir as readdir2, realpath, stat as stat4 } from "node:fs/promises";
24670
24762
  import { homedir as homedir3 } from "node:os";
24671
24763
  import { basename as basename3, isAbsolute as isAbsolute3, join as join5, relative, resolve as resolve2 } from "node:path";
@@ -26114,7 +26206,26 @@ var CodexRemoteDomain = class {
26114
26206
  };
26115
26207
  function codexBinaryCandidates(configured, hostPlatform = process.platform, userHome = homedir3()) {
26116
26208
  if (configured !== "codex" || hostPlatform !== "darwin") return [configured];
26209
+ const bundledCandidates = [
26210
+ "/Applications/ChatGPT.app",
26211
+ join5(userHome, "Applications", "ChatGPT.app")
26212
+ ].flatMap((chatGptApp) => {
26213
+ const codexCli = join5(chatGptApp, "Contents", "Resources", "codex-cli");
26214
+ try {
26215
+ const manifest = JSON.parse(readFileSync(join5(codexCli, "codex-package.json"), "utf8"));
26216
+ if (!isRecord13(manifest) || typeof manifest.entrypoint !== "string" || manifest.entrypoint.length === 0) {
26217
+ return [];
26218
+ }
26219
+ const candidate = join5(codexCli, manifest.entrypoint);
26220
+ if (!existsSync2(candidate)) return [];
26221
+ accessSync(candidate, constants.X_OK);
26222
+ return [candidate];
26223
+ } catch {
26224
+ return [];
26225
+ }
26226
+ });
26117
26227
  return [.../* @__PURE__ */ new Set([
26228
+ ...bundledCandidates,
26118
26229
  "/Applications/ChatGPT.app/Contents/Resources/codex",
26119
26230
  join5(userHome, "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
26120
26231
  configured
@@ -26935,7 +27046,7 @@ var HostPluginRuntime = class {
26935
27046
  this.fileViewerHost = fileViewerHost;
26936
27047
  this.terminalSpawner = terminalSpawner;
26937
27048
  this.terminalEnabled = config.terminal.enabled;
26938
- this.loopback = new LoopbackHost(config.loopback.ports);
27049
+ this.loopbackPorts = [...config.loopback.ports];
26939
27050
  this.codex = new CodexRemoteDomain(config.codex, logger);
26940
27051
  this.connections = new ConnectionController(this.identities, (context, send) => {
26941
27052
  const harnessApi = this.apiProxy === void 0 ? void 0 : new HarnessApiBridge(
@@ -26978,7 +27089,8 @@ var HostPluginRuntime = class {
26978
27089
  () => this.hostCapabilities(),
26979
27090
  codex,
26980
27091
  acp,
26981
- this.loopback
27092
+ // Handles and their lifetime belong to this connection; only policy is shared.
27093
+ this.createLoopbackHost()
26982
27094
  );
26983
27095
  }, this.logger);
26984
27096
  if (config.serverUrl !== void 0) {
@@ -26987,8 +27099,9 @@ var HostPluginRuntime = class {
26987
27099
  }
26988
27100
  connections;
26989
27101
  terminalOwners = /* @__PURE__ */ new Map();
27102
+ loopbackHosts = /* @__PURE__ */ new Set();
26990
27103
  terminalEnabled;
26991
- loopback;
27104
+ loopbackPorts;
26992
27105
  identity;
26993
27106
  serverApi;
26994
27107
  serverConnection;
@@ -27002,7 +27115,14 @@ var HostPluginRuntime = class {
27002
27115
  this.terminalEnabled = enabled;
27003
27116
  }
27004
27117
  setLoopbackPorts(ports) {
27005
- this.loopback.setPorts(ports);
27118
+ this.loopbackPorts = [...ports];
27119
+ for (const loopback of this.loopbackHosts) loopback.setPorts(this.loopbackPorts);
27120
+ }
27121
+ createLoopbackHost() {
27122
+ let loopback;
27123
+ loopback = new LoopbackHost(() => this.loopbackPorts, () => this.loopbackHosts.delete(loopback));
27124
+ this.loopbackHosts.add(loopback);
27125
+ return loopback;
27006
27126
  }
27007
27127
  async start() {
27008
27128
  if (this.closed) throw new Error("remote runtime is closed");
@@ -27045,6 +27165,9 @@ var HostPluginRuntime = class {
27045
27165
  connectedClients: this.listConnectedClients()
27046
27166
  };
27047
27167
  }
27168
+ async hasStoredAuthorization() {
27169
+ return this.serverApi?.hasStoredAuthorization() ?? false;
27170
+ }
27048
27171
  listConnectedClients() {
27049
27172
  return this.connections.connectedPeers().map((peer) => {
27050
27173
  const trusted = this.identities.trustedPeer(peer.deviceId);
@@ -27228,7 +27351,7 @@ var HostPluginRuntime = class {
27228
27351
  }
27229
27352
  hostCapabilities() {
27230
27353
  const capabilities = [];
27231
- if (this.loopback.hasPorts()) capabilities.push("loopback.http-ws.v1");
27354
+ if (this.loopbackPorts.length > 0) capabilities.push("loopback.http-ws.v1");
27232
27355
  if (this.localGateway?.supportsCarrier === true) {
27233
27356
  capabilities.push(
27234
27357
  harnessSessionGeneration(this.harnessVersion) === "v3" ? "harness.remote.v3" : "harness.remote.v1",