ds-harness-remote 0.4.13 → 0.4.14

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 (46) hide show
  1. package/dist/acp.d.ts +75 -0
  2. package/dist/acp.d.ts.map +1 -0
  3. package/dist/acp.js +61 -0
  4. package/dist/acp.js.map +1 -0
  5. package/dist/client-runtime.d.ts +6 -0
  6. package/dist/client-runtime.d.ts.map +1 -1
  7. package/dist/client-runtime.js.map +1 -1
  8. package/dist/client.github.js +224 -9
  9. package/dist/client.js +224 -9
  10. package/dist/client.js.map +1 -1
  11. package/dist/codex/method-policy.d.ts +10 -10
  12. package/dist/config.d.ts +24 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +3 -0
  15. package/dist/config.js.map +1 -1
  16. package/dist/connection-controller.d.ts +5 -0
  17. package/dist/connection-controller.d.ts.map +1 -1
  18. package/dist/connection-controller.js +16 -1
  19. package/dist/connection-controller.js.map +1 -1
  20. package/dist/control-runtime.d.ts +4 -0
  21. package/dist/control-runtime.d.ts.map +1 -1
  22. package/dist/control-runtime.js +55 -0
  23. package/dist/control-runtime.js.map +1 -1
  24. package/dist/harness-api-bridge.d.ts.map +1 -1
  25. package/dist/harness-api-bridge.js +29 -8
  26. package/dist/harness-api-bridge.js.map +1 -1
  27. package/dist/harness-remote-bridge.d.ts +2 -1
  28. package/dist/harness-remote-bridge.d.ts.map +1 -1
  29. package/dist/harness-remote-bridge.js +44 -2
  30. package/dist/harness-remote-bridge.js.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +328 -23
  34. package/dist/index.js.map +4 -4
  35. package/dist/rpc-router.d.ts +5 -2
  36. package/dist/rpc-router.d.ts.map +1 -1
  37. package/dist/rpc-router.js +18 -1
  38. package/dist/rpc-router.js.map +1 -1
  39. package/dist/service.d.ts +9 -0
  40. package/dist/service.d.ts.map +1 -1
  41. package/dist/service.js +31 -2
  42. package/dist/service.js.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/dsh-plugin.json +1 -1
  46. package/package.json +21 -21
package/dist/index.js CHANGED
@@ -4066,6 +4066,8 @@ var MAX_RPC_TEXT_INPUT_BYTES = 64 * 1024;
4066
4066
  var MAX_FILEVIEWER_RANGE_BYTES = 512 * 1024;
4067
4067
  var MIN_REPLAY_WINDOW_MS = 15 * 6e4;
4068
4068
  var MAX_ALPHA_STREAMS_PER_CONNECTION = 16;
4069
+ var MAX_ACP_PROMPT_BYTES = 64 * 1024;
4070
+ var MAX_ACP_UPDATE_BYTES = 512 * 1024;
4069
4071
  var SECURE_FRAGMENT_MAGIC = new Uint8Array([68, 83, 72, 70]);
4070
4072
  var SECURE_FRAGMENT_VERSION = 1;
4071
4073
  var SECURE_FRAGMENT_HEADER_BYTES = 17;
@@ -4121,7 +4123,14 @@ var rpcMethods = [
4121
4123
  "codex.app.transfer.chunk",
4122
4124
  "codex.app.transfer.commit",
4123
4125
  "codex.app.transfer.read",
4124
- "codex.app.transfer.close"
4126
+ "codex.app.transfer.close",
4127
+ "acp.initialize",
4128
+ "acp.session.new",
4129
+ "acp.session.load",
4130
+ "acp.session.prompt",
4131
+ "acp.session.respond_permission",
4132
+ "acp.session.cancel",
4133
+ "acp.session.set_mode"
4125
4134
  ];
4126
4135
  var errorCodes = [
4127
4136
  // Protocol / Version
@@ -17422,7 +17431,8 @@ var Config = s.object({
17422
17431
  codex: s.object({
17423
17432
  enabled: s.boolean(),
17424
17433
  binary: s.string()
17425
- })
17434
+ }),
17435
+ acp: s.object({ enabled: s.boolean(), backends: s.array(s.object({ id: s.string(), enabled: s.boolean(), command: s.string(), args: s.array(s.string()), cwd: s.string() })) })
17426
17436
  });
