@ai-sdk/harness-opencode 1.0.71 → 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,12 @@ 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 OpenCodeAuthOptions = {
5
+ type OpenCodeResolvedAuthenticationMode = 'anthropic' | 'openai' | 'ai-gateway';
6
+ type OpenCodeAuthenticationMode = OpenCodeResolvedAuthenticationMode | 'auto';
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.
9
+ */
10
+ type LegacyOpenCodeAuthOptions = {
6
11
  readonly gateway?: {
7
12
  readonly apiKey?: string;
8
13
  readonly baseUrl?: string;
@@ -25,6 +30,7 @@ type OpenCodeAuthOptions = {
25
30
  readonly queryParams?: Record<string, string>;
26
31
  };
27
32
  };
33
+ type OpenCodeAuthOptions = OpenCodeAuthenticationMode | LegacyOpenCodeAuthOptions;
28
34
 
29
35
  type OpenCodeHarnessSettings = {
30
36
  readonly auth?: OpenCodeAuthOptions;
@@ -184,4 +190,4 @@ declare const openCode: _ai_sdk_harness.HarnessV1<{
184
190
  }, any, any>;
185
191
  }>;
186
192
 
187
- export { type OpenCodeAuthOptions, type OpenCodeHarnessSettings, VERSION, createOpenCode, openCode };
193
+ export { type OpenCodeAuthOptions, type OpenCodeAuthenticationMode, type OpenCodeHarnessSettings, VERSION, createOpenCode, openCode };
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
@@ -66,26 +164,75 @@ function resolveOpenCodeEnv({
66
164
  provider,
67
165
  processEnv = process.env
68
166
  }) {
167
+ const normalizedAuth = normalizeOpenCodeAuthToLegacyAuth(auth);
69
168
  const selectedProvider = resolveOpenCodeProvider({ model, provider });
70
- if (auth?.openaiCompatible) {
71
- return pickOpenAICompatible(auth.openaiCompatible, processEnv);
169
+ if (normalizedAuth?.openaiCompatible) {
170
+ return pickOpenAICompatible(normalizedAuth.openaiCompatible, processEnv);
72
171
  }
73
172
  if (selectedProvider === "openai") {
74
- if (auth?.openai) {
75
- return pickOpenAI({ explicit: auth.openai, processEnv });
173
+ if (normalizedAuth?.openai) {
174
+ return pickOpenAI({ explicit: normalizedAuth.openai, processEnv });
76
175
  }
77
- } else if (auth?.anthropic) {
78
- return pickAnthropic({ explicit: auth.anthropic, processEnv });
176
+ } else if (normalizedAuth?.anthropic) {
177
+ return pickAnthropic({ explicit: normalizedAuth.anthropic, processEnv });
79
178
  }
80
179
  const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env: processEnv });
81
- if (auth?.gateway) {
82
- return pickGateway({ explicit: auth.gateway, gatewayAuthFromEnv });
180
+ if (normalizedAuth?.gateway) {
181
+ return pickGateway({
182
+ explicit: normalizedAuth.gateway,
183
+ gatewayAuthFromEnv
184
+ });
83
185
  }
84
186
  if (gatewayAuthFromEnv.apiKey) {
85
187
  return pickGateway({ explicit: {}, gatewayAuthFromEnv });
86
188
  }
87
189
  return selectedProvider === "openai" ? pickOpenAI({ processEnv }) : pickAnthropic({ processEnv });
88
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
+ }
215
+ function normalizeOpenCodeAuthToLegacyAuth(auth) {
216
+ if (auth == null || auth === "auto") {
217
+ return void 0;
218
+ }
219
+ if (typeof auth === "string") {
220
+ switch (auth) {
221
+ case "anthropic":
222
+ return { anthropic: {} };
223
+ case "openai":
224
+ return { openai: {} };
225
+ case "ai-gateway":
226
+ return { gateway: {} };
227
+ default:
228
+ return void 0;
229
+ }
230
+ }
231
+ console.warn(
232
+ '[opencode] Passing an object to auth options is deprecated. Use a string mode ("auto" | "anthropic" | "openai" | "ai-gateway") instead, and pass credentials via environment variables.'
233
+ );
234
+ return auth;
235
+ }
89
236
  function pickOpenAICompatible(explicit, processEnv) {
90
237
  const env = {};
91
238
  const apiKey = explicit.apiKey ?? processEnv.OPENAI_API_KEY;
@@ -136,9 +283,10 @@ function pickGateway({
136
283
  const env = {};
137
284
  const apiKey = explicit.apiKey ?? gatewayAuthFromEnv.apiKey;
138
285
  if (apiKey) env.AI_GATEWAY_API_KEY = apiKey;
139
- env.AI_GATEWAY_BASE_URL = toOpenCodeGatewayBaseUrl(
286
+ const baseUrl = toOpenCodeGatewayBaseUrl(
140
287
  explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl
141
288
  );
289
+ env.AI_GATEWAY_BASE_URL = baseUrl;
142
290
  return env;
143
291
  }
144
292
  function toOpenCodeGatewayBaseUrl(baseUrl) {
@@ -169,7 +317,7 @@ var inboundMessageSchema = z.discriminatedUnion("type", [
169
317
  ]);
170
318
 
171
319
  // src/version.ts
172
- var VERSION = true ? "1.0.71" : "0.0.0-test";
320
+ var VERSION = true ? "1.0.73" : "0.0.0-test";
173
321
 
174
322
  // src/opencode-harness.ts
175
323
  var OPENCODE_CLIENT_APP = `ai-sdk/harness-opencode/${VERSION}`;
@@ -272,7 +420,6 @@ var OPENCODE_BUILTIN_TOOLS = {
272
420
  })
273
421
  })
274
422
  };
275
- var BOOTSTRAP_DIR = ".harness-bootstrap/opencode";
276
423
  var openCodeBridgeCoordsSchema = z2.object({
277
424
  port: z2.number(),
278
425
  token: z2.string(),
@@ -289,51 +436,48 @@ function createOpenCode(settings = {}) {
289
436
  'OpenCode MCP server name "harness-tools" is reserved for HarnessAgent tools.'
290
437
  );
291
438
  }
292
- let cachedBootstrap;
293
439
  return {
294
440
  specificationVersion: "harness-v1",
295
441
  harnessId: "opencode",
296
442
  builtinTools: OPENCODE_BUILTIN_TOOLS,
297
443
  supportsBuiltinToolApprovals: true,
298
444
  lifecycleStateSchema: openCodeResumeStateSchema,
299
- getBootstrap: async () => {
300
- if (cachedBootstrap != null) return cachedBootstrap;
301
- const [pkg, lock, bridge, hostToolMcp] = await Promise.all([
302
- readBridgeAsset("package.json"),
303
- readBridgeAsset("pnpm-lock.yaml"),
304
- readBridgeAsset("index.mjs"),
305
- readBridgeAsset("host-tool-mcp.mjs")
306
- ]);
307
- cachedBootstrap = {
308
- harnessId: "opencode",
309
- bootstrapDir: BOOTSTRAP_DIR,
310
- files: [
311
- { path: `${BOOTSTRAP_DIR}/package.json`, content: pkg },
312
- { path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
313
- { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
314
- {
315
- path: `${BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
316
- content: hostToolMcp
317
- }
318
- ],
319
- commands: [
320
- {
321
- command: "pnpm install --frozen-lockfile --store-dir .pnpm-store"
322
- },
323
- {
324
- command: "node node_modules/opencode-ai/postinstall.mjs && ./node_modules/.bin/opencode --version"
325
- }
326
- ]
327
- };
328
- return cachedBootstrap;
329
- },
445
+ getBootstrap: getOpenCodeBootstrap,
330
446
  doStart: async (startOpts) => {
331
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
+ }
332
476
  const session = sandboxSession.restricted();
333
477
  const sandboxId = sandboxSession.id;
334
478
  const bootstrapDir = path.posix.resolve(
335
479
  sandboxSession.defaultWorkingDirectory,
336
- BOOTSTRAP_DIR
480
+ OPENCODE_BOOTSTRAP_DIR
337
481
  );
338
482
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
339
483
  const isResume = lifecycleState != null;
@@ -359,12 +503,16 @@ function createOpenCode(settings = {}) {
359
503
  });
360
504
  if (coords) {
361
505
  try {
362
- const attachUrl = await sandboxSession.getPortUrl({
506
+ const endpoint2 = await sandboxSession.getPortEndpoint({
363
507
  port: coords.port,
364
508
  protocol: "ws"
365
- }) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
509
+ });
510
+ const attachEndpoint = withBridgeToken({
511
+ endpoint: endpoint2,
512
+ token: coords.token
513
+ });
366
514
  const attachChannel = new SandboxChannel({
367
- connect: () => openWebSocket(attachUrl),
515
+ connect: () => openWebSocket(attachEndpoint),
368
516
  outboundSchema: outboundMessageSchema,
369
517
  initialLastSeenEventId: coords.lastSeenEventId,
370
518
  onDiagnostic,
@@ -422,11 +570,7 @@ function createOpenCode(settings = {}) {
422
570
  abortSignal: startOpts.abortSignal
423
571
  }) : void 0;
424
572
  const env = {
425
- ...resolveOpenCodeEnv({
426
- auth: settings.auth,
427
- model: settings.model,
428
- provider: settings.provider
429
- }),
573
+ ...sandboxAuthEnvironment,
430
574
  AI_SDK_HARNESS_CLIENT_APP: OPENCODE_CLIENT_APP,
431
575
  BRIDGE_CHANNEL_TOKEN: token,
432
576
  BRIDGE_WS_PORT: String(port),
@@ -485,12 +629,13 @@ function createOpenCode(settings = {}) {
485
629
  })
486
630
  });
487
631
  void drainBridgeProcessStream(proc.stdout);
488
- const wsUrl = await sandboxSession.getPortUrl({
632
+ const endpoint = await sandboxSession.getPortEndpoint({
489
633
  port: boundPort,
490
634
  protocol: "ws"
491
- }) + `?agent_bridge_token=${encodeURIComponent(token)}`;
635
+ });
636
+ const bridgeEndpoint = withBridgeToken({ endpoint, token });
492
637
  const channel = new SandboxChannel({
493
- connect: () => openWebSocket(wsUrl),
638
+ connect: () => openWebSocket(bridgeEndpoint),
494
639
  outboundSchema: outboundMessageSchema,
495
640
  onDiagnostic,
496
641
  onBridgeError,
@@ -529,23 +674,6 @@ function resolveBridgePort(sandboxSession, override) {
529
674
  message: "The OpenCode harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createOpenCode({ port })`."
530
675
  });
531
676
  }
532
- async function readBridgeAsset(name) {
533
- const candidates = [
534
- new URL(`./bridge/${name}`, import.meta.url),
535
- new URL(`../bridge/${name}`, import.meta.url)
536
- ];
537
- let lastErr;
538
- for (const url of candidates) {
539
- try {
540
- return await readFile(fileURLToPath(url), "utf8");
541
- } catch (err) {
542
- const code = err.code;
543
- if (code !== "ENOENT") throw err;
544
- lastErr = err;
545
- }
546
- }
547
- throw lastErr ?? new Error(`bridge asset not found: ${name}`);
548
- }
549
677
  async function writeOpenCodeSkills({
550
678
  sandbox,
551
679
  skills,
@@ -563,9 +691,14 @@ async function writeOpenCodeSkills({
563
691
  });
564
692
  return { skillsDir };
565
693
  }
566
- function openWebSocket(url) {
694
+ function openWebSocket({
695
+ url,
696
+ headers
697
+ }) {
567
698
  return new Promise((resolve, reject) => {
568
- const ws = new WebSocket(url);
699
+ const ws = new WebSocket(url, {
700
+ headers: headers == null ? void 0 : { ...headers }
701
+ });
569
702
  const onOpen = () => {
570
703
  ws.off("error", onError);
571
704
  resolve(ws);
@@ -578,6 +711,14 @@ function openWebSocket(url) {
578
711
  ws.once("error", onError);
579
712
  });
580
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
+ }
581
722
  function createSession({
582
723
  sessionId,
583
724
  channel,