@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.
@@ -0,0 +1,58 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import type { HarnessV1Bootstrap } from '@ai-sdk/harness';
4
+
5
+ export const OPENCODE_BOOTSTRAP_DIR = '.harness-bootstrap/opencode';
6
+
7
+ let cachedBootstrap: HarnessV1Bootstrap | undefined;
8
+
9
+ export async function getOpenCodeBootstrap(): Promise<HarnessV1Bootstrap> {
10
+ if (cachedBootstrap != null) return cachedBootstrap;
11
+ const [pkg, lock, bridge, hostToolMcp] = await Promise.all([
12
+ readBridgeAsset('package.json'),
13
+ readBridgeAsset('pnpm-lock.yaml'),
14
+ readBridgeAsset('index.mjs'),
15
+ readBridgeAsset('host-tool-mcp.mjs'),
16
+ ]);
17
+ cachedBootstrap = {
18
+ harnessId: 'opencode',
19
+ bootstrapDir: OPENCODE_BOOTSTRAP_DIR,
20
+ files: [
21
+ { path: `${OPENCODE_BOOTSTRAP_DIR}/package.json`, content: pkg },
22
+ { path: `${OPENCODE_BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
23
+ { path: `${OPENCODE_BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
24
+ {
25
+ path: `${OPENCODE_BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
26
+ content: hostToolMcp,
27
+ },
28
+ ],
29
+ commands: [
30
+ {
31
+ command: 'pnpm install --frozen-lockfile --store-dir .pnpm-store',
32
+ },
33
+ {
34
+ command:
35
+ 'node node_modules/opencode-ai/postinstall.mjs && ./node_modules/.bin/opencode --version',
36
+ },
37
+ ],
38
+ };
39
+ return cachedBootstrap;
40
+ }
41
+
42
+ async function readBridgeAsset(name: string): Promise<string> {
43
+ const candidates = [
44
+ new URL(`./bridge/${name}`, import.meta.url),
45
+ new URL(`../bridge/${name}`, import.meta.url),
46
+ ];
47
+ let lastErr: unknown;
48
+ for (const url of candidates) {
49
+ try {
50
+ return await readFile(fileURLToPath(url), 'utf8');
51
+ } catch (err) {
52
+ const code = (err as NodeJS.ErrnoException).code;
53
+ if (code !== 'ENOENT') throw err;
54
+ lastErr = err;
55
+ }
56
+ }
57
+ throw lastErr ?? new Error(`bridge asset not found: ${name}`);
58
+ }
@@ -1,13 +1,10 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
3
2
  import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
3
  import {
6
4
  commonTool,
7
5
  HarnessCapabilityUnsupportedError,
8
6
  harnessV1DiagnosticFromBridgeFrame,
9
7
  type HarnessV1,
10
- type HarnessV1Bootstrap,
11
8
  type HarnessV1BuiltinTool,
12
9
  type HarnessV1BuiltinToolFiltering,
13
10
  type HarnessV1ContinueTurnState,
@@ -16,6 +13,7 @@ import {
16
13
  type HarnessV1PermissionMode,
17
14
  type HarnessV1Prompt,
18
15
  type HarnessV1PromptControl,
16
+ type HarnessV1PortEndpoint,
19
17
  type HarnessV1ResumeSessionState,
20
18
  type HarnessV1Session,
21
19
  type HarnessV1Skill,
@@ -28,9 +26,11 @@ import {
28
26
  drainBridgeProcessStream,
29
27
  forwardBridgeProcessStream,
30
28
  markBridgeStarting,
29
+ maskSandboxCredentials,
31
30
  resolveSandboxHomeDir,
32
31
  SandboxChannel,
33
32
  shellQuote,
33
+ warnCredentialBrokeringUnavailable,
34
34
  waitForBridgeReady,
35
35
  writeSkills as writeHarnessSkills,
36
36
  } from '@ai-sdk/harness/utils';
@@ -42,6 +42,13 @@ import {
42
42
  import { WebSocket } from 'ws';
43
43
  import { z } from 'zod/v4';
44
44
  import {
45
+ getOpenCodeBootstrap,
46
+ OPENCODE_BOOTSTRAP_DIR as BOOTSTRAP_DIR,
47
+ } from './opencode-bootstrap';
48
+ import {
49
+ createOpenCodeRequestTransformations,
50
+ OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
51
+ resolveOpenCodeAuthenticationMode,
45
52
  resolveOpenCodeEnv,
46
53
  splitOpenCodeModel,
47
54
  type OpenCodeAuthOptions,
@@ -192,8 +199,6 @@ const OPENCODE_BUILTIN_TOOLS = {
192
199
  }),
193
200
  } as const satisfies Record<string, HarnessV1BuiltinTool<any, any>>;
194
201
 
195
- const BOOTSTRAP_DIR = '.harness-bootstrap/opencode';
196
-
197
202
  const openCodeBridgeCoordsSchema = z.object({
198
203
  port: z.number(),
199
204
  token: z.string(),
@@ -219,48 +224,44 @@ export function createOpenCode(
219
224
  'OpenCode MCP server name "harness-tools" is reserved for HarnessAgent tools.',
220
225
  );
221
226
  }
222
- let cachedBootstrap: HarnessV1Bootstrap | undefined;
223
-
224
227
  return {
225
228
  specificationVersion: 'harness-v1',
226
229
  harnessId: 'opencode',
227
230
  builtinTools: OPENCODE_BUILTIN_TOOLS,
228
231
  supportsBuiltinToolApprovals: true,
229
232
  lifecycleStateSchema: openCodeResumeStateSchema,
230
- getBootstrap: async () => {
231
- if (cachedBootstrap != null) return cachedBootstrap;
232
- const [pkg, lock, bridge, hostToolMcp] = await Promise.all([
233
- readBridgeAsset('package.json'),
234
- readBridgeAsset('pnpm-lock.yaml'),
235
- readBridgeAsset('index.mjs'),
236
- readBridgeAsset('host-tool-mcp.mjs'),
237
- ]);
238
- cachedBootstrap = {
239
- harnessId: 'opencode',
240
- bootstrapDir: BOOTSTRAP_DIR,
241
- files: [
242
- { path: `${BOOTSTRAP_DIR}/package.json`, content: pkg },
243
- { path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
244
- { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
245
- {
246
- path: `${BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
247
- content: hostToolMcp,
248
- },
249
- ],
250
- commands: [
251
- {
252
- command: 'pnpm install --frozen-lockfile --store-dir .pnpm-store',
253
- },
254
- {
255
- command:
256
- 'node node_modules/opencode-ai/postinstall.mjs && ./node_modules/.bin/opencode --version',
257
- },
258
- ],
259
- };
260
- return cachedBootstrap;
261
- },
233
+ getBootstrap: getOpenCodeBootstrap,
262
234
  doStart: async startOpts => {
263
235
  const sandboxSession = startOpts.sandboxSession;
236
+ const authenticationMode = resolveOpenCodeAuthenticationMode({
237
+ auth: settings.auth,
238
+ model: settings.model,
239
+ provider: settings.provider,
240
+ });
241
+ const resolvedAuthEnvironment = resolveOpenCodeEnv({
242
+ auth: settings.auth,
243
+ model: settings.model,
244
+ provider: settings.provider,
245
+ });
246
+ let sandboxAuthEnvironment = resolvedAuthEnvironment;
247
+ if (sandboxSession.addRequestTransformations != null) {
248
+ const requestTransformations = createOpenCodeRequestTransformations(
249
+ resolvedAuthEnvironment,
250
+ authenticationMode,
251
+ );
252
+ if (requestTransformations.length > 0) {
253
+ await sandboxSession.addRequestTransformations(
254
+ requestTransformations,
255
+ );
256
+ }
257
+ sandboxAuthEnvironment = maskSandboxCredentials({
258
+ environment: resolvedAuthEnvironment,
259
+ credentialEnvironmentVariables:
260
+ OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
261
+ });
262
+ } else {
263
+ warnCredentialBrokeringUnavailable();
264
+ }
264
265
  const session = sandboxSession.restricted();
265
266
  const sandboxId = sandboxSession.id;
266
267
  const bootstrapDir = path.posix.resolve(
@@ -307,13 +308,16 @@ export function createOpenCode(
307
308
 
308
309
  if (coords) {
309
310
  try {
310
- const attachUrl =
311
- (await sandboxSession.getPortUrl({
312
- port: coords.port,
313
- protocol: 'ws',
314
- })) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
311
+ const endpoint = await sandboxSession.getPortEndpoint({
312
+ port: coords.port,
313
+ protocol: 'ws',
314
+ });
315
+ const attachEndpoint = withBridgeToken({
316
+ endpoint,
317
+ token: coords.token,
318
+ });
315
319
  const attachChannel: OpenCodeChannel = new SandboxChannel({
316
- connect: () => openWebSocket(attachUrl),
320
+ connect: () => openWebSocket(attachEndpoint),
317
321
  outboundSchema: outboundMessageSchema,
318
322
  initialLastSeenEventId: coords.lastSeenEventId,
319
323
  onDiagnostic,
@@ -380,11 +384,7 @@ export function createOpenCode(
380
384
  })
381
385
  : undefined;
382
386
  const env = {
383
- ...resolveOpenCodeEnv({
384
- auth: settings.auth,
385
- model: settings.model,
386
- provider: settings.provider,
387
- }),
387
+ ...sandboxAuthEnvironment,
388
388
  AI_SDK_HARNESS_CLIENT_APP: OPENCODE_CLIENT_APP,
389
389
  BRIDGE_CHANNEL_TOKEN: token,
390
390
  BRIDGE_WS_PORT: String(port),
@@ -452,14 +452,14 @@ export function createOpenCode(
452
452
  });
453
453
  void drainBridgeProcessStream(proc.stdout);
454
454
 
455
- const wsUrl =
456
- (await sandboxSession.getPortUrl({
457
- port: boundPort,
458
- protocol: 'ws',
459
- })) + `?agent_bridge_token=${encodeURIComponent(token)}`;
455
+ const endpoint = await sandboxSession.getPortEndpoint({
456
+ port: boundPort,
457
+ protocol: 'ws',
458
+ });
459
+ const bridgeEndpoint = withBridgeToken({ endpoint, token });
460
460
 
461
461
  const channel: OpenCodeChannel = new SandboxChannel({
462
- connect: () => openWebSocket(wsUrl),
462
+ connect: () => openWebSocket(bridgeEndpoint),
463
463
  outboundSchema: outboundMessageSchema,
464
464
  onDiagnostic,
465
465
  onBridgeError,
@@ -508,24 +508,6 @@ function resolveBridgePort(
508
508
  });
509
509
  }
510
510
 
511
- async function readBridgeAsset(name: string): Promise<string> {
512
- const candidates = [
513
- new URL(`./bridge/${name}`, import.meta.url),
514
- new URL(`../bridge/${name}`, import.meta.url),
515
- ];
516
- let lastErr: unknown;
517
- for (const url of candidates) {
518
- try {
519
- return await readFile(fileURLToPath(url), 'utf8');
520
- } catch (err) {
521
- const code = (err as NodeJS.ErrnoException).code;
522
- if (code !== 'ENOENT') throw err;
523
- lastErr = err;
524
- }
525
- }
526
- throw lastErr ?? new Error(`bridge asset not found: ${name}`);
527
- }
528
-
529
511
  async function writeOpenCodeSkills({
530
512
  sandbox,
531
513
  skills,
@@ -552,9 +534,14 @@ async function writeOpenCodeSkills({
552
534
  return { skillsDir };
553
535
  }
554
536
 
555
- function openWebSocket(url: string): Promise<WebSocket> {
537
+ function openWebSocket({
538
+ url,
539
+ headers,
540
+ }: HarnessV1PortEndpoint): Promise<WebSocket> {
556
541
  return new Promise((resolve, reject) => {
557
- const ws = new WebSocket(url);
542
+ const ws = new WebSocket(url, {
543
+ headers: headers == null ? undefined : { ...headers },
544
+ });
558
545
  const onOpen = () => {
559
546
  ws.off('error', onError);
560
547
  resolve(ws);
@@ -568,6 +555,18 @@ function openWebSocket(url: string): Promise<WebSocket> {
568
555
  });
569
556
  }
570
557
 
558
+ function withBridgeToken({
559
+ endpoint,
560
+ token,
561
+ }: {
562
+ endpoint: HarnessV1PortEndpoint;
563
+ token: string;
564
+ }): HarnessV1PortEndpoint {
565
+ const bridgeUrl = new URL(endpoint.url);
566
+ bridgeUrl.searchParams.set('agent_bridge_token', token);
567
+ return { ...endpoint, url: bridgeUrl.toString() };
568
+ }
569
+
571
570
  function createSession({
572
571
  sessionId,
573
572
  channel,