17427
17437
  var reconnectSchema = external_exports.union([
17428
17438
  external_exports.boolean(),
@@ -17443,7 +17453,8 @@ var configSchema = external_exports.object({
17443
17453
  codex: external_exports.object({
17444
17454
  enabled: external_exports.boolean().optional(),
17445
17455
  binary: external_exports.string().trim().min(1).max(4096).optional()
17446
- }).strict().optional()
17456
+ }).strict().optional(),
17457
+ acp: external_exports.object({ enabled: external_exports.boolean().optional(), backends: external_exports.array(external_exports.object({ id: external_exports.string().trim().regex(/^[a-z0-9][a-z0-9._-]{0,31}$/i), enabled: external_exports.boolean().optional(), command: external_exports.string().trim().min(1).max(4096).optional(), args: external_exports.array(external_exports.string().max(4096)).max(32).optional(), cwd: external_exports.string().max(4096).optional() }).strict()).max(12).optional(), backend: external_exports.string().trim().regex(/^[a-z0-9][a-z0-9._-]{0,31}$/i).optional(), command: external_exports.string().trim().min(1).max(4096).optional(), args: external_exports.array(external_exports.string().max(4096)).max(32).optional(), cwd: external_exports.string().max(4096).optional() }).strict().optional()
17447
17458
  }).strict();
17448
17459
  function resolveConfig(input2 = {}, env = process.env) {
17449
17460
  const parsed = configSchema.parse(input2);
@@ -17471,7 +17482,12 @@ function resolveConfig(input2 = {}, env = process.env) {
17471
17482
  codex: {
17472
17483
  enabled: parsed.codex?.enabled ?? true,
17473
17484
  binary: parsed.codex?.binary ?? "codex"
17474
- }
17485
+ },
17486
+ acp: { enabled: parsed.acp?.enabled ?? true, backends: [.../* @__PURE__ */ new Set(["codex", "cursor", "kimi", ...parsed.acp?.backends?.map((item) => item.id) ?? []])].map((id2) => {
17487
+ const d = parsed.acp?.backends?.find((x) => x.id === id2);
17488
+ const legacy = parsed.acp?.backend === id2 ? parsed.acp : void 0;
17489
+ return { id: id2, enabled: d?.enabled ?? legacy?.enabled ?? true, command: d?.command ?? legacy?.command ?? { codex: "codex", cursor: "agent", kimi: "kimi" }[id2] ?? id2, args: d?.args ?? legacy?.args ?? ["acp"], ...d?.cwd ?? legacy?.cwd ? { cwd: d?.cwd ?? legacy?.cwd } : {} };
17490
+ }) }
17475
17491
  };
17476
17492
  }
