@ai-sdk/harness-opencode 1.0.72 → 1.0.74
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/CHANGELOG.md +22 -0
- package/dist/bridge/index.mjs +68 -28
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +199 -69
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +3 -0
- package/src/bridge/index.ts +47 -7
- package/src/bridge/opencode-server-auth.ts +15 -0
- package/src/opencode-auth.ts +101 -4
- package/src/opencode-bootstrap.ts +58 -0
- package/src/opencode-harness.ts +101 -76
|
@@ -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
|
+
}
|
package/src/opencode-harness.ts
CHANGED
|
@@ -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:
|
|
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
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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(
|
|
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
|
-
...
|
|
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
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
@@ -769,6 +768,16 @@ function createSession({
|
|
|
769
768
|
isResume,
|
|
770
769
|
modelId: model,
|
|
771
770
|
doPromptTurn: async promptOpts => {
|
|
771
|
+
if (
|
|
772
|
+
promptOpts.responseFormat?.type === 'json' &&
|
|
773
|
+
promptOpts.responseFormat.schema == null
|
|
774
|
+
) {
|
|
775
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
776
|
+
message:
|
|
777
|
+
"Harness 'opencode' requires a JSON schema for structured output.",
|
|
778
|
+
harnessId: 'opencode',
|
|
779
|
+
});
|
|
780
|
+
}
|
|
772
781
|
const control = wireTurn({
|
|
773
782
|
emit: promptOpts.emit,
|
|
774
783
|
abortSignal: promptOpts.abortSignal,
|
|
@@ -782,6 +791,9 @@ function createSession({
|
|
|
782
791
|
description: t.description,
|
|
783
792
|
inputSchema: t.inputSchema,
|
|
784
793
|
})),
|
|
794
|
+
...(promptOpts.responseFormat == null
|
|
795
|
+
? {}
|
|
796
|
+
: { responseFormat: promptOpts.responseFormat }),
|
|
785
797
|
...(promptOpts.instructions
|
|
786
798
|
? { instructions: promptOpts.instructions }
|
|
787
799
|
: {}),
|
|
@@ -791,6 +803,16 @@ function createSession({
|
|
|
791
803
|
return control;
|
|
792
804
|
},
|
|
793
805
|
doContinueTurn: async continueOpts => {
|
|
806
|
+
if (
|
|
807
|
+
continueOpts.responseFormat?.type === 'json' &&
|
|
808
|
+
continueOpts.responseFormat.schema == null
|
|
809
|
+
) {
|
|
810
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
811
|
+
message:
|
|
812
|
+
"Harness 'opencode' requires a JSON schema for structured output.",
|
|
813
|
+
harnessId: 'opencode',
|
|
814
|
+
});
|
|
815
|
+
}
|
|
794
816
|
const control = wireTurn({
|
|
795
817
|
emit: continueOpts.emit,
|
|
796
818
|
abortSignal: continueOpts.abortSignal,
|
|
@@ -805,6 +827,9 @@ function createSession({
|
|
|
805
827
|
description: t.description,
|
|
806
828
|
inputSchema: t.inputSchema,
|
|
807
829
|
})),
|
|
830
|
+
...(continueOpts.responseFormat == null
|
|
831
|
+
? {}
|
|
832
|
+
: { responseFormat: continueOpts.responseFormat }),
|
|
808
833
|
...(continueOpts.instructions
|
|
809
834
|
? { instructions: continueOpts.instructions }
|
|
810
835
|
: {}),
|