@ai-sdk/harness-opencode 1.0.78 → 1.0.80

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.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _ai_sdk_provider_utils from '@ai-sdk/provider-utils';
2
2
  import * as _ai_sdk_harness from '@ai-sdk/harness';
3
- import { HarnessV1, HarnessV1BuiltinTool } from '@ai-sdk/harness';
3
+ import { HarnessV1PortEndpoint, HarnessV1, HarnessV1BuiltinTool } from '@ai-sdk/harness';
4
4
 
5
5
  type OpenCodeResolvedAuthenticationMode = 'anthropic' | 'openai' | 'ai-gateway';
6
6
  type OpenCodeAuthenticationMode = OpenCodeResolvedAuthenticationMode | 'auto';
@@ -48,6 +48,11 @@ type OpenCodeHarnessSettings = {
48
48
  */
49
49
  readonly reasoningVariant?: string;
50
50
  readonly port?: number;
51
+ /**
52
+ * Override the host endpoint used to connect to the sandbox bridge. Required
53
+ * together with `port` when using a basic sandbox session.
54
+ */
55
+ readonly portEndpoint?: HarnessV1PortEndpoint;
51
56
  readonly startupTimeoutMs?: number;
52
57
  /**
53
58
  * Creates the authentication token used by the sandbox bridge. Defaults to
package/dist/index.js CHANGED
@@ -8,12 +8,15 @@ import {
8
8
  } from "@ai-sdk/harness";
9
9
  import {
10
10
  classifyDiskLog,
11
+ experimental_createBridgeUserMessageSubmitter,
11
12
  createBridgeErrorHandler,
12
13
  createBridgeStartupError,
13
14
  drainBridgeProcessStream,
14
15
  forwardBridgeProcessStream,
16
+ getRestrictedSandboxSession,
15
17
  markBridgeStarting,
16
18
  maskSandboxCredentials,
19
+ resolveSandboxDefaultWorkingDirectory,
17
20
  resolveSandboxHomeDir,
18
21
  SandboxChannel,
19
22
  shellQuote,
@@ -22,6 +25,7 @@ import {
22
25
  writeSkills as writeHarnessSkills
23
26
  } from "@ai-sdk/harness/utils";
24
27
  import {
28
+ safeParseJSON,
25
29
  tool
26
30
  } from "@ai-sdk/provider-utils";
27
31
  import { WebSocket } from "ws";
@@ -317,7 +321,7 @@ var inboundMessageSchema = z.discriminatedUnion("type", [
317
321
  ]);
318
322
 
319
323
  // src/version.ts
320
- var VERSION = true ? "1.0.78" : "0.0.0-test";
324
+ var VERSION = true ? "1.0.80" : "0.0.0-test";
321
325
 
322
326
  // src/opencode-harness.ts
323
327
  var OPENCODE_CLIENT_APP = `ai-sdk/harness-opencode/${VERSION}`;
@@ -445,6 +449,23 @@ function createOpenCode(settings = {}) {
445
449
  getBootstrap: getOpenCodeBootstrap,
446
450
  doStart: async (startOpts) => {
447
451
  const sandboxSession = startOpts.sandboxSession;
452
+ const toolSafeSandboxSession = getRestrictedSandboxSession(sandboxSession);
453
+ const sandboxId = "id" in sandboxSession ? sandboxSession.id : void 0;
454
+ validateBasicSandboxSettings({
455
+ sandboxSession,
456
+ port: settings.port,
457
+ portEndpoint: settings.portEndpoint
458
+ });
459
+ if (settings.mintBridgeToken != null && sandboxId == null) {
460
+ throw new HarnessCapabilityUnsupportedError({
461
+ harnessId: "opencode",
462
+ message: "The OpenCode harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id."
463
+ });
464
+ }
465
+ const defaultWorkingDirectory = await resolveSandboxDefaultWorkingDirectory({
466
+ sandboxSession,
467
+ abortSignal: startOpts.abortSignal
468
+ });
448
469
  const authenticationMode = resolveOpenCodeAuthenticationMode({
449
470
  auth: settings.auth,
450
471
  model: settings.model,
@@ -456,7 +477,7 @@ function createOpenCode(settings = {}) {
456
477
  provider: settings.provider
457
478
  });
458
479
  let sandboxAuthEnvironment = resolvedAuthEnvironment;
459
- if (sandboxSession.addRequestTransformations != null) {
480
+ if ("addRequestTransformations" in sandboxSession && sandboxSession.addRequestTransformations != null) {
460
481
  const requestTransformations = createOpenCodeRequestTransformations(
461
482
  resolvedAuthEnvironment,
462
483
  authenticationMode
@@ -473,10 +494,8 @@ function createOpenCode(settings = {}) {
473
494
  } else {
474
495
  warnCredentialBrokeringUnavailable();
475
496
  }
476
- const session = sandboxSession.restricted();
477
- const sandboxId = sandboxSession.id;
478
497
  const bootstrapDir = path.posix.resolve(
479
- sandboxSession.defaultWorkingDirectory,
498
+ defaultWorkingDirectory,
480
499
  OPENCODE_BOOTSTRAP_DIR
481
500
  );
482
501
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
@@ -486,7 +505,7 @@ function createOpenCode(settings = {}) {
486
505
  const resumeSessionId = typeof resumeData?.openCodeSessionId === "string" && resumeData.openCodeSessionId.length > 0 ? resumeData.openCodeSessionId : void 0;
487
506
  const coords = resumeData?.bridge;
488
507
  const workDir = startOpts.sessionWorkDir;
489
- const sessionDataDir = `${sandboxSession.defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
508
+ const sessionDataDir = `${defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
490
509
  const bridgeStateDir = `${sessionDataDir}/bridge`;
491
510
  const timeoutMs = settings.startupTimeoutMs ?? 12e4;
492
511
  const model = splitOpenCodeModel(settings.model, settings.provider).model;
@@ -503,16 +522,24 @@ function createOpenCode(settings = {}) {
503
522
  });
504
523
  if (coords) {
505
524
  try {
506
- const endpoint2 = await sandboxSession.getPortEndpoint({
507
- port: coords.port,
508
- protocol: "ws"
525
+ const endpoint2 = await resolveBridgeEndpoint({
526
+ sandboxSession,
527
+ override: settings.portEndpoint,
528
+ port: coords.port
509
529
  });
510
530
  const attachEndpoint = withBridgeToken({
511
531
  endpoint: endpoint2,
512
532
  token: coords.token
513
533
  });
534
+ let supportsUserMessageResponses2 = false;
514
535
  const attachChannel = new SandboxChannel({
515
- connect: () => openWebSocket(attachEndpoint),
536
+ connect: () => openWebSocket({
537
+ endpoint: attachEndpoint,
538
+ helloTimeoutMs: Math.min(timeoutMs, 5e3),
539
+ onHello: (supported) => {
540
+ supportsUserMessageResponses2 = supported;
541
+ }
542
+ }),
516
543
  outboundSchema: outboundMessageSchema,
517
544
  initialLastSeenEventId: coords.lastSeenEventId,
518
545
  onDiagnostic,
@@ -536,7 +563,8 @@ function createOpenCode(settings = {}) {
536
563
  sandboxId,
537
564
  debug: startOpts.observability?.debug,
538
565
  permissionMode: startOpts.permissionMode,
539
- builtinToolFiltering: startOpts.builtinToolFiltering
566
+ builtinToolFiltering: startOpts.builtinToolFiltering,
567
+ supportsUserMessageResponses: () => supportsUserMessageResponses2
540
568
  });
541
569
  } catch {
542
570
  }
@@ -544,7 +572,7 @@ function createOpenCode(settings = {}) {
544
572
  let respawnStrategy = isResume ? "rerun" : void 0;
545
573
  if (coords && isContinue) {
546
574
  const logRaw = await Promise.resolve(
547
- session.readTextFile({
575
+ toolSafeSandboxSession.readTextFile({
548
576
  path: `${bridgeStateDir}/event-log.ndjson`,
549
577
  abortSignal: startOpts.abortSignal
550
578
  })
@@ -553,10 +581,13 @@ function createOpenCode(settings = {}) {
553
581
  respawnStrategy = "replay";
554
582
  }
555
583
  }
556
- const port = resolveBridgePort(sandboxSession, settings.port);
584
+ const port = resolveBridgePort({
585
+ sandboxSession,
586
+ override: settings.port
587
+ });
557
588
  const token = settings.mintBridgeToken == null ? randomBytes(32).toString("hex") : settings.mintBridgeToken(sandboxId);
558
589
  const sandboxHomeDir = await resolveSandboxHomeDir({
559
- sandbox: session,
590
+ sandbox: toolSafeSandboxSession,
560
591
  abortSignal: startOpts.abortSignal
561
592
  });
562
593
  const xdgConfigHome = `${sandboxHomeDir}/.config`;
@@ -564,7 +595,7 @@ function createOpenCode(settings = {}) {
564
595
  const xdgDataHome = `${sandboxHomeDir}/.local/share`;
565
596
  const xdgStateHome = `${sandboxHomeDir}/.local/state`;
566
597
  const skillSetup = startOpts.skills && startOpts.skills.length > 0 ? await writeOpenCodeSkills({
567
- sandbox: session,
598
+ sandbox: toolSafeSandboxSession,
568
599
  skills: startOpts.skills,
569
600
  homeDir: sandboxHomeDir,
570
601
  abortSignal: startOpts.abortSignal
@@ -583,18 +614,18 @@ function createOpenCode(settings = {}) {
583
614
  ...respawnStrategy === "replay" ? { BRIDGE_REPLAY_FROM_DISK: "1" } : {}
584
615
  };
585
616
  if (respawnStrategy === void 0) {
586
- await session.run({
617
+ await toolSafeSandboxSession.run({
587
618
  command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)} ${shellQuote(xdgConfigHome)} ${shellQuote(xdgCacheHome)} ${shellQuote(xdgDataHome)} ${shellQuote(xdgStateHome)}`,
588
619
  abortSignal: startOpts.abortSignal
589
620
  });
590
621
  }
591
622
  await markBridgeStarting({
592
- sandbox: session,
623
+ sandbox: toolSafeSandboxSession,
593
624
  bridgeStateDir,
594
625
  bridgeType: "opencode",
595
626
  abortSignal: startOpts.abortSignal
596
627
  });
597
- const proc = await session.spawn({
628
+ const proc = await toolSafeSandboxSession.spawn({
598
629
  command: `node ${shellQuote(`${bootstrapDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(bootstrapDir)}${skillSetup ? ` --skills-dir ${shellQuote(skillSetup.skillsDir)}` : ""}`,
599
630
  env,
600
631
  abortSignal: startOpts.abortSignal
@@ -608,7 +639,7 @@ function createOpenCode(settings = {}) {
608
639
  });
609
640
  const { port: boundPort } = await waitForBridgeReady({
610
641
  proc,
611
- sandbox: session,
642
+ sandbox: toolSafeSandboxSession,
612
643
  bridgeStateDir,
613
644
  bridgeType: "opencode",
614
645
  timeoutMs,
@@ -629,13 +660,21 @@ function createOpenCode(settings = {}) {
629
660
  })
630
661
  });
631
662
  void drainBridgeProcessStream(proc.stdout);
632
- const endpoint = await sandboxSession.getPortEndpoint({
633
- port: boundPort,
634
- protocol: "ws"
663
+ const endpoint = await resolveBridgeEndpoint({
664
+ sandboxSession,
665
+ override: settings.portEndpoint,
666
+ port: boundPort
635
667
  });
636
668
  const bridgeEndpoint = withBridgeToken({ endpoint, token });
669
+ let supportsUserMessageResponses = false;
637
670
  const channel = new SandboxChannel({
638
- connect: () => openWebSocket(bridgeEndpoint),
671
+ connect: () => openWebSocket({
672
+ endpoint: bridgeEndpoint,
673
+ helloTimeoutMs: Math.min(timeoutMs, 5e3),
674
+ onHello: (supported) => {
675
+ supportsUserMessageResponses = supported;
676
+ }
677
+ }),
639
678
  outboundSchema: outboundMessageSchema,
640
679
  onDiagnostic,
641
680
  onBridgeError,
@@ -661,19 +700,58 @@ function createOpenCode(settings = {}) {
661
700
  sandboxId,
662
701
  debug: startOpts.observability?.debug,
663
702
  permissionMode: startOpts.permissionMode,
664
- builtinToolFiltering: startOpts.builtinToolFiltering
703
+ builtinToolFiltering: startOpts.builtinToolFiltering,
704
+ supportsUserMessageResponses: () => supportsUserMessageResponses
665
705
  });
666
706
  }
667
707
  };
668
708
  }
669
- function resolveBridgePort(sandboxSession, override) {
709
+ function resolveBridgePort({
710
+ sandboxSession,
711
+ override
712
+ }) {
670
713
  if (override !== void 0) return override;
671
- if (sandboxSession.ports.length > 0) return sandboxSession.ports[0];
714
+ if ("ports" in sandboxSession && sandboxSession.ports.length > 0) {
715
+ return sandboxSession.ports[0];
716
+ }
672
717
  throw new HarnessCapabilityUnsupportedError({
673
718
  harnessId: "opencode",
674
719
  message: "The OpenCode harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createOpenCode({ port })`."
675
720
  });
676
721
  }
722
+ function validateBasicSandboxSettings({
723
+ sandboxSession,
724
+ port,
725
+ portEndpoint
726
+ }) {
727
+ if ("getPortEndpoint" in sandboxSession) return;
728
+ if (port == null) {
729
+ throw new HarnessCapabilityUnsupportedError({
730
+ harnessId: "opencode",
731
+ message: "The OpenCode harness requires an explicit `port` when using a basic sandbox session."
732
+ });
733
+ }
734
+ if (portEndpoint == null) {
735
+ throw new HarnessCapabilityUnsupportedError({
736
+ harnessId: "opencode",
737
+ message: "The OpenCode harness requires an explicit `portEndpoint` when using a basic sandbox session."
738
+ });
739
+ }
740
+ }
741
+ async function resolveBridgeEndpoint({
742
+ sandboxSession,
743
+ override,
744
+ port
745
+ }) {
746
+ if (override != null) return override;
747
+ if ("getPortEndpoint" in sandboxSession) {
748
+ return sandboxSession.getPortEndpoint({ port, protocol: "ws" });
749
+ }
750
+ throw new HarnessCapabilityUnsupportedError({
751
+ harnessId: "opencode",
752
+ message: "The OpenCode harness requires an explicit `portEndpoint` when using a basic sandbox session."
753
+ });
754
+ }
677
755
  async function writeOpenCodeSkills({
678
756
  sandbox,
679
757
  skills,
@@ -692,25 +770,88 @@ async function writeOpenCodeSkills({
692
770
  return { skillsDir };
693
771
  }
694
772
  function openWebSocket({
695
- url,
696
- headers
773
+ endpoint,
774
+ helloTimeoutMs,
775
+ onHello
697
776
  }) {
698
777
  return new Promise((resolve, reject) => {
699
- const ws = new WebSocket(url, {
700
- headers: headers == null ? void 0 : { ...headers }
778
+ const ws = new WebSocket(endpoint.url, {
779
+ headers: endpoint.headers == null ? void 0 : { ...endpoint.headers }
701
780
  });
702
- const onOpen = () => {
781
+ let opened = false;
782
+ let receivedHello = false;
783
+ let settled = false;
784
+ const cleanup = () => {
785
+ clearTimeout(helloTimer);
786
+ ws.off("open", onOpen);
787
+ ws.off("message", onMessage);
788
+ ws.off("close", onClose);
703
789
  ws.off("error", onError);
704
- resolve(ws);
705
790
  };
791
+ const settle = (error) => {
792
+ if (settled) return;
793
+ settled = true;
794
+ cleanup();
795
+ if (error == null) {
796
+ resolve(ws);
797
+ } else {
798
+ reject(error);
799
+ }
800
+ };
801
+ const tryResolve = () => {
802
+ if (opened && receivedHello) settle();
803
+ };
804
+ const onOpen = () => {
805
+ opened = true;
806
+ tryResolve();
807
+ };
808
+ const onMessage = (raw) => {
809
+ void (async () => {
810
+ const parsed = await safeParseJSON({
811
+ text: webSocketMessageToString(raw)
812
+ });
813
+ if (!parsed.success) return;
814
+ const value = parsed.value;
815
+ if (typeof value !== "object" || value == null || Array.isArray(value) || value.type !== "bridge-hello") {
816
+ return;
817
+ }
818
+ const capabilities = value.capabilities;
819
+ onHello(capabilities?.experimental_userMessageResponses === true);
820
+ receivedHello = true;
821
+ tryResolve();
822
+ })();
823
+ };
824
+ const onClose = () => settle(new Error("OpenCode bridge closed before sending bridge-hello."));
706
825
  const onError = (err) => {
707
- ws.off("open", onOpen);
708
- reject(err);
826
+ settle(err);
709
827
  };
710
- ws.once("open", onOpen);
711
- ws.once("error", onError);
828
+ ws.on("open", onOpen);
829
+ ws.on("message", onMessage);
830
+ ws.on("close", onClose);
831
+ ws.on("error", onError);
832
+ const helloTimer = setTimeout(
833
+ () => settle(
834
+ new Error(
835
+ `OpenCode bridge did not send bridge-hello within ${helloTimeoutMs}ms.`
836
+ )
837
+ ),
838
+ helloTimeoutMs
839
+ );
840
+ helloTimer.unref?.();
712
841
  });
713
842
  }
843
+ function webSocketMessageToString(raw) {
844
+ if (typeof raw === "string") return raw;
845
+ if (Buffer.isBuffer(raw)) return raw.toString("utf8");
846
+ if (Array.isArray(raw)) return Buffer.concat(raw).toString("utf8");
847
+ if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString("utf8");
848
+ if (ArrayBuffer.isView(raw)) {
849
+ return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString(
850
+ "utf8"
851
+ );
852
+ }
853
+ return String(raw);
854
+ }
714
855
  function withBridgeToken({
715
856
  endpoint,
716
857
  token
@@ -736,7 +877,8 @@ function createSession({
736
877
  sandboxId,
737
878
  debug,
738
879
  permissionMode,
739
- builtinToolFiltering
880
+ builtinToolFiltering,
881
+ supportsUserMessageResponses
740
882
  }) {
741
883
  let stopped = false;
742
884
  let stopPromise;
@@ -755,6 +897,11 @@ function createSession({
755
897
  pendingResolve = resolve;
756
898
  pendingReject = reject;
757
899
  });
900
+ const userMessageSubmitter = supportsUserMessageResponses() ? experimental_createBridgeUserMessageSubmitter({
901
+ send: (message) => channel.send(message),
902
+ onResponse: (listener) => channel.on("user-message-response", listener),
903
+ onReconnect: (listener) => channel.onReconnect(listener)
904
+ }) : void 0;
758
905
  const unsubs = [];
759
906
  const forward = (event) => {
760
907
  try {
@@ -783,6 +930,7 @@ function createSession({
783
930
  if (isSettled) return;
784
931
  isSettled = true;
785
932
  activeTurn = false;
933
+ userMessageSubmitter?.close();
786
934
  for (const u of unsubs) u();
787
935
  pendingResolve();
788
936
  };
@@ -790,6 +938,7 @@ function createSession({
790
938
  if (isSettled) return;
791
939
  isSettled = true;
792
940
  activeTurn = false;
941
+ userMessageSubmitter?.close(err);
793
942
  for (const u of unsubs) u();
794
943
  pendingReject(err);
795
944
  };
@@ -862,8 +1011,10 @@ function createSession({
862
1011
  reason: input.reason
863
1012
  });
864
1013
  },
865
- submitUserMessage: async (text) => {
866
- channel.send({ type: "user-message", text });
1014
+ ...userMessageSubmitter == null ? {} : {
1015
+ submitUserMessage: async (text) => {
1016
+ await userMessageSubmitter.submit(text);
1017
+ }
867
1018
  },
868
1019
  done
869
1020
  };
@@ -980,7 +1131,7 @@ function createSession({
980
1131
  port: bridgePort,
981
1132
  token: bridgeToken,
982
1133
  lastSeenEventId,
983
- sandboxId
1134
+ ...sandboxId == null ? {} : { sandboxId }
984
1135
  }
985
1136
  }
986
1137
  };
@@ -1094,7 +1245,7 @@ function createSession({
1094
1245
  port: bridgePort,
1095
1246
  token: bridgeToken,
1096
1247
  lastSeenEventId,
1097
- sandboxId
1248
+ ...sandboxId == null ? {} : { sandboxId }
1098
1249
  }
1099
1250
  }
1100
1251
  };