17477
17493
  function normalizeServerUrl(value) {
@@ -17493,7 +17509,7 @@ function normalizeServerUrl(value) {
17493
17509
  }
17494
17510
 
17495
17511
  // src/version.ts
17496
- var PLUGIN_VERSION = "0.4.13";
17512
+ var PLUGIN_VERSION = "0.4.14";
17497
17513
 
17498
17514
  // src/server-api.ts
17499
17515
  var TERMINAL_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/u;
@@ -20182,6 +20198,7 @@ function iceServersForAttempt(attempt, iceServers) {
20182
20198
 
20183
20199
  // src/control-runtime.ts
20184
20200
  import { hostname as hostname2 } from "node:os";
20201
+ import { execFileSync } from "node:child_process";
20185
20202
 
20186
20203
  // src/identity-store.ts
20187
20204
  import { createHash } from "node:crypto";
@@ -20446,6 +20463,9 @@ var PluginControlRuntime = class {
20446
20463
  if (endpoint === "settings.server.set") return ok3(await this.setServer(payload));
20447
20464
  if (endpoint === "settings.role.set") return ok3(await this.setRole(payload));
20448
20465
  if (endpoint === "settings.codex.set") return ok3(await this.setCodex(payload));
20466
+ if (endpoint === "settings.acp.set") return ok3(await this.setAcp(payload));
20467
+ if (endpoint === "settings.acp.add") return ok3(await this.addAcp(payload));
20468
+ if (endpoint === "settings.acp.remove") return ok3(await this.removeAcp(payload));
20449
20469
  if (endpoint === "settings.logout") return ok3(await this.logout());
20450
20470
  if (endpoint === "host.reconnect") {
20451
20471
  if (this.host === void 0) throw new ClientModeError("METHOD_NOT_ALLOWED", "This plugin is not running as a Host.");
@@ -20557,6 +20577,37 @@ var PluginControlRuntime = class {
20557
20577
  await this.settings.replace(editableConfig(next));
20558
20578
  return this.settingsView();
20559
20579
  }
20580
+ async setAcp(payload) {
20581
+ if (this.settings === void 0) throw new ClientModeError("SETTINGS_UNAVAILABLE", "DSH user settings are unavailable in this profile.");
20582
+ const value = record4(payload);
20583
+ if (typeof value.backend !== "string" || typeof value.enabled !== "boolean") throw new ClientModeError("INVALID_MESSAGE", "ACP backend and enabled are required.");
20584
+ const current = resolveConfig(this.settings.get());
20585
+ const backends = current.acp?.backends.map((item) => item.id === value.backend ? { ...item, enabled: value.enabled } : item) ?? [];
20586
+ await this.settings.replace({ ...editableConfig(current), acp: { enabled: current.acp?.enabled ?? true, backends } });
20587
+ return this.settingsView();
20588
+ }
20589
+ async addAcp(payload) {
20590
+ if (this.settings === void 0) throw new ClientModeError("SETTINGS_UNAVAILABLE", "DSH user settings are unavailable in this profile.");
20591
+ const value = record4(payload);
20592
+ if (typeof value.id !== "string" || typeof value.command !== "string" || !Array.isArray(value.args) || !value.args.every((item) => typeof item === "string")) {
20593
+ throw new ClientModeError("INVALID_MESSAGE", "ACP name, command, and arguments are required.");
20594
+ }
20595
+ const current = resolveConfig(this.settings.get());
20596
+ if (current.acp?.backends.some((item) => item.id === value.id)) throw new ClientModeError("INVALID_MESSAGE", "ACP backend already exists.");
20597
+ const backends = [...current.acp?.backends ?? [], { id: value.id, command: value.command, args: value.args, enabled: false }];
20598
+ const next = resolveConfig({ ...editableConfig(current), acp: { enabled: current.acp?.enabled ?? true, backends } });
20599
+ await this.settings.replace(editableConfig(next));
20600
+ return this.settingsView();
20601
+ }
20602
+ async removeAcp(payload) {
20603
+ if (this.settings === void 0) throw new ClientModeError("SETTINGS_UNAVAILABLE", "DSH user settings are unavailable in this profile.");
20604
+ const id2 = record4(payload).id;
20605
+ if (typeof id2 !== "string" || ["codex", "cursor", "kimi"].includes(id2)) throw new ClientModeError("INVALID_MESSAGE", "Only custom ACP backends can be removed.");
20606
+ const current = resolveConfig(this.settings.get());
20607
+ const backends = (current.acp?.backends ?? []).filter((item) => item.id !== id2);
20608
+ await this.settings.replace(editableConfig({ ...current, acp: { enabled: current.acp?.enabled ?? true, backends } }));
20609
+ return this.settingsView();
20610
+ }
20560
20611
  async authorizeOwnedRole(serverUrl, sourceRole, targetRole) {
20561
20612
  const sourceDirectory = serverStorageDirectory(this.identityDirectory, serverUrl, sourceRole);
20562
20613
  const sourceIdentity = await new IdentityStore({ directory: sourceDirectory }).loadOrCreate(hostname2());
@@ -20597,6 +20648,7 @@ var PluginControlRuntime = class {
20597
20648
  writable: this.settings !== void 0,
20598
20649
  applies: "restart",
20599
20650
  associations,
20651
+ acpAvailability: Object.fromEntries((config.acp?.backends ?? []).map((item) => [item.id, commandAvailable(item.command ?? "")])),
20600
20652
  ...association === void 0 ? {} : { association }
20601
20653
  };
20602
20654
  }
@@ -20633,6 +20685,14 @@ var PluginControlRuntime = class {
20633
20685
  };
20634
20686
  }
20635
20687
  };
20688
+ function commandAvailable(command) {
20689
+ try {
20690
+ execFileSync(process.platform === "win32" ? "where" : "which", [command], { stdio: "ignore" });
20691
+ return true;
20692
+ } catch {
20693
+ return false;
20694
+ }
20695
+ }
20636
20696
  function editableConfig(config) {
20637
20697
  return {
20638
20698
  enabled: config.enabled,
@@ -20648,7 +20708,8 @@ function editableConfig(config) {
20648
20708
  codex: {
20649
20709
  enabled: config.codex.enabled,
20650
20710
  binary: config.codex.binary
20651
- }
20711
+ },
20712
+ ...config.acp === void 0 ? {} : { acp: { enabled: config.acp.enabled, backends: config.acp.backends } }
20652
20713
  };
20653
20714
  }
20654
20715
  function isRecord8(value) {
@@ -20779,7 +20840,21 @@ var ConnectionController = class {
20779
20840
  return this.active.size;
20780
20841
  }
20781
20842
  peerDeviceIds() {
20782
- return [...new Set([...this.active.values()].map((connection) => connection.channel.peerDeviceId))];
20843
+ return this.connectedPeers().map((peer) => peer.deviceId);
20844
+ }
20845
+ connectedPeers() {
20846
+ const peers = [];
20847
+ const seen = /* @__PURE__ */ new Set();
20848
+ for (const connection of this.active.values()) {
20849
+ const deviceId = connection.channel.peerDeviceId;
20850
+ if (seen.has(deviceId)) continue;
20851
+ seen.add(deviceId);
20852
+ peers.push({
20853
+ deviceId,
20854
+ ...connection.channel.mode === void 0 ? {} : { mode: connection.channel.mode }
20855
+ });
20856
+ }
20857
+ return peers;
20783
20858
  }
20784
20859
  peerDeviceId() {
20785
20860
  const peers = this.peerDeviceIds();
@@ -20924,7 +20999,14 @@ var apiMethods = /* @__PURE__ */ new Set([
20924
20999
  "codex.app.transfer.chunk",
20925
21000
  "codex.app.transfer.commit",
20926
21001
  "codex.app.transfer.read",
20927
- "codex.app.transfer.close"
21002
+ "codex.app.transfer.close",
21003
+ "acp.initialize",
21004
+ "acp.session.new",
21005
+ "acp.session.load",
21006
+ "acp.session.prompt",
21007
+ "acp.session.respond_permission",
21008
+ "acp.session.cancel",
21009
+ "acp.session.set_mode"
20928
21010
  ]);
20929
21011
  var HOST_CAPABILITIES = [
20930
21012
  "harness.api.v1",
@@ -20933,10 +21015,11 @@ var HOST_CAPABILITIES = [
20933
21015
  "harness.remote.transfer.v1",
20934
21016
  "fileviewer.read.v1",
20935
21017
  "codex.appserver.v1",
20936
- "codex.appserver.transfer.v1"
21018
+ "codex.appserver.transfer.v1",
21019
+ "agent.acp.v1"
20937
21020
  ];
20938
21021
  var RpcRouter = class {
20939
- constructor(harnessApi, maxPending = 128, logger, fileViewer, harnessRemote, capabilities = () => HOST_CAPABILITIES, codex) {
21022
+ constructor(harnessApi, maxPending = 128, logger, fileViewer, harnessRemote, capabilities = () => HOST_CAPABILITIES, codex, acp) {
20940
21023
  this.harnessApi = harnessApi;
20941
21024
  this.maxPending = maxPending;
20942
21025
  this.logger = logger;
@@ -20944,6 +21027,7 @@ var RpcRouter = class {
20944
21027
  this.harnessRemote = harnessRemote;
20945
21028
  this.capabilities = capabilities;
20946
21029
  this.codex = codex;
21030
+ this.acp = acp;
20947
21031
  }
20948
21032
  active = 0;
20949
21033
  async closePeerStreams() {
@@ -21052,6 +21136,20 @@ var RpcRouter = class {
21052
21136
  return this.requireCodex().readTransfer(params);
21053
21137
  case "codex.app.transfer.close":
21054
21138
  return this.requireCodex().closeTransfer(params);
21139
+ case "acp.initialize":
21140
+ return this.requireAcp().initialize(params);
21141
+ case "acp.session.new":
21142
+ return this.requireAcp().sessionNew(params);
21143
+ case "acp.session.load":
21144
+ return this.requireAcp().sessionLoad(params);
21145
+ case "acp.session.prompt":
21146
+ return this.requireAcp().prompt(params, async () => void 0);
21147
+ case "acp.session.respond_permission":
21148
+ return this.requireAcp().respondPermission(params);
21149
+ case "acp.session.cancel":
21150
+ return this.requireAcp().cancel(params);
21151
+ case "acp.session.set_mode":
21152
+ return this.requireAcp().setMode(params);
21055
21153
  default:
21056
21154
  throw new RpcError("METHOD_NOT_FOUND", "The requested method does not exist.");
21057
21155
  }
@@ -21068,6 +21166,10 @@ var RpcRouter = class {
21068
21166
  }
21069
21167
  return this.harnessRemote;
21070
21168
  }
21169
+ requireAcp() {
21170
+ if (!this.acp) throw new RpcError("CAPABILITY_NOT_SUPPORTED", "ACP is not configured on this Host.");
21171
+ return this.acp;
21172
+ }
21071
21173
  requireCodex() {
21072
21174
  if (this.codex === void 0) {
21073
21175
  throw new RpcError("FEATURE_NOT_SUPPORTED", "Codex Remote is disabled or unavailable on this Host.");
@@ -22154,11 +22256,22 @@ var transferCloseSchema = external_exports.object({ transferId: transferIdSchema
22154
22256
  var commandExecuteSchema = external_exports.object({
22155
22257
  agentId: external_exports.string().min(1).max(128),
22156
22258
  line: external_exports.string().min(1).max(2048),
22157
- // The official Harness command endpoint requires the `images` wire field.
22158
- // Remote command execution keeps attachments out of scope, so only an empty
22159
- // list is accepted and legacy clients that omit it are normalized below.
22160
- images: external_exports.array(external_exports.never()).length(0).optional()
22161
- }).strict();
22259
+ // dsh-commands <= 0.1.2 calls this field `images`; dsh-commands 0.1.5
22260
+ // renamed it to `submittedAttachments` and expanded its element type to
22261
+ // include staged file receipts. Remote command execution keeps attachments
22262
+ // out of scope, so either compatibility field is restricted to an empty
22263
+ // list. The dispatcher below chooses the matching Host descriptor.
22264
+ images: external_exports.array(external_exports.never()).length(0).optional(),
22265
+ submittedAttachments: external_exports.array(external_exports.never()).length(0).optional()
22266
+ }).strict().superRefine((value, context) => {
22267
+ if (value.images !== void 0 && value.submittedAttachments !== void 0) {
22268
+ context.addIssue({
22269
+ code: external_exports.ZodIssueCode.custom,
22270
+ message: "Use either images or submittedAttachments, not both.",
22271
+ path: ["submittedAttachments"]
22272
+ });
22273
+ }
22274
+ });
22162
22275
  var commandListSchema = external_exports.object({
22163
22276
  agentId: external_exports.string().min(1).max(128)
22164
22277
  }).strict();
@@ -22279,7 +22392,7 @@ var HarnessApiBridge = class {
22279
22392
  this.maxStreams = maxStreams;
22280
22393
  this.logger = logger;
22281
22394
  this.harnessVersion = harnessVersion;
22282
- this.methods = createMethodMap(api, typertGateway);
22395
+ this.methods = createMethodMap(api, typertGateway, harnessVersion);
22283
22396
  this.mux = api.events.mux.bind(api.events);
22284
22397
  this.host = api.events.host.bind(api.events);
22285
22398
  this.answer = api.respond.bind(api);
@@ -22600,7 +22713,7 @@ function normalizeHostDescribe(response, harnessVersion, canListDirectory) {
22600
22713
  }
22601
22714
  };
22602
22715
  }
22603
- function createMethodMap(api, typertGateway) {
22716
+ function createMethodMap(api, typertGateway, harnessVersion) {
22604
22717
  const domains = api;
22605
22718
  const methods = /* @__PURE__ */ new Map();
22606
22719
  for (const method of HARNESS_API_ALLOWLIST) {
@@ -22610,7 +22723,17 @@ function createMethodMap(api, typertGateway) {
22610
22723
  const implementation2 = async (request, signal) => {
22611
22724
  if (commandMethod === "execute") {
22612
22725
  const payload = commandExecuteSchema.parse(request.payload);
22613
- const args2 = { ...payload, images: payload.images ?? [] };
22726
+ const legacyArgs = {
22727
+ agentId: payload.agentId,
22728
+ line: payload.line,
22729
+ images: payload.images ?? []
22730
+ };
22731
+ const currentArgs = {
22732
+ agentId: payload.agentId,
22733
+ line: payload.line,
22734
+ submittedAttachments: payload.submittedAttachments ?? []
22735
+ };
22736
+ const args2 = harnessSessionGeneration(harnessVersion) === "v3" ? currentArgs : legacyArgs;
22614
22737
  const value2 = await typertGateway.invoke({
22615
22738
  namespace,
22616
22739
  method: "execute",
@@ -22912,6 +23035,24 @@ var streamOpenSchema2 = external_exports.object({
22912
23035
  payload: external_exports.unknown()
22913
23036
  }).strict();
22914
23037
  var streamCloseSchema2 = external_exports.object({ streamId: external_exports.string().min(1).max(128) }).strict();
23038
+ var commandExecuteArgsSchema = external_exports.object({
23039
+ agentId: external_exports.string().min(1).max(128),
23040
+ line: external_exports.string().min(1).max(2048),
23041
+ // dsh-commands <= 0.1.2 calls this field `images`; dsh-commands 0.1.5
23042
+ // renamed it to `submittedAttachments`. Only empty arrays are exposed by
23043
+ // the Remote bridge; command attachments are deliberately out of scope.
23044
+ images: external_exports.array(external_exports.never()).length(0).optional(),
23045
+ submittedAttachments: external_exports.array(external_exports.never()).length(0).optional()
23046
+ }).strict().superRefine((value, context) => {
23047
+ if (value.images !== void 0 && value.submittedAttachments !== void 0) {
23048
+ context.addIssue({
23049
+ code: external_exports.ZodIssueCode.custom,
23050
+ message: "Use either images or submittedAttachments, not both.",
23051
+ path: ["submittedAttachments"]
23052
+ });
23053
+ }
23054
+ });
23055
+ var commandExecutePayloadSchema = external_exports.object({ args: commandExecuteArgsSchema }).strict();
22915
23056
  var transferOpenSchema2 = external_exports.object({
22916
23057
  transferId: external_exports.string().uuid(),
22917
23058
  totalBytes: external_exports.number().int().positive().max(MAX_HARNESS_API_TRANSFER_BYTES),
@@ -22993,10 +23134,11 @@ var HARNESS_REMOTE_ALLOWLIST = [
22993
23134
  ];
22994
23135
  var allowedEndpoints = new Set(HARNESS_REMOTE_ALLOWLIST);
22995
23136
  var HarnessRemoteBridge = class {
22996
- constructor(gateway, publish, logger) {
23137
+ constructor(gateway, publish, logger, harnessVersion) {
22997
23138
  this.gateway = gateway;
22998
23139
  this.publish = publish;
22999
23140
  this.logger = logger;
23141
+ this.harnessVersion = harnessVersion;
23000
23142
  }
23001
23143
  streams = /* @__PURE__ */ new Map();
23002
23144
  incomingTransfers = /* @__PURE__ */ new Map();
@@ -23010,7 +23152,7 @@ var HarnessRemoteBridge = class {
23010
23152
  const startedAt = performance.now();
23011
23153
  const signal = AbortSignal.timeout(6e4);
23012
23154
  try {
23013
- const nativeResult = await this.gateway.dispatch(params.endpoint, params.payload, signal);
23155
+ const nativeResult = params.endpoint === "commands/execute" ? await dispatchCommandForHost(this.gateway, params.payload, signal, this.harnessVersion) : await this.gateway.dispatch(params.endpoint, params.payload, signal);
23014
23156
  const result = params.endpoint === "directoryPicker/list" && needsDirectoryFallback(nativeResult) ? await this.directoryList(params.payload, signal) : nativeResult;
23015
23157
  this.logger?.debug("harness remote call ok", {
23016
23158
  endpoint: params.endpoint,
@@ -23209,6 +23351,29 @@ var HarnessRemoteBridge = class {
23209
23351
  for (const [id2, transfer] of this.outgoingTransfers) if (transfer.touchedAt < cutoff) this.outgoingTransfers.delete(id2);
23210
23352
  }
23211
23353
  };
23354
+ async function dispatchCommandForHost(gateway, payload, signal, harnessVersion) {
23355
+ const parsed = commandExecutePayloadSchema.parse(payload);
23356
+ const legacyPayload = {
23357
+ args: {
23358
+ agentId: parsed.args.agentId,
23359
+ line: parsed.args.line,
23360
+ images: parsed.args.images ?? []
23361
+ }
23362
+ };
23363
+ const currentPayload = {
23364
+ args: {
23365
+ agentId: parsed.args.agentId,
23366
+ line: parsed.args.line,
23367
+ submittedAttachments: parsed.args.submittedAttachments ?? []
23368
+ }
23369
+ };
23370
+ const args = harnessSessionGeneration(harnessVersion) === "v3" ? currentPayload : legacyPayload;
23371
+ return gateway.dispatch(
23372
+ "commands/execute",
23373
+ args,
23374
+ signal
23375
+ );
23376
+ }
23212
23377
  function decodeCanonicalBase642(value) {
23213
23378
  const bytes = Buffer.from(value, "base64");
23214
23379
  if (bytes.toString("base64") !== value) throw new RpcError("INVALID_MESSAGE", "The Harness Remote transfer chunk is invalid.");
@@ -24912,7 +25077,119 @@ function isActiveWriterMessage(message) {
24912
25077
  return message.toLowerCase().includes("active writer");
24913
25078
  }
24914
25079
 
25080
+ // src/acp.ts
25081
+ import { spawn as spawn3 } from "node:child_process";
25082
+ var AcpGateway = class {
25083
+ constructor(adapters) {
25084
+ this.adapters = adapters;
25085
+ }
25086
+ get(p) {
25087
+ const list = this.adapters instanceof Object && "backend" in this.adapters ? [this.adapters] : [...this.adapters];
25088
+ const a = list.find((x) => !p.backend || x.backend === p.backend);
25089
+ if (!a) throw new Error("CAPABILITY_NOT_SUPPORTED");
25090
+ return a;
25091
+ }
25092
+ initialize(p) {
25093
+ return this.get(p).initialize(p);
25094
+ }
25095
+ sessionNew(p) {
25096
+ return this.get(p).sessionNew(p);
25097
+ }
25098
+ sessionLoad(p) {
25099
+ const a = this.get(p);
25100
+ if (!a.sessionLoad) throw new Error("CAPABILITY_NOT_SUPPORTED");
25101
+ return a.sessionLoad(p);
25102
+ }
25103
+ prompt(p, emit) {
25104
+ return this.get(p).prompt(p, emit);
25105
+ }
25106
+ respondPermission(p) {
25107
+ const a = this.get(p);
25108
+ if (!a.respondPermission) throw new Error("CAPABILITY_NOT_SUPPORTED");
25109
+ return a.respondPermission(p);
25110
+ }
25111
+ cancel(p) {
25112
+ const a = this.get(p);
25113
+ if (!a.cancel) throw new Error("CAPABILITY_NOT_SUPPORTED");
25114
+ return a.cancel(p);
25115
+ }
25116
+ setMode(p) {
25117
+ const a = this.get(p);
25118
+ if (!a.setMode) throw new Error("CAPABILITY_NOT_SUPPORTED");
25119
+ return a.setMode(p);
25120
+ }
25121
+ };
25122
+ var StdioAcpAdapter = class {
25123
+ constructor(config) {
25124
+ this.config = config;
25125
+ this.backend = config.id;
25126
+ }
25127
+ backend;
25128
+ child;
25129
+ nextId = 1;
25130
+ ensure() {
25131
+ if (!this.child) this.child = spawn3(this.config.command, this.config.args ?? [], { cwd: this.config.cwd, stdio: "pipe" });
25132
+ return this.child;
25133
+ }
25134
+ async initialize(params) {
25135
+ await this.call("initialize", params);
25136
+ return { protocolVersion: 1, capability: "agent.acp.v1", backend: this.backend, capabilities: ["session.new", "session.load", "session.prompt", "session.cancel"] };
25137
+ }
25138
+ async sessionNew(params) {
25139
+ const r = await this.call("session/new", params);
25140
+ if (!r.sessionId) throw new Error("INVALID_MESSAGE");
25141
+ return { sessionId: r.sessionId };
25142
+ }
25143
+ async sessionLoad(params) {
25144
+ const r = await this.call("session/load", params);
25145
+ if (!r.sessionId) throw new Error("INVALID_MESSAGE");
25146
+ return { sessionId: r.sessionId };
25147
+ }
25148
+ async prompt(params, emit) {
25149
+ await this.call("session/prompt", params, async (n) => emit({ sessionId: params.sessionId, update: n, seq: this.nextId++ }));
25150
+ }
25151
+ async cancel(p) {
25152
+ await this.call("session/cancel", p);
25153
+ }
25154
+ async setMode(p) {
25155
+ await this.call("session/set_mode", p);
25156
+ }
25157
+ async respondPermission(p) {
25158
+ await this.call("session/request_permission", p);
25159
+ }
25160
+ async close() {
25161
+ this.child?.kill();
25162
+ this.child = void 0;
25163
+ }
25164
+ call(method, params, onNotification) {
25165
+ const c = this.ensure();
25166
+ const id2 = this.nextId++;
25167
+ c.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: id2, method, params }) + "\n");
25168
+ return new Promise((resolve3, reject) => {
25169
+ let buf = "";
25170
+ const onData = async (d) => {
25171
+ buf += d;
25172
+ const lines = buf.split("\n");
25173
+ buf = lines.pop() ?? "";
25174
+ for (const l of lines) {
25175
+ try {
25176
+ const m = JSON.parse(l);
25177
+ if (m.id === id2) {
25178
+ c.stdout.off("data", onData);
25179
+ m.error ? reject(new Error(m.error.message ?? "ACP error")) : resolve3(m.result);
25180
+ } else if (m.method && onNotification) await onNotification(m.params);
25181
+ } catch {
25182
+ }
25183
+ }
25184
+ };
25185
+ c.stdout.on("data", onData);
25186
+ c.once("error", reject);
25187
+ });
25188
+ }
25189
+ };
25190
+
24915
25191
  // src/service.ts
25192
+ import { execFileSync as execFileSync2 } from "node:child_process";
24916
25193
  var HostPluginRuntime = class {
24917
25194
  constructor(config, identities, apiProxy, logger, localGateway, fileViewerHost) {
24918
25195
  this.config = config;
@@ -24934,7 +25211,8 @@ var HostPluginRuntime = class {
24934
25211
  const harnessRemote = this.localGateway?.supportsCarrier === true ? new HarnessRemoteBridge(
24935
25212
  this.localGateway,
24936
25213
  (event, data) => send(createEvent(event, data)),
24937
- this.logger
25214
+ this.logger,
25215
+ this.harnessVersion
24938
25216
  ) : void 0;
24939
25217
  const fileViewer = new RemoteFileViewerBridge(
24940
25218
  () => this.fileViewerHost?.(),
@@ -24944,6 +25222,8 @@ var HostPluginRuntime = class {
24944
25222
  context,
24945
25223
  (event, data) => send(createEvent(event, data))
24946
25224
  );
25225
+ const adapters = (config.acp?.backends ?? []).filter((item) => item.enabled && this.acpAvailable(item.command)).map((item) => new StdioAcpAdapter(item));
25226
+ const acp = config.acp?.enabled && adapters.length > 0 ? new AcpGateway(adapters) : void 0;
24947
25227
  return new RpcRouter(
24948
25228
  harnessApi,
24949
25229
  void 0,
@@ -24951,7 +25231,8 @@ var HostPluginRuntime = class {
24951
25231
  fileViewer,
24952
25232
  harnessRemote,
24953
25233
  () => this.hostCapabilities(),
24954
- codex
25234
+ codex,
25235
+ acp
24955
25236
  );
24956
25237
  }, this.logger);
24957
25238
  if (config.serverUrl !== void 0) {
@@ -25004,9 +25285,21 @@ var HostPluginRuntime = class {
25004
25285
  ...error === void 0 ? {} : { error },
25005
25286
  ...authorization?.account === void 0 ? {} : { account: authorization.account },
25006
25287
  authorized: authorization !== void 0,
25007
- accountRequired: error === "ACCOUNT_AUTH_REQUIRED" || error === "AUTH_INVALID" || error === "TOKEN_EXPIRED"
25288
+ accountRequired: error === "ACCOUNT_AUTH_REQUIRED" || error === "AUTH_INVALID" || error === "TOKEN_EXPIRED",
25289
+ connectedClients: this.listConnectedClients()
25008
25290
  };
25009
25291
  }
25292
+ listConnectedClients() {
25293
+ return this.connections.connectedPeers().map((peer) => {
25294
+ const trusted = this.identities.trustedPeer(peer.deviceId);
25295
+ return {
25296
+ deviceId: peer.deviceId,
25297
+ name: trusted?.name.trim() ?? "",
25298
+ ...trusted === void 0 ? {} : { platform: trusted.platform },
25299
+ ...peer.mode === void 0 ? {} : { mode: peer.mode }
25300
+ };
25301
+ });
25302
+ }
25010
25303
  localHarnessVersion() {
25011
25304
  return this.harnessVersion;
25012
25305
  }
@@ -25190,8 +25483,19 @@ var HostPluginRuntime = class {
25190
25483
  }
25191
25484
  if (this.fileViewerHost?.() !== void 0) capabilities.push("fileviewer.read.v1");
25192
25485
  if (this.codex.isAvailable()) capabilities.push("codex.appserver.v1", "codex.appserver.transfer.v1");
25486
+ if (this.config.acp?.enabled) {
25487
+ for (const item of this.config.acp.backends) if (item.enabled && this.acpAvailable(item.command)) capabilities.push(`agent.acp.v1.${item.id}`);
25488
+ }
25193
25489
  return capabilities;
25194
25490
  }
25491
+ acpAvailable(command) {
25492
+ try {
25493
+ execFileSync2(process.platform === "win32" ? "where" : "which", [command], { stdio: "ignore" });
25494
+ return true;
25495
+ } catch {
25496
+ return false;
25497
+ }
25498
+ }
25195
25499
  requireLocalCodexPeer() {
25196
25500
  if (!this.codex.isAvailable()) {
25197
25501
  throw new RpcError("CODEX_UNAVAILABLE", "Local CodeX is disabled or unavailable on this Host.");
@@ -26108,6 +26412,7 @@ function isPlainRecord2(value) {
26108
26412
  return typeof value === "object" && value !== null && !Array.isArray(value);
26109
26413
  }
26110
26414
  export {
26415
+ AcpGateway,
26111
26416
  ApiProxySwitch,
26112
26417
  CODEX_APP_ALLOWLIST,
26113
26418
  ClientModeError,