@ai-sdk/harness-opencode 1.0.72 → 1.0.73

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
@@ -2,7 +2,8 @@ import * as _ai_sdk_provider_utils from '@ai-sdk/provider-utils';
2
2
  import * as _ai_sdk_harness from '@ai-sdk/harness';
3
3
  import { HarnessV1, HarnessV1BuiltinTool } from '@ai-sdk/harness';
4
4
 
5
- type OpenCodeAuthenticationMode = 'auto' | 'anthropic' | 'openai' | 'ai-gateway';
5
+ type OpenCodeResolvedAuthenticationMode = 'anthropic' | 'openai' | 'ai-gateway';
6
+ type OpenCodeAuthenticationMode = OpenCodeResolvedAuthenticationMode | 'auto';
6
7
  /**
7
8
  * @deprecated Passing an object to auth options is deprecated. Use an `OpenCodeAuthenticationMode` string value ("auto" | "anthropic" | "openai" | "ai-gateway") instead, and pass credentials via environment variables.
8
9
  */
package/dist/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  // src/opencode-harness.ts
2
2
  import { randomBytes } from "crypto";
3
- import { readFile } from "fs/promises";
4
3
  import path from "path";
5
- import { fileURLToPath } from "url";
6
4
  import {
7
5
  commonTool,
8
6
  HarnessCapabilityUnsupportedError,
@@ -15,9 +13,11 @@ import {
15
13
  drainBridgeProcessStream,
16
14
  forwardBridgeProcessStream,
17
15
  markBridgeStarting,
16
+ maskSandboxCredentials,
18
17
  resolveSandboxHomeDir,
19
18
  SandboxChannel,
20
19
  shellQuote,
20
+ warnCredentialBrokeringUnavailable,
21
21
  waitForBridgeReady,
22
22
  writeSkills as writeHarnessSkills
23
23
  } from "@ai-sdk/harness/utils";
@@ -27,8 +27,106 @@ import {
27
27
  import { WebSocket } from "ws";
28
28
  import { z as z2 } from "zod/v4";
29
29
 
30
+ // src/opencode-bootstrap.ts
31
+ import { readFile } from "fs/promises";
32
+ import { fileURLToPath } from "url";
33
+ var OPENCODE_BOOTSTRAP_DIR = ".harness-bootstrap/opencode";
34
+ var cachedBootstrap;
35
+ async function getOpenCodeBootstrap() {
36
+ if (cachedBootstrap != null) return cachedBootstrap;
37
+ const [pkg, lock, bridge, hostToolMcp] = await Promise.all([
38
+ readBridgeAsset("package.json"),
39
+ readBridgeAsset("pnpm-lock.yaml"),
40
+ readBridgeAsset("index.mjs"),
41
+ readBridgeAsset("host-tool-mcp.mjs")
42
+ ]);
43
+ cachedBootstrap = {
44
+ harnessId: "opencode",
45
+ bootstrapDir: OPENCODE_BOOTSTRAP_DIR,
46
+ files: [
47
+ { path: `${OPENCODE_BOOTSTRAP_DIR}/package.json`, content: pkg },
48
+ { path: `${OPENCODE_BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
49
+ { path: `${OPENCODE_BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
50
+ {
51
+ path: `${OPENCODE_BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
52
+ content: hostToolMcp
53
+ }
54
+ ],
55
+ commands: [
56
+ {
57
+ command: "pnpm install --frozen-lockfile --store-dir .pnpm-store"
58
+ },
59
+ {
60
+ command: "node node_modules/opencode-ai/postinstall.mjs && ./node_modules/.bin/opencode --version"
61
+ }
62
+ ]
63
+ };
64
+ return cachedBootstrap;
65
+ }
66
+ async function readBridgeAsset(name) {
67
+ const candidates = [
68
+ new URL(`./bridge/${name}`, import.meta.url),
69
+ new URL(`../bridge/${name}`, import.meta.url)
70
+ ];
71
+ let lastErr;
72
+ for (const url of candidates) {
73
+ try {
74
+ return await readFile(fileURLToPath(url), "utf8");
75
+ } catch (err) {
76
+ const code = err.code;
77
+ if (code !== "ENOENT") throw err;
78
+ lastErr = err;
79
+ }
80
+ }
81
+ throw lastErr ?? new Error(`bridge asset not found: ${name}`);
82
+ }
83
+
30
84
  // src/opencode-auth.ts
31
- import { getAiGatewayAuthFromEnv } from "@ai-sdk/harness/utils";
85
+ import {
86
+ createCredentialRequestTransformation,
87
+ getAiGatewayAuthFromEnv
88
+ } from "@ai-sdk/harness/utils";
89
+ var OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES = [
90
+ "AI_GATEWAY_API_KEY",
91
+ "OPENAI_API_KEY",
92
+ "ANTHROPIC_API_KEY",
93
+ "ANTHROPIC_AUTH_TOKEN"
94
+ ];
95
+ function createOpenCodeRequestTransformations(env, auth) {
96
+ switch (auth) {
97
+ case "ai-gateway":
98
+ return env.AI_GATEWAY_API_KEY ? [
99
+ createCredentialRequestTransformation({
100
+ baseUrl: env.AI_GATEWAY_BASE_URL,
101
+ headers: {
102
+ Authorization: `Bearer ${env.AI_GATEWAY_API_KEY}`
103
+ }
104
+ })
105
+ ] : [];
106
+ case "openai":
107
+ return env.OPENAI_API_KEY ? [
108
+ createCredentialRequestTransformation({
109
+ baseUrl: env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
110
+ headers: { Authorization: `Bearer ${env.OPENAI_API_KEY}` }
111
+ })
112
+ ] : [];
113
+ case "anthropic": {
114
+ const headers = {};
115
+ if (env.ANTHROPIC_API_KEY) {
116
+ headers["x-api-key"] = env.ANTHROPIC_API_KEY;
117
+ }
118
+ if (env.ANTHROPIC_AUTH_TOKEN) {
119
+ headers.Authorization = `Bearer ${env.ANTHROPIC_AUTH_TOKEN}`;
120
+ }
121
+ return Object.keys(headers).length === 0 ? [] : [
122
+ createCredentialRequestTransformation({
123
+ baseUrl: env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com",
124
+ headers
125
+ })
126
+ ];
127
+ }
128
+ }
129
+ }
32
130
  function resolveOpenCodeProvider({
33
131
  model,
34
132
  provider
@@ -90,6 +188,30 @@ function resolveOpenCodeEnv({
90
188
  }
91
189
  return selectedProvider === "openai" ? pickOpenAI({ processEnv }) : pickAnthropic({ processEnv });
92
190
  }
191
+ function resolveOpenCodeAuthenticationMode({
192
+ auth,
193
+ model,
194
+ provider,
195
+ processEnv = process.env
196
+ }) {
197
+ if (typeof auth !== "string" && auth?.openaiCompatible) {
198
+ return "openai";
199
+ }
200
+ const selectedProvider = resolveOpenCodeProvider({ model, provider });
201
+ if (selectedProvider === "openai" && (auth === "openai" || typeof auth !== "string" && auth?.openai)) {
202
+ return "openai";
203
+ }
204
+ if (selectedProvider === "anthropic" && (auth === "anthropic" || typeof auth !== "string" && auth?.anthropic)) {
205
+ return "anthropic";
206
+ }
207
+ if (auth === "ai-gateway" || typeof auth !== "string" && auth?.gateway) {
208
+ return "ai-gateway";
209
+ }
210
+ if (getAiGatewayAuthFromEnv({ env: processEnv }).apiKey) {
211
+ return "ai-gateway";
212
+ }
213
+ return selectedProvider;
214
+ }
93
215
  function normalizeOpenCodeAuthToLegacyAuth(auth) {
94
216
  if (auth == null || auth === "auto") {
95
217
  return void 0;
@@ -161,9 +283,10 @@ function pickGateway({
161
283
  const env = {};
162
284
  const apiKey = explicit.apiKey ?? gatewayAuthFromEnv.apiKey;
163
285
  if (apiKey) env.AI_GATEWAY_API_KEY = apiKey;
164
- env.AI_GATEWAY_BASE_URL = toOpenCodeGatewayBaseUrl(
286
+ const baseUrl = toOpenCodeGatewayBaseUrl(
165
287
  explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl
166
288
  );
289
+ env.AI_GATEWAY_BASE_URL = baseUrl;
167
290
  return env;
168
291
  }
169
292
  function toOpenCodeGatewayBaseUrl(baseUrl) {
@@ -194,7 +317,7 @@ var inboundMessageSchema = z.discriminatedUnion("type", [
194
317
  ]);
195
318
 
196
319
  // src/version.ts
197
- var VERSION = true ? "1.0.72" : "0.0.0-test";
320
+ var VERSION = true ? "1.0.73" : "0.0.0-test";
198
321
 
199
322
  // src/opencode-harness.ts
200
323
  var OPENCODE_CLIENT_APP = `ai-sdk/harness-opencode/${VERSION}`;
@@ -297,7 +420,6 @@ var OPENCODE_BUILTIN_TOOLS = {
297
420
  })
298
421
  })
299
422
  };
300
- var BOOTSTRAP_DIR = ".harness-bootstrap/opencode";
301
423
  var openCodeBridgeCoordsSchema = z2.object({
302
424
  port: z2.number(),
303
425
  token: z2.string(),
@@ -314,51 +436,48 @@ function createOpenCode(settings = {}) {
314
436
  'OpenCode MCP server name "harness-tools" is reserved for HarnessAgent tools.'
315
437
  );
316
438
  }
317
- let cachedBootstrap;
318
439
  return {
319
440
  specificationVersion: "harness-v1",
320
441
  harnessId: "opencode",
321
442
  builtinTools: OPENCODE_BUILTIN_TOOLS,
322
443
  supportsBuiltinToolApprovals: true,
323
444
  lifecycleStateSchema: openCodeResumeStateSchema,
324
- getBootstrap: async () => {
325
- if (cachedBootstrap != null) return cachedBootstrap;
326
- const [pkg, lock, bridge, hostToolMcp] = await Promise.all([
327
- readBridgeAsset("package.json"),
328
- readBridgeAsset("pnpm-lock.yaml"),
329
- readBridgeAsset("index.mjs"),
330
- readBridgeAsset("host-tool-mcp.mjs")
331
- ]);
332
- cachedBootstrap = {
333
- harnessId: "opencode",
334
- bootstrapDir: BOOTSTRAP_DIR,
335
- files: [
336
- { path: `${BOOTSTRAP_DIR}/package.json`, content: pkg },
337
- { path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
338
- { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
339
- {
340
- path: `${BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
341
- content: hostToolMcp
342
- }
343
- ],
344
- commands: [
345
- {
346
- command: "pnpm install --frozen-lockfile --store-dir .pnpm-store"
347
- },
348
- {
349
- command: "node node_modules/opencode-ai/postinstall.mjs && ./node_modules/.bin/opencode --version"
350
- }
351
- ]
352
- };
353
- return cachedBootstrap;
354
- },
445
+ getBootstrap: getOpenCodeBootstrap,
355
446
  doStart: async (startOpts) => {
356
447
  const sandboxSession = startOpts.sandboxSession;
448
+ const authenticationMode = resolveOpenCodeAuthenticationMode({
449
+ auth: settings.auth,
450
+ model: settings.model,
451
+ provider: settings.provider
452
+ });
453
+ const resolvedAuthEnvironment = resolveOpenCodeEnv({
454
+ auth: settings.auth,
455
+ model: settings.model,
456
+ provider: settings.provider
457
+ });
458
+ let sandboxAuthEnvironment = resolvedAuthEnvironment;
459
+ if (sandboxSession.addRequestTransformations != null) {
460
+ const requestTransformations = createOpenCodeRequestTransformations(
461
+ resolvedAuthEnvironment,
462
+ authenticationMode
463
+ );
464
+ if (requestTransformations.length > 0) {
465
+ await sandboxSession.addRequestTransformations(
466
+ requestTransformations
467
+ );
468
+ }
469
+ sandboxAuthEnvironment = maskSandboxCredentials({
470
+ environment: resolvedAuthEnvironment,
471
+ credentialEnvironmentVariables: OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES
472
+ });
473
+ } else {
474
+ warnCredentialBrokeringUnavailable();
475
+ }
357
476
  const session = sandboxSession.restricted();
358
477
  const sandboxId = sandboxSession.id;
359
478
  const bootstrapDir = path.posix.resolve(
360
479
  sandboxSession.defaultWorkingDirectory,
361
- BOOTSTRAP_DIR
480
+ OPENCODE_BOOTSTRAP_DIR
362
481
  );
363
482
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
364
483
  const isResume = lifecycleState != null;
@@ -384,12 +503,16 @@ function createOpenCode(settings = {}) {
384
503
  });
385
504
  if (coords) {
386
505
  try {
387
- const attachUrl = await sandboxSession.getPortUrl({
506
+ const endpoint2 = await sandboxSession.getPortEndpoint({
388
507
  port: coords.port,
389
508
  protocol: "ws"
390
- }) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
509
+ });
510
+ const attachEndpoint = withBridgeToken({
511
+ endpoint: endpoint2,
512
+ token: coords.token
513
+ });
391
514
  const attachChannel = new SandboxChannel({
392
- connect: () => openWebSocket(attachUrl),
515
+ connect: () => openWebSocket(attachEndpoint),
393
516
  outboundSchema: outboundMessageSchema,
394
517
  initialLastSeenEventId: coords.lastSeenEventId,
395
518
  onDiagnostic,
@@ -447,11 +570,7 @@ function createOpenCode(settings = {}) {
447
570
  abortSignal: startOpts.abortSignal
448
571
  }) : void 0;
449
572
  const env = {
450
- ...resolveOpenCodeEnv({
451
- auth: settings.auth,
452
- model: settings.model,
453
- provider: settings.provider
454
- }),
573
+ ...sandboxAuthEnvironment,
455
574
  AI_SDK_HARNESS_CLIENT_APP: OPENCODE_CLIENT_APP,
456
575
  BRIDGE_CHANNEL_TOKEN: token,
457
576
  BRIDGE_WS_PORT: String(port),
@@ -510,12 +629,13 @@ function createOpenCode(settings = {}) {
510
629
  })
511
630
  });
512
631
  void drainBridgeProcessStream(proc.stdout);
513
- const wsUrl = await sandboxSession.getPortUrl({
632
+ const endpoint = await sandboxSession.getPortEndpoint({
514
633
  port: boundPort,
515
634
  protocol: "ws"
516
- }) + `?agent_bridge_token=${encodeURIComponent(token)}`;
635
+ });
636
+ const bridgeEndpoint = withBridgeToken({ endpoint, token });
517
637
  const channel = new SandboxChannel({
518
- connect: () => openWebSocket(wsUrl),
638
+ connect: () => openWebSocket(bridgeEndpoint),
519
639
  outboundSchema: outboundMessageSchema,
520
640
  onDiagnostic,
521
641
  onBridgeError,
@@ -554,23 +674,6 @@ function resolveBridgePort(sandboxSession, override) {
554
674
  message: "The OpenCode harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createOpenCode({ port })`."
555
675
  });
556
676
  }
557
- async function readBridgeAsset(name) {
558
- const candidates = [
559
- new URL(`./bridge/${name}`, import.meta.url),
560
- new URL(`../bridge/${name}`, import.meta.url)
561
- ];
562
- let lastErr;
563
- for (const url of candidates) {
564
- try {
565
- return await readFile(fileURLToPath(url), "utf8");
566
- } catch (err) {
567
- const code = err.code;
568
- if (code !== "ENOENT") throw err;
569
- lastErr = err;
570
- }
571
- }
572
- throw lastErr ?? new Error(`bridge asset not found: ${name}`);
573
- }
574
677
  async function writeOpenCodeSkills({
575
678
  sandbox,
576
679
  skills,
@@ -588,9 +691,14 @@ async function writeOpenCodeSkills({
588
691
  });
589
692
  return { skillsDir };
590
693
  }
591
- function openWebSocket(url) {
694
+ function openWebSocket({
695
+ url,
696
+ headers
697
+ }) {
592
698
  return new Promise((resolve, reject) => {
593
- const ws = new WebSocket(url);
699
+ const ws = new WebSocket(url, {
700
+ headers: headers == null ? void 0 : { ...headers }
701
+ });
594
702
  const onOpen = () => {
595
703
  ws.off("error", onError);
596
704
  resolve(ws);
@@ -603,6 +711,14 @@ function openWebSocket(url) {
603
711
  ws.once("error", onError);
604
712
  });
605
713
  }
714
+ function withBridgeToken({
715
+ endpoint,
716
+ token
717
+ }) {
718
+ const bridgeUrl = new URL(endpoint.url);
719
+ bridgeUrl.searchParams.set("agent_bridge_token", token);
720
+ return { ...endpoint, url: bridgeUrl.toString() };
721
+ }
606
722
  function createSession({
607
723
  sessionId,
608
724
  channel,