@ai-sdk/harness-opencode 1.0.134 → 1.0.136

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.js CHANGED
@@ -1,2064 +1,1629 @@
1
- // src/opencode-harness.ts
2
- import path from "path";
3
- import {
4
- commonTool,
5
- HARNESS_V1_BUILTIN_TOOLS,
6
- HarnessCapabilityUnsupportedError as HarnessCapabilityUnsupportedError2,
7
- harnessV1DiagnosticFromBridgeFrame,
8
- harnessStateDirectoryPath,
9
- harnessSessionDataDirectoryPath
10
- } from "@ai-sdk/harness";
11
- import {
12
- applyCredentialForwarding,
13
- classifyDiskLog,
14
- createSandboxCredentialEnvironment,
15
- createBridgeToken,
16
- experimental_createBridgeUserMessageSubmitter,
17
- createBridgeErrorHandler,
18
- createBridgeStartupError,
19
- drainBridgeProcessStream,
20
- forwardBridgeProcessStream,
21
- getRestrictedSandboxSession,
22
- markBridgeStarting,
23
- resolveSandboxHomeDir,
24
- SandboxChannel,
25
- shellQuote,
26
- warnCredentialBrokeringUnavailable,
27
- waitForBridgeReady,
28
- withBridgeToken,
29
- writeSkills
30
- } from "@ai-sdk/harness/utils";
31
- import {
32
- safeParseJSON as safeParseJSON2,
33
- tool
34
- } from "@ai-sdk/provider-utils";
1
+ import path, { dirname, join } from "node:path";
2
+ import { HARNESS_V1_BUILTIN_TOOLS, HarnessCapabilityUnsupportedError, commonTool, harnessSessionDataDirectoryPath, harnessStateDirectoryPath, harnessV1BridgeInboundCommandSchemas, harnessV1BridgeOutboundMessageSchema, harnessV1BridgeStartBaseSchema, harnessV1DiagnosticFromBridgeFrame } from "@ai-sdk/harness";
3
+ import { SandboxChannel, applyCredentialForwarding, classifyDiskLog, createBridgeErrorHandler, createBridgeStartupError, createBridgeToken, createCredentialRequestTransformation, createReadBridgeAsset, drainBridgeProcessStream, experimental_createBridgeUserMessageSubmitter, forwardBridgeProcessStream, getAiGatewayAuthFromEnv, getRestrictedSandboxSession, isAccessTokenExpiringSoon, isHarnessAuthenticationEnvironment, isWindows, markBridgeStarting, parseJwtPayload, refreshOAuthAccessToken, resolveSandboxCredentialEnvironment, resolveSandboxHomeDir, shellQuote, waitForBridgeReady, warnCredentialBrokeringUnavailable, withBridgeToken, writeSkills } from "@ai-sdk/harness/utils";
4
+ import { isRecord, safeParseJSON, tool } from "@ai-sdk/provider-utils";
35
5
  import { WebSocket } from "ws";
36
- import { z as z2 } from "zod/v4";
37
-
38
- // src/opencode-bootstrap.ts
39
- import { createReadBridgeAsset } from "@ai-sdk/harness/utils";
40
- var readBridgeAsset = createReadBridgeAsset({
41
- "package.json": new URL("./bridge/package.json", import.meta.url),
42
- "pnpm-lock.yaml": new URL("./bridge/pnpm-lock.yaml", import.meta.url),
43
- "pnpm-workspace.yaml": new URL(
44
- "./bridge/pnpm-workspace.yaml",
45
- import.meta.url
46
- ),
47
- "index.mjs": new URL("./bridge/index.mjs", import.meta.url),
48
- "host-tool-mcp.mjs": new URL("./bridge/host-tool-mcp.mjs", import.meta.url)
6
+ import { z } from "zod/v4";
7
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
8
+ import { homedir } from "node:os";
9
+ //#region src/opencode-bootstrap.ts
10
+ const readBridgeAsset = createReadBridgeAsset({
11
+ "package.json": new URL("./bridge/package.json", import.meta.url),
12
+ "pnpm-lock.yaml": new URL("./bridge/pnpm-lock.yaml", import.meta.url),
13
+ "pnpm-workspace.yaml": new URL("./bridge/pnpm-workspace.yaml", import.meta.url),
14
+ "index.mjs": new URL("./bridge/index.mjs", import.meta.url),
15
+ "host-tool-mcp.mjs": new URL("./bridge/host-tool-mcp.mjs", import.meta.url)
49
16
  });
50
- var OPENCODE_BOOTSTRAP_DIR = ".harness-bootstrap/opencode";
51
- var cachedBootstrap;
17
+ const OPENCODE_BOOTSTRAP_DIR = ".harness-bootstrap/opencode";
18
+ let cachedBootstrap;
52
19
  async function getOpenCodeBootstrap() {
53
- if (cachedBootstrap != null) return cachedBootstrap;
54
- const [pkg, lock, workspace, bridge, hostToolMcp] = await Promise.all([
55
- readBridgeAsset("package.json"),
56
- readBridgeAsset("pnpm-lock.yaml"),
57
- readBridgeAsset("pnpm-workspace.yaml"),
58
- readBridgeAsset("index.mjs"),
59
- readBridgeAsset("host-tool-mcp.mjs")
60
- ]);
61
- cachedBootstrap = {
62
- harnessId: "opencode",
63
- bootstrapDir: OPENCODE_BOOTSTRAP_DIR,
64
- files: [
65
- { path: `${OPENCODE_BOOTSTRAP_DIR}/package.json`, content: pkg },
66
- { path: `${OPENCODE_BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
67
- {
68
- path: `${OPENCODE_BOOTSTRAP_DIR}/pnpm-workspace.yaml`,
69
- content: workspace
70
- },
71
- { path: `${OPENCODE_BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
72
- {
73
- path: `${OPENCODE_BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
74
- content: hostToolMcp
75
- }
76
- ],
77
- commands: [
78
- {
79
- command: "pnpm install --frozen-lockfile --store-dir .pnpm-store"
80
- },
81
- {
82
- command: "./node_modules/.bin/opencode --version"
83
- }
84
- ]
85
- };
86
- return cachedBootstrap;
20
+ if (cachedBootstrap != null) return cachedBootstrap;
21
+ const [pkg, lock, workspace, bridge, hostToolMcp] = await Promise.all([
22
+ readBridgeAsset("package.json"),
23
+ readBridgeAsset("pnpm-lock.yaml"),
24
+ readBridgeAsset("pnpm-workspace.yaml"),
25
+ readBridgeAsset("index.mjs"),
26
+ readBridgeAsset("host-tool-mcp.mjs")
27
+ ]);
28
+ cachedBootstrap = {
29
+ harnessId: "opencode",
30
+ bootstrapDir: OPENCODE_BOOTSTRAP_DIR,
31
+ files: [
32
+ {
33
+ path: `${OPENCODE_BOOTSTRAP_DIR}/package.json`,
34
+ content: pkg
35
+ },
36
+ {
37
+ path: `${OPENCODE_BOOTSTRAP_DIR}/pnpm-lock.yaml`,
38
+ content: lock
39
+ },
40
+ {
41
+ path: `${OPENCODE_BOOTSTRAP_DIR}/pnpm-workspace.yaml`,
42
+ content: workspace
43
+ },
44
+ {
45
+ path: `${OPENCODE_BOOTSTRAP_DIR}/bridge.mjs`,
46
+ content: bridge
47
+ },
48
+ {
49
+ path: `${OPENCODE_BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
50
+ content: hostToolMcp
51
+ }
52
+ ],
53
+ commands: [{ command: "pnpm install --frozen-lockfile --store-dir .pnpm-store" }, { command: "./node_modules/.bin/opencode --version" }]
54
+ };
55
+ return cachedBootstrap;
87
56
  }
88
-
89
- // src/opencode-auth.ts
90
- import {
91
- createCredentialRequestTransformation,
92
- getAiGatewayAuthFromEnv,
93
- isHarnessAuthenticationEnvironment
94
- } from "@ai-sdk/harness/utils";
95
- var OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE = "AI_SDK_OPENCODE_NATIVE_ACCESS_TOKEN";
96
- var OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES = [
97
- "AI_GATEWAY_API_KEY",
98
- "OPENAI_API_KEY",
99
- "ANTHROPIC_API_KEY",
100
- "ANTHROPIC_AUTH_TOKEN",
101
- "GOOGLE_GENERATIVE_AI_API_KEY",
102
- "XAI_API_KEY",
103
- "GITHUB_TOKEN",
104
- "GITHUB_COPILOT_TOKEN",
105
- "POE_API_KEY",
106
- "OPENCODE_API_KEY",
107
- "GITLAB_TOKEN",
108
- OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE
57
+ //#endregion
58
+ //#region src/opencode-auth.ts
59
+ const OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE = "AI_SDK_OPENCODE_NATIVE_ACCESS_TOKEN";
60
+ const OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES = [
61
+ "AI_GATEWAY_API_KEY",
62
+ "OPENAI_API_KEY",
63
+ "ANTHROPIC_API_KEY",
64
+ "ANTHROPIC_AUTH_TOKEN",
65
+ "GOOGLE_GENERATIVE_AI_API_KEY",
66
+ "XAI_API_KEY",
67
+ "GITHUB_TOKEN",
68
+ "GITHUB_COPILOT_TOKEN",
69
+ "POE_API_KEY",
70
+ "OPENCODE_API_KEY",
71
+ "GITLAB_TOKEN",
72
+ OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE
109
73
  ];
110
- var NON_GOOGLE_DIRECT_CREDENTIAL_ENVIRONMENT_VARIABLES = [
111
- "OPENAI_API_KEY",
112
- "ANTHROPIC_API_KEY",
113
- "ANTHROPIC_AUTH_TOKEN",
114
- "XAI_API_KEY",
115
- "GITHUB_TOKEN",
116
- "GITHUB_COPILOT_TOKEN",
117
- "POE_API_KEY",
118
- "OPENCODE_API_KEY",
119
- "GITLAB_TOKEN"
74
+ const NON_GOOGLE_DIRECT_CREDENTIAL_ENVIRONMENT_VARIABLES = [
75
+ "OPENAI_API_KEY",
76
+ "ANTHROPIC_API_KEY",
77
+ "ANTHROPIC_AUTH_TOKEN",
78
+ "XAI_API_KEY",
79
+ "GITHUB_TOKEN",
80
+ "GITHUB_COPILOT_TOKEN",
81
+ "POE_API_KEY",
82
+ "OPENCODE_API_KEY",
83
+ "GITLAB_TOKEN"
120
84
  ];
121
- function createOpenCodeRequestTransformations({
122
- env: environment,
123
- sandboxEnv: sandboxEnvironment,
124
- auth: authenticationMode
125
- }) {
126
- switch (authenticationMode) {
127
- case "ai-gateway":
128
- return environment.AI_GATEWAY_API_KEY && sandboxEnvironment.AI_GATEWAY_API_KEY ? [
129
- createCredentialRequestTransformation({
130
- matchUrl: environment.AI_GATEWAY_BASE_URL,
131
- matchHeaders: {
132
- "x-api-key": sandboxEnvironment.AI_GATEWAY_API_KEY
133
- },
134
- transformHeaders: {
135
- Authorization: `Bearer ${environment.AI_GATEWAY_API_KEY}`
136
- }
137
- }),
138
- createCredentialRequestTransformation({
139
- matchUrl: environment.AI_GATEWAY_BASE_URL,
140
- matchHeaders: {
141
- Authorization: `Bearer ${sandboxEnvironment.AI_GATEWAY_API_KEY}`
142
- },
143
- transformHeaders: {
144
- Authorization: `Bearer ${environment.AI_GATEWAY_API_KEY}`
145
- }
146
- })
147
- ] : [];
148
- case "openai":
149
- return environment.OPENAI_API_KEY && sandboxEnvironment.OPENAI_API_KEY ? [
150
- createCredentialRequestTransformation({
151
- matchUrl: environment.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
152
- matchHeaders: {
153
- Authorization: `Bearer ${sandboxEnvironment.OPENAI_API_KEY}`
154
- },
155
- transformHeaders: {
156
- Authorization: `Bearer ${environment.OPENAI_API_KEY}`
157
- }
158
- })
159
- ] : [];
160
- case "anthropic": {
161
- const matchUrl = environment.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com";
162
- const transformations = [];
163
- if (environment.ANTHROPIC_API_KEY != null && sandboxEnvironment.ANTHROPIC_API_KEY != null) {
164
- transformations.push(
165
- createCredentialRequestTransformation({
166
- matchUrl,
167
- matchHeaders: {
168
- "x-api-key": sandboxEnvironment.ANTHROPIC_API_KEY
169
- },
170
- transformHeaders: {
171
- "x-api-key": environment.ANTHROPIC_API_KEY
172
- }
173
- })
174
- );
175
- }
176
- if (environment.ANTHROPIC_AUTH_TOKEN != null && sandboxEnvironment.ANTHROPIC_AUTH_TOKEN != null) {
177
- transformations.push(
178
- createCredentialRequestTransformation({
179
- matchUrl,
180
- matchHeaders: {
181
- Authorization: `Bearer ${sandboxEnvironment.ANTHROPIC_AUTH_TOKEN}`
182
- },
183
- transformHeaders: {
184
- Authorization: `Bearer ${environment.ANTHROPIC_AUTH_TOKEN}`
185
- }
186
- })
187
- );
188
- }
189
- return transformations;
190
- }
191
- case "google":
192
- return environment.GOOGLE_GENERATIVE_AI_API_KEY && sandboxEnvironment.GOOGLE_GENERATIVE_AI_API_KEY ? [
193
- createCredentialRequestTransformation({
194
- matchUrl: "https://generativelanguage.googleapis.com",
195
- matchHeaders: {
196
- "x-goog-api-key": sandboxEnvironment.GOOGLE_GENERATIVE_AI_API_KEY
197
- },
198
- transformHeaders: {
199
- "x-goog-api-key": environment.GOOGLE_GENERATIVE_AI_API_KEY
200
- }
201
- })
202
- ] : [];
203
- case "xai":
204
- return createBearerTransformation({
205
- environment,
206
- sandboxEnvironment,
207
- environmentVariableName: "XAI_API_KEY",
208
- matchUrl: environment.XAI_BASE_URL ?? "https://api.x.ai/v1"
209
- });
210
- case "github-copilot":
211
- return createBearerTransformation({
212
- environment,
213
- sandboxEnvironment,
214
- environmentVariableName: environment.GITHUB_COPILOT_TOKEN != null ? "GITHUB_COPILOT_TOKEN" : "GITHUB_TOKEN",
215
- matchUrl: "https://api.githubcopilot.com"
216
- });
217
- case "poe":
218
- return createBearerTransformation({
219
- environment,
220
- sandboxEnvironment,
221
- environmentVariableName: "POE_API_KEY",
222
- matchUrl: "https://api.poe.com"
223
- });
224
- case "opencode-go":
225
- return createBearerTransformation({
226
- environment,
227
- sandboxEnvironment,
228
- environmentVariableName: "OPENCODE_API_KEY",
229
- matchUrl: "https://opencode.ai/zen/go/v1"
230
- });
231
- case "gitlab":
232
- return createBearerTransformation({
233
- environment,
234
- sandboxEnvironment,
235
- environmentVariableName: "GITLAB_TOKEN",
236
- matchUrl: environment.GITLAB_INSTANCE_URL ?? "https://gitlab.com"
237
- });
238
- }
85
+ function createOpenCodeRequestTransformations({ env: environment, sandboxEnv: sandboxEnvironment, auth: authenticationMode }) {
86
+ switch (authenticationMode) {
87
+ case "ai-gateway": return environment.AI_GATEWAY_API_KEY && sandboxEnvironment.AI_GATEWAY_API_KEY ? [createCredentialRequestTransformation({
88
+ matchUrl: environment.AI_GATEWAY_BASE_URL,
89
+ matchHeaders: { "x-api-key": sandboxEnvironment.AI_GATEWAY_API_KEY },
90
+ transformHeaders: { Authorization: `Bearer ${environment.AI_GATEWAY_API_KEY}` }
91
+ }), createCredentialRequestTransformation({
92
+ matchUrl: environment.AI_GATEWAY_BASE_URL,
93
+ matchHeaders: { Authorization: `Bearer ${sandboxEnvironment.AI_GATEWAY_API_KEY}` },
94
+ transformHeaders: { Authorization: `Bearer ${environment.AI_GATEWAY_API_KEY}` }
95
+ })] : [];
96
+ case "openai": return environment.OPENAI_API_KEY && sandboxEnvironment.OPENAI_API_KEY ? [createCredentialRequestTransformation({
97
+ matchUrl: environment.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
98
+ matchHeaders: { Authorization: `Bearer ${sandboxEnvironment.OPENAI_API_KEY}` },
99
+ transformHeaders: { Authorization: `Bearer ${environment.OPENAI_API_KEY}` }
100
+ })] : [];
101
+ case "anthropic": {
102
+ const matchUrl = environment.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com";
103
+ const transformations = [];
104
+ if (environment.ANTHROPIC_API_KEY != null && sandboxEnvironment.ANTHROPIC_API_KEY != null) transformations.push(createCredentialRequestTransformation({
105
+ matchUrl,
106
+ matchHeaders: { "x-api-key": sandboxEnvironment.ANTHROPIC_API_KEY },
107
+ transformHeaders: { "x-api-key": environment.ANTHROPIC_API_KEY }
108
+ }));
109
+ if (environment.ANTHROPIC_AUTH_TOKEN != null && sandboxEnvironment.ANTHROPIC_AUTH_TOKEN != null) transformations.push(createCredentialRequestTransformation({
110
+ matchUrl,
111
+ matchHeaders: { Authorization: `Bearer ${sandboxEnvironment.ANTHROPIC_AUTH_TOKEN}` },
112
+ transformHeaders: { Authorization: `Bearer ${environment.ANTHROPIC_AUTH_TOKEN}` }
113
+ }));
114
+ return transformations;
115
+ }
116
+ case "google": return environment.GOOGLE_GENERATIVE_AI_API_KEY && sandboxEnvironment.GOOGLE_GENERATIVE_AI_API_KEY ? [createCredentialRequestTransformation({
117
+ matchUrl: "https://generativelanguage.googleapis.com",
118
+ matchHeaders: { "x-goog-api-key": sandboxEnvironment.GOOGLE_GENERATIVE_AI_API_KEY },
119
+ transformHeaders: { "x-goog-api-key": environment.GOOGLE_GENERATIVE_AI_API_KEY }
120
+ })] : [];
121
+ case "xai": return createBearerTransformation({
122
+ environment,
123
+ sandboxEnvironment,
124
+ environmentVariableName: "XAI_API_KEY",
125
+ matchUrl: environment.XAI_BASE_URL ?? "https://api.x.ai/v1"
126
+ });
127
+ case "github-copilot": return createBearerTransformation({
128
+ environment,
129
+ sandboxEnvironment,
130
+ environmentVariableName: environment.GITHUB_COPILOT_TOKEN != null ? "GITHUB_COPILOT_TOKEN" : "GITHUB_TOKEN",
131
+ matchUrl: "https://api.githubcopilot.com"
132
+ });
133
+ case "poe": return createBearerTransformation({
134
+ environment,
135
+ sandboxEnvironment,
136
+ environmentVariableName: "POE_API_KEY",
137
+ matchUrl: "https://api.poe.com"
138
+ });
139
+ case "opencode-go": return createBearerTransformation({
140
+ environment,
141
+ sandboxEnvironment,
142
+ environmentVariableName: "OPENCODE_API_KEY",
143
+ matchUrl: "https://opencode.ai/zen/go/v1"
144
+ });
145
+ case "gitlab": return createBearerTransformation({
146
+ environment,
147
+ sandboxEnvironment,
148
+ environmentVariableName: "GITLAB_TOKEN",
149
+ matchUrl: environment.GITLAB_INSTANCE_URL ?? "https://gitlab.com"
150
+ });
151
+ }
239
152
  }
240
- function resolveOpenCodeProvider({
241
- model,
242
- provider
243
- }) {
244
- if (isDirectProvider(provider)) {
245
- return provider;
246
- }
247
- if (model?.includes("/")) {
248
- const [modelProvider] = model.split("/");
249
- if (isDirectProvider(modelProvider)) {
250
- return modelProvider;
251
- }
252
- }
253
- return "anthropic";
153
+ function resolveOpenCodeProvider({ model, provider }) {
154
+ if (isDirectProvider(provider)) return provider;
155
+ if (model?.includes("/")) {
156
+ const [modelProvider] = model.split("/");
157
+ if (isDirectProvider(modelProvider)) return modelProvider;
158
+ }
159
+ return "anthropic";
254
160
  }
255
- function resolveOpenCodeEnv({
256
- auth,
257
- model,
258
- provider,
259
- processEnv = process.env
260
- }) {
261
- const suppliedEnvironment = isHarnessAuthenticationEnvironment(auth);
262
- const authenticationEnvironment = suppliedEnvironment ? auth : processEnv;
263
- const selectedProvider = resolveOpenCodeAuthenticationProvider({
264
- auth,
265
- model,
266
- provider,
267
- environment: authenticationEnvironment
268
- });
269
- if (selectedProvider === "openai" && auth === "openai" || selectedProvider === "anthropic" && auth === "anthropic") {
270
- return pickDirectProvider({
271
- provider: selectedProvider,
272
- processEnv: authenticationEnvironment
273
- });
274
- }
275
- const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({
276
- env: authenticationEnvironment
277
- });
278
- if (auth === "ai-gateway" || gatewayAuthFromEnv.apiKey) {
279
- return pickGateway({ gatewayAuthFromEnv });
280
- }
281
- return pickDirectProvider({
282
- provider: selectedProvider,
283
- processEnv: authenticationEnvironment
284
- });
161
+ function resolveOpenCodeEnv({ auth, model, provider, processEnv = process.env }) {
162
+ const authenticationEnvironment = isHarnessAuthenticationEnvironment(auth) ? auth : processEnv;
163
+ const selectedProvider = resolveOpenCodeAuthenticationProvider({
164
+ auth,
165
+ model,
166
+ provider,
167
+ environment: authenticationEnvironment
168
+ });
169
+ if (selectedProvider === "openai" && auth === "openai" || selectedProvider === "anthropic" && auth === "anthropic") return pickDirectProvider({
170
+ provider: selectedProvider,
171
+ processEnv: authenticationEnvironment
172
+ });
173
+ const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env: authenticationEnvironment });
174
+ if (auth === "ai-gateway" || gatewayAuthFromEnv.apiKey) return pickGateway({ gatewayAuthFromEnv });
175
+ return pickDirectProvider({
176
+ provider: selectedProvider,
177
+ processEnv: authenticationEnvironment
178
+ });
285
179
  }
286
- function resolveOpenCodeAuthenticationMode({
287
- auth,
288
- model,
289
- provider,
290
- processEnv = process.env
291
- }) {
292
- if (isHarnessAuthenticationEnvironment(auth)) {
293
- return getAiGatewayAuthFromEnv({ env: auth }).apiKey ? "ai-gateway" : resolveOpenCodeAuthenticationProvider({
294
- auth,
295
- model,
296
- provider,
297
- environment: auth
298
- });
299
- }
300
- const selectedProvider = resolveOpenCodeAuthenticationProvider({
301
- auth,
302
- model,
303
- provider,
304
- environment: processEnv
305
- });
306
- if (selectedProvider === "openai" && auth === "openai") {
307
- return "openai";
308
- }
309
- if (selectedProvider === "anthropic" && auth === "anthropic") {
310
- return "anthropic";
311
- }
312
- if (auth === "ai-gateway") {
313
- return "ai-gateway";
314
- }
315
- if (getAiGatewayAuthFromEnv({ env: processEnv }).apiKey) {
316
- return "ai-gateway";
317
- }
318
- return selectedProvider;
180
+ function resolveOpenCodeAuthenticationMode({ auth, model, provider, processEnv = process.env }) {
181
+ if (isHarnessAuthenticationEnvironment(auth)) return getAiGatewayAuthFromEnv({ env: auth }).apiKey ? "ai-gateway" : resolveOpenCodeAuthenticationProvider({
182
+ auth,
183
+ model,
184
+ provider,
185
+ environment: auth
186
+ });
187
+ const selectedProvider = resolveOpenCodeAuthenticationProvider({
188
+ auth,
189
+ model,
190
+ provider,
191
+ environment: processEnv
192
+ });
193
+ if (selectedProvider === "openai" && auth === "openai") return "openai";
194
+ if (selectedProvider === "anthropic" && auth === "anthropic") return "anthropic";
195
+ if (auth === "ai-gateway") return "ai-gateway";
196
+ if (getAiGatewayAuthFromEnv({ env: processEnv }).apiKey) return "ai-gateway";
197
+ return selectedProvider;
319
198
  }
320
- function resolveOpenCodeAuthenticationProvider({
321
- auth,
322
- model,
323
- provider,
324
- environment
325
- }) {
326
- const selectedProvider = resolveOpenCodeProvider({ model, provider });
327
- if (model == null && provider == null && (auth === "anthropic" || auth === "openai")) {
328
- return auth;
329
- }
330
- return model == null && provider == null && (environment.GOOGLE_GENERATIVE_AI_API_KEY?.length ?? 0) > 0 && !NON_GOOGLE_DIRECT_CREDENTIAL_ENVIRONMENT_VARIABLES.some(
331
- (name) => (environment[name]?.length ?? 0) > 0
332
- ) ? "google" : selectedProvider;
199
+ function resolveOpenCodeAuthenticationProvider({ auth, model, provider, environment }) {
200
+ const selectedProvider = resolveOpenCodeProvider({
201
+ model,
202
+ provider
203
+ });
204
+ if (model == null && provider == null && (auth === "anthropic" || auth === "openai")) return auth;
205
+ return model == null && provider == null && (environment.GOOGLE_GENERATIVE_AI_API_KEY?.length ?? 0) > 0 && !NON_GOOGLE_DIRECT_CREDENTIAL_ENVIRONMENT_VARIABLES.some((name) => (environment[name]?.length ?? 0) > 0) ? "google" : selectedProvider;
333
206
  }
334
- function pickOpenAI({
335
- processEnv
336
- }) {
337
- const env = {};
338
- const apiKey = processEnv.OPENAI_API_KEY;
339
- if (apiKey) env.OPENAI_API_KEY = apiKey;
340
- const baseUrl = processEnv.OPENAI_BASE_URL;
341
- if (baseUrl) env.OPENAI_BASE_URL = baseUrl;
342
- const organization = processEnv.OPENAI_ORGANIZATION;
343
- if (organization) env.OPENAI_ORGANIZATION = organization;
344
- const project = processEnv.OPENAI_PROJECT;
345
- if (project) env.OPENAI_PROJECT = project;
346
- return env;
207
+ function pickOpenAI({ processEnv }) {
208
+ const env = {};
209
+ const apiKey = processEnv.OPENAI_API_KEY;
210
+ if (apiKey) env.OPENAI_API_KEY = apiKey;
211
+ const baseUrl = processEnv.OPENAI_BASE_URL;
212
+ if (baseUrl) env.OPENAI_BASE_URL = baseUrl;
213
+ const organization = processEnv.OPENAI_ORGANIZATION;
214
+ if (organization) env.OPENAI_ORGANIZATION = organization;
215
+ const project = processEnv.OPENAI_PROJECT;
216
+ if (project) env.OPENAI_PROJECT = project;
217
+ return env;
347
218
  }
348
- function pickAnthropic({
349
- processEnv
350
- }) {
351
- const env = {};
352
- const apiKey = processEnv.ANTHROPIC_API_KEY;
353
- if (apiKey) env.ANTHROPIC_API_KEY = apiKey;
354
- const authToken = processEnv.ANTHROPIC_AUTH_TOKEN;
355
- if (authToken) env.ANTHROPIC_AUTH_TOKEN = authToken;
356
- const baseUrl = processEnv.ANTHROPIC_BASE_URL;
357
- if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl;
358
- return env;
219
+ function pickAnthropic({ processEnv }) {
220
+ const env = {};
221
+ const apiKey = processEnv.ANTHROPIC_API_KEY;
222
+ if (apiKey) env.ANTHROPIC_API_KEY = apiKey;
223
+ const authToken = processEnv.ANTHROPIC_AUTH_TOKEN;
224
+ if (authToken) env.ANTHROPIC_AUTH_TOKEN = authToken;
225
+ const baseUrl = processEnv.ANTHROPIC_BASE_URL;
226
+ if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl;
227
+ return env;
359
228
  }
360
- function pickDirectProvider({
361
- provider,
362
- processEnv
363
- }) {
364
- if (provider === "openai") return pickOpenAI({ processEnv });
365
- if (provider === "anthropic") return pickAnthropic({ processEnv });
366
- const names = provider === "google" ? ["GOOGLE_GENERATIVE_AI_API_KEY"] : provider === "xai" ? ["XAI_API_KEY", "XAI_BASE_URL"] : provider === "github-copilot" ? ["GITHUB_COPILOT_TOKEN", "GITHUB_TOKEN"] : provider === "poe" ? ["POE_API_KEY"] : provider === "opencode-go" ? ["OPENCODE_API_KEY"] : ["GITLAB_TOKEN", "GITLAB_INSTANCE_URL"];
367
- return Object.fromEntries(
368
- names.flatMap(
369
- (name) => processEnv[name] == null ? [] : [[name, processEnv[name]]]
370
- )
371
- );
229
+ function pickDirectProvider({ provider, processEnv }) {
230
+ if (provider === "openai") return pickOpenAI({ processEnv });
231
+ if (provider === "anthropic") return pickAnthropic({ processEnv });
232
+ return Object.fromEntries((provider === "google" ? ["GOOGLE_GENERATIVE_AI_API_KEY"] : provider === "xai" ? ["XAI_API_KEY", "XAI_BASE_URL"] : provider === "github-copilot" ? ["GITHUB_COPILOT_TOKEN", "GITHUB_TOKEN"] : provider === "poe" ? ["POE_API_KEY"] : provider === "opencode-go" ? ["OPENCODE_API_KEY"] : ["GITLAB_TOKEN", "GITLAB_INSTANCE_URL"]).flatMap((name) => processEnv[name] == null ? [] : [[name, processEnv[name]]]));
372
233
  }
373
- function hasOpenCodeCredential({
374
- environment,
375
- authenticationMode
376
- }) {
377
- if (authenticationMode === "ai-gateway") {
378
- return environment.AI_GATEWAY_API_KEY != null;
379
- }
380
- return Object.keys(environment).some(
381
- (name) => name.endsWith("_API_KEY") || name.endsWith("_TOKEN") || name === "ANTHROPIC_AUTH_TOKEN"
382
- );
234
+ function hasOpenCodeCredential({ environment, authenticationMode }) {
235
+ if (authenticationMode === "ai-gateway") return environment.AI_GATEWAY_API_KEY != null;
236
+ return Object.keys(environment).some((name) => name.endsWith("_API_KEY") || name.endsWith("_TOKEN") || name === "ANTHROPIC_AUTH_TOKEN");
383
237
  }
384
- function createBearerTransformation({
385
- environment,
386
- sandboxEnvironment,
387
- environmentVariableName,
388
- matchUrl
389
- }) {
390
- const credential = environment[environmentVariableName];
391
- const sandboxCredential = sandboxEnvironment[environmentVariableName];
392
- return credential == null || sandboxCredential == null ? [] : [
393
- createCredentialRequestTransformation({
394
- matchUrl,
395
- matchHeaders: {
396
- Authorization: `Bearer ${sandboxCredential}`
397
- },
398
- transformHeaders: { Authorization: `Bearer ${credential}` }
399
- })
400
- ];
238
+ function createBearerTransformation({ environment, sandboxEnvironment, environmentVariableName, matchUrl }) {
239
+ const credential = environment[environmentVariableName];
240
+ const sandboxCredential = sandboxEnvironment[environmentVariableName];
241
+ return credential == null || sandboxCredential == null ? [] : [createCredentialRequestTransformation({
242
+ matchUrl,
243
+ matchHeaders: { Authorization: `Bearer ${sandboxCredential}` },
244
+ transformHeaders: { Authorization: `Bearer ${credential}` }
245
+ })];
401
246
  }
402
247
  function isDirectProvider(value) {
403
- return value === "anthropic" || value === "openai" || value === "google" || value === "xai" || value === "github-copilot" || value === "poe" || value === "opencode-go" || value === "gitlab";
248
+ return value === "anthropic" || value === "openai" || value === "google" || value === "xai" || value === "github-copilot" || value === "poe" || value === "opencode-go" || value === "gitlab";
404
249
  }
405
- function pickGateway({
406
- gatewayAuthFromEnv
407
- }) {
408
- const env = {};
409
- const apiKey = gatewayAuthFromEnv.apiKey;
410
- if (apiKey) env.AI_GATEWAY_API_KEY = apiKey;
411
- const baseUrl = toOpenCodeGatewayBaseUrl(gatewayAuthFromEnv.baseUrl);
412
- env.AI_GATEWAY_BASE_URL = baseUrl;
413
- return env;
250
+ function pickGateway({ gatewayAuthFromEnv }) {
251
+ const env = {};
252
+ const apiKey = gatewayAuthFromEnv.apiKey;
253
+ if (apiKey) env.AI_GATEWAY_API_KEY = apiKey;
254
+ env.AI_GATEWAY_BASE_URL = toOpenCodeGatewayBaseUrl(gatewayAuthFromEnv.baseUrl);
255
+ return env;
414
256
  }
415
257
  function toOpenCodeGatewayBaseUrl(baseUrl) {
416
- const trimmed = baseUrl.replace(/\/+$/, "");
417
- return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
258
+ const trimmed = baseUrl.replace(/\/+$/, "");
259
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
418
260
  }
419
-
420
- // src/opencode-subscription.ts
421
- import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
422
- import { homedir } from "os";
423
- import { dirname, join } from "path";
424
- import {
425
- HarnessCapabilityUnsupportedError
426
- } from "@ai-sdk/harness";
427
- import {
428
- createCredentialRequestTransformation as createCredentialRequestTransformation2,
429
- isAccessTokenExpiringSoon,
430
- isHarnessAuthenticationEnvironment as isHarnessAuthenticationEnvironment2,
431
- isWindows,
432
- parseJwtPayload,
433
- refreshOAuthAccessToken
434
- } from "@ai-sdk/harness/utils";
435
- import { isRecord, safeParseJSON } from "@ai-sdk/provider-utils";
436
- var OPENAI_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
437
- var XAI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
438
- var GITLAB_CLIENT_ID = "1d89f9fdb23ee96d4e603201f6861dab6e143c5c3c00469a018a2d94bdc03d4e";
439
- var GITLAB_AI_GATEWAY_URL = "https://cloud.gitlab.com";
440
- var GITLAB_ANTHROPIC_PROVIDER_ID = "ai-sdk-gitlab-anthropic";
441
- var GITLAB_OPENAI_CHAT_PROVIDER_ID = "ai-sdk-gitlab-openai-chat";
442
- var GITLAB_OPENAI_RESPONSES_PROVIDER_ID = "ai-sdk-gitlab-openai-responses";
443
- var OPEN_CODE_GITLAB_MODELS = {
444
- "duo-chat-fable-5": {
445
- providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
446
- modelId: "claude-fable-5"
447
- },
448
- "duo-chat-opus-5": {
449
- providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
450
- modelId: "claude-opus-5"
451
- },
452
- "duo-chat-opus-4-8": {
453
- providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
454
- modelId: "claude-opus-4-8"
455
- },
456
- "duo-chat-opus-4-7": {
457
- providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
458
- modelId: "claude-opus-4-7"
459
- },
460
- "duo-chat-opus-4-6": {
461
- providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
462
- modelId: "claude-opus-4-6"
463
- },
464
- "duo-chat-sonnet-5": {
465
- providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
466
- modelId: "claude-sonnet-5"
467
- },
468
- "duo-chat-sonnet-4-6": {
469
- providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
470
- modelId: "claude-sonnet-4-6"
471
- },
472
- "duo-chat-opus-4-5": {
473
- providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
474
- modelId: "claude-opus-4-5-20251101"
475
- },
476
- "duo-chat-sonnet-4-5": {
477
- providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
478
- modelId: "claude-sonnet-4-5-20250929"
479
- },
480
- "duo-chat-haiku-4-5": {
481
- providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
482
- modelId: "claude-haiku-4-5-20251001"
483
- },
484
- "duo-chat-gpt-5-1": {
485
- providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
486
- modelId: "gpt-5.1-2025-11-13"
487
- },
488
- "duo-chat-gpt-5-2": {
489
- providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
490
- modelId: "gpt-5.2-2025-12-11"
491
- },
492
- "duo-chat-gpt-5-4": {
493
- providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
494
- modelId: "gpt-5.4-2026-03-05"
495
- },
496
- "duo-chat-gpt-5-5": {
497
- providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
498
- modelId: "gpt-5.5-2026-04-23"
499
- },
500
- "duo-chat-gpt-5-mini": {
501
- providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
502
- modelId: "gpt-5-mini-2025-08-07"
503
- },
504
- "duo-chat-gpt-5-4-mini": {
505
- providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
506
- modelId: "gpt-5.4-mini"
507
- },
508
- "duo-chat-gpt-5-4-nano": {
509
- providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
510
- modelId: "gpt-5.4-nano"
511
- },
512
- "duo-chat-gpt-5-6-sol": {
513
- providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
514
- modelId: "gpt-5.6-sol"
515
- },
516
- "duo-chat-gpt-5-6-terra": {
517
- providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
518
- modelId: "gpt-5.6-terra"
519
- },
520
- "duo-chat-gpt-5-6-luna": {
521
- providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
522
- modelId: "gpt-5.6-luna"
523
- },
524
- "duo-chat-gpt-5-codex": {
525
- providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
526
- modelId: "gpt-5-codex"
527
- },
528
- "duo-chat-gpt-5-2-codex": {
529
- providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
530
- modelId: "gpt-5.2-codex"
531
- },
532
- "duo-chat-gpt-5-3-codex": {
533
- providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
534
- modelId: "gpt-5.3-codex"
535
- },
536
- "duo-workflow": { providerId: "workflow", modelId: "default" },
537
- "duo-workflow-default": { providerId: "workflow", modelId: "default" },
538
- "duo-workflow-sonnet-4-5": {
539
- providerId: "workflow",
540
- modelId: "anthropic/claude-sonnet-4-5-20250929"
541
- },
542
- "duo-workflow-sonnet-5": {
543
- providerId: "workflow",
544
- modelId: "claude_sonnet_5"
545
- },
546
- "duo-workflow-opus-5": {
547
- providerId: "workflow",
548
- modelId: "claude_opus_5"
549
- },
550
- "duo-workflow-sonnet-4-6": {
551
- providerId: "workflow",
552
- modelId: "claude_sonnet_4_6"
553
- },
554
- "duo-workflow-opus-4-5": {
555
- providerId: "workflow",
556
- modelId: "anthropic/claude-opus-4-5-20251101"
557
- },
558
- "duo-workflow-haiku-4-5": {
559
- providerId: "workflow",
560
- modelId: "claude_haiku_4_5_20251001"
561
- },
562
- "duo-workflow-opus-4-6": {
563
- providerId: "workflow",
564
- modelId: "claude_opus_4_6_20260205"
565
- }
261
+ //#endregion
262
+ //#region src/opencode-subscription.ts
263
+ const OPENAI_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
264
+ const XAI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
265
+ const GITLAB_CLIENT_ID = "1d89f9fdb23ee96d4e603201f6861dab6e143c5c3c00469a018a2d94bdc03d4e";
266
+ const GITLAB_AI_GATEWAY_URL = "https://cloud.gitlab.com";
267
+ const GITLAB_ANTHROPIC_PROVIDER_ID = "ai-sdk-gitlab-anthropic";
268
+ const GITLAB_OPENAI_CHAT_PROVIDER_ID = "ai-sdk-gitlab-openai-chat";
269
+ const GITLAB_OPENAI_RESPONSES_PROVIDER_ID = "ai-sdk-gitlab-openai-responses";
270
+ const OPEN_CODE_GITLAB_MODELS = {
271
+ "duo-chat-fable-5": {
272
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
273
+ modelId: "claude-fable-5"
274
+ },
275
+ "duo-chat-opus-5": {
276
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
277
+ modelId: "claude-opus-5"
278
+ },
279
+ "duo-chat-opus-4-8": {
280
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
281
+ modelId: "claude-opus-4-8"
282
+ },
283
+ "duo-chat-opus-4-7": {
284
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
285
+ modelId: "claude-opus-4-7"
286
+ },
287
+ "duo-chat-opus-4-6": {
288
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
289
+ modelId: "claude-opus-4-6"
290
+ },
291
+ "duo-chat-sonnet-5": {
292
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
293
+ modelId: "claude-sonnet-5"
294
+ },
295
+ "duo-chat-sonnet-4-6": {
296
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
297
+ modelId: "claude-sonnet-4-6"
298
+ },
299
+ "duo-chat-opus-4-5": {
300
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
301
+ modelId: "claude-opus-4-5-20251101"
302
+ },
303
+ "duo-chat-sonnet-4-5": {
304
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
305
+ modelId: "claude-sonnet-4-5-20250929"
306
+ },
307
+ "duo-chat-haiku-4-5": {
308
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
309
+ modelId: "claude-haiku-4-5-20251001"
310
+ },
311
+ "duo-chat-gpt-5-1": {
312
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
313
+ modelId: "gpt-5.1-2025-11-13"
314
+ },
315
+ "duo-chat-gpt-5-2": {
316
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
317
+ modelId: "gpt-5.2-2025-12-11"
318
+ },
319
+ "duo-chat-gpt-5-4": {
320
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
321
+ modelId: "gpt-5.4-2026-03-05"
322
+ },
323
+ "duo-chat-gpt-5-5": {
324
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
325
+ modelId: "gpt-5.5-2026-04-23"
326
+ },
327
+ "duo-chat-gpt-5-mini": {
328
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
329
+ modelId: "gpt-5-mini-2025-08-07"
330
+ },
331
+ "duo-chat-gpt-5-4-mini": {
332
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
333
+ modelId: "gpt-5.4-mini"
334
+ },
335
+ "duo-chat-gpt-5-4-nano": {
336
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
337
+ modelId: "gpt-5.4-nano"
338
+ },
339
+ "duo-chat-gpt-5-6-sol": {
340
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
341
+ modelId: "gpt-5.6-sol"
342
+ },
343
+ "duo-chat-gpt-5-6-terra": {
344
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
345
+ modelId: "gpt-5.6-terra"
346
+ },
347
+ "duo-chat-gpt-5-6-luna": {
348
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
349
+ modelId: "gpt-5.6-luna"
350
+ },
351
+ "duo-chat-gpt-5-codex": {
352
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
353
+ modelId: "gpt-5-codex"
354
+ },
355
+ "duo-chat-gpt-5-2-codex": {
356
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
357
+ modelId: "gpt-5.2-codex"
358
+ },
359
+ "duo-chat-gpt-5-3-codex": {
360
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
361
+ modelId: "gpt-5.3-codex"
362
+ },
363
+ "duo-workflow": {
364
+ providerId: "workflow",
365
+ modelId: "default"
366
+ },
367
+ "duo-workflow-default": {
368
+ providerId: "workflow",
369
+ modelId: "default"
370
+ },
371
+ "duo-workflow-sonnet-4-5": {
372
+ providerId: "workflow",
373
+ modelId: "anthropic/claude-sonnet-4-5-20250929"
374
+ },
375
+ "duo-workflow-sonnet-5": {
376
+ providerId: "workflow",
377
+ modelId: "claude_sonnet_5"
378
+ },
379
+ "duo-workflow-opus-5": {
380
+ providerId: "workflow",
381
+ modelId: "claude_opus_5"
382
+ },
383
+ "duo-workflow-sonnet-4-6": {
384
+ providerId: "workflow",
385
+ modelId: "claude_sonnet_4_6"
386
+ },
387
+ "duo-workflow-opus-4-5": {
388
+ providerId: "workflow",
389
+ modelId: "anthropic/claude-opus-4-5-20251101"
390
+ },
391
+ "duo-workflow-haiku-4-5": {
392
+ providerId: "workflow",
393
+ modelId: "claude_haiku_4_5_20251001"
394
+ },
395
+ "duo-workflow-opus-4-6": {
396
+ providerId: "workflow",
397
+ modelId: "claude_opus_4_6_20260205"
398
+ }
566
399
  };
567
- async function resolveOpenCodeAuthentication({
568
- auth,
569
- model,
570
- provider,
571
- processEnv = process.env,
572
- readSubscription = readOpenCodeSubscription
573
- }) {
574
- const environment = resolveOpenCodeEnv({
575
- auth,
576
- model,
577
- provider,
578
- processEnv
579
- });
580
- const authenticationMode = resolveOpenCodeAuthenticationMode({
581
- auth,
582
- model,
583
- provider,
584
- processEnv
585
- });
586
- if (isHarnessAuthenticationEnvironment2(auth) || auth === "ai-gateway" || authenticationMode === "ai-gateway" || hasOpenCodeCredential({ environment, authenticationMode })) {
587
- return { environment, authenticationMode };
588
- }
589
- const subscription = await readSubscription({
590
- providerId: authenticationMode,
591
- env: processEnv
592
- });
593
- return subscription == null ? { environment, authenticationMode } : {
594
- environment: {
595
- ...environment,
596
- [OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE]: subscription.accessToken
597
- },
598
- authenticationMode,
599
- subscription
600
- };
400
+ async function resolveOpenCodeAuthentication({ auth, model, provider, processEnv = process.env, readSubscription = readOpenCodeSubscription }) {
401
+ const environment = resolveOpenCodeEnv({
402
+ auth,
403
+ model,
404
+ provider,
405
+ processEnv
406
+ });
407
+ const authenticationMode = resolveOpenCodeAuthenticationMode({
408
+ auth,
409
+ model,
410
+ provider,
411
+ processEnv
412
+ });
413
+ if (isHarnessAuthenticationEnvironment(auth) || auth === "ai-gateway" || authenticationMode === "ai-gateway" || hasOpenCodeCredential({
414
+ environment,
415
+ authenticationMode
416
+ })) return {
417
+ environment,
418
+ authenticationMode
419
+ };
420
+ const subscription = await readSubscription({
421
+ providerId: authenticationMode,
422
+ env: processEnv
423
+ });
424
+ return subscription == null ? {
425
+ environment,
426
+ authenticationMode
427
+ } : {
428
+ environment: {
429
+ ...environment,
430
+ [OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE]: subscription.accessToken
431
+ },
432
+ authenticationMode,
433
+ subscription
434
+ };
601
435
  }
602
- async function readOpenCodeSubscription({
603
- providerId,
604
- env = process.env,
605
- homeDirectory = homedir(),
606
- platform = process.platform,
607
- fetch
608
- }) {
609
- if (!isSubscriptionProvider(providerId)) return void 0;
610
- const stored = await readStore({ env, homeDirectory, platform });
611
- if (stored == null) return void 0;
612
- const candidate = stored.value[providerId];
613
- if (!isRecord(candidate)) return void 0;
614
- if (candidate.type === "api") {
615
- return typeof candidate.key === "string" && candidate.key.length > 0 ? { providerId, accessToken: candidate.key } : void 0;
616
- }
617
- if (candidate.type !== "oauth") return void 0;
618
- if (providerId === "opencode-go") return void 0;
619
- const access = candidate.access;
620
- const refresh = candidate.refresh;
621
- const expires = candidate.expires;
622
- if (typeof access !== "string" || typeof refresh !== "string" || typeof expires !== "number") {
623
- return void 0;
624
- }
625
- if (providerId === "github-copilot") {
626
- return {
627
- providerId,
628
- accessToken: refresh,
629
- ...typeof candidate.enterpriseUrl === "string" ? { enterpriseUrl: candidate.enterpriseUrl } : {}
630
- };
631
- }
632
- if (providerId === "poe") {
633
- if (isAccessTokenExpiringSoon({ expiresAt: expires })) {
634
- throw new Error(
635
- "OpenCode Poe subscription API key is expiring soon. Run OpenCode login again."
636
- );
637
- }
638
- return { providerId, accessToken: access };
639
- }
640
- let accessToken = access;
641
- let accountId = typeof candidate.accountId === "string" ? candidate.accountId : providerId === "openai" ? await extractOpenAIAccountId(access) : void 0;
642
- if (isAccessTokenExpiringSoon({ expiresAt: expires })) {
643
- const refreshSettings = resolveRefreshSettings({
644
- providerId,
645
- candidate,
646
- env
647
- });
648
- const refreshed = await refreshOAuthAccessToken({
649
- ...refreshSettings,
650
- refreshToken: refresh,
651
- ...fetch == null ? {} : { fetch }
652
- });
653
- accessToken = refreshed.accessToken;
654
- if (providerId === "openai") {
655
- accountId = await extractOpenAIAccountId(refreshed.accessToken) ?? accountId;
656
- }
657
- stored.value[providerId] = {
658
- ...candidate,
659
- access: refreshed.accessToken,
660
- refresh: refreshed.refreshToken ?? refresh,
661
- expires: refreshed.expiresAt,
662
- ...accountId == null ? {} : { accountId }
663
- };
664
- await stored.write?.(stored.value);
665
- }
666
- return {
667
- providerId,
668
- accessToken,
669
- ...accountId == null ? {} : { accountId },
670
- ...typeof candidate.enterpriseUrl === "string" ? { enterpriseUrl: candidate.enterpriseUrl } : {}
671
- };
436
+ async function readOpenCodeSubscription({ providerId, env = process.env, homeDirectory = homedir(), platform = process.platform, fetch }) {
437
+ if (!isSubscriptionProvider(providerId)) return void 0;
438
+ const stored = await readStore({
439
+ env,
440
+ homeDirectory,
441
+ platform
442
+ });
443
+ if (stored == null) return void 0;
444
+ const candidate = stored.value[providerId];
445
+ if (!isRecord(candidate)) return void 0;
446
+ if (candidate.type === "api") return typeof candidate.key === "string" && candidate.key.length > 0 ? {
447
+ providerId,
448
+ accessToken: candidate.key
449
+ } : void 0;
450
+ if (candidate.type !== "oauth") return void 0;
451
+ if (providerId === "opencode-go") return void 0;
452
+ const access = candidate.access;
453
+ const refresh = candidate.refresh;
454
+ const expires = candidate.expires;
455
+ if (typeof access !== "string" || typeof refresh !== "string" || typeof expires !== "number") return;
456
+ if (providerId === "github-copilot") return {
457
+ providerId,
458
+ accessToken: refresh,
459
+ ...typeof candidate.enterpriseUrl === "string" ? { enterpriseUrl: candidate.enterpriseUrl } : {}
460
+ };
461
+ if (providerId === "poe") {
462
+ if (isAccessTokenExpiringSoon({ expiresAt: expires })) throw new Error("OpenCode Poe subscription API key is expiring soon. Run OpenCode login again.");
463
+ return {
464
+ providerId,
465
+ accessToken: access
466
+ };
467
+ }
468
+ let accessToken = access;
469
+ let accountId = typeof candidate.accountId === "string" ? candidate.accountId : providerId === "openai" ? await extractOpenAIAccountId(access) : void 0;
470
+ if (isAccessTokenExpiringSoon({ expiresAt: expires })) {
471
+ const refreshSettings = resolveRefreshSettings({
472
+ providerId,
473
+ candidate,
474
+ env
475
+ });
476
+ const refreshed = await refreshOAuthAccessToken({
477
+ ...refreshSettings,
478
+ refreshToken: refresh,
479
+ ...fetch == null ? {} : { fetch }
480
+ });
481
+ accessToken = refreshed.accessToken;
482
+ if (providerId === "openai") accountId = await extractOpenAIAccountId(refreshed.accessToken) ?? accountId;
483
+ stored.value[providerId] = {
484
+ ...candidate,
485
+ access: refreshed.accessToken,
486
+ refresh: refreshed.refreshToken ?? refresh,
487
+ expires: refreshed.expiresAt,
488
+ ...accountId == null ? {} : { accountId }
489
+ };
490
+ await stored.write?.(stored.value);
491
+ }
492
+ return {
493
+ providerId,
494
+ accessToken,
495
+ ...accountId == null ? {} : { accountId },
496
+ ...typeof candidate.enterpriseUrl === "string" ? { enterpriseUrl: candidate.enterpriseUrl } : {}
497
+ };
672
498
  }
673
- async function requestOpenCodeGitLabDirectAccess({
674
- accessToken,
675
- instanceUrl = "https://gitlab.com",
676
- aiGatewayUrl = GITLAB_AI_GATEWAY_URL,
677
- fetch: fetchImplementation = globalThis.fetch
678
- }) {
679
- const response = await fetchImplementation(
680
- `${instanceUrl.replace(/\/+$/, "")}/api/v4/ai/third_party_agents/direct_access`,
681
- {
682
- method: "POST",
683
- headers: {
684
- Authorization: `Bearer ${accessToken}`,
685
- "Content-Type": "application/json"
686
- },
687
- body: JSON.stringify({
688
- feature_flags: { DuoAgentPlatformNext: true }
689
- })
690
- }
691
- );
692
- if (!response.ok) {
693
- throw new Error(
694
- `OpenCode GitLab direct access request failed with status ${response.status}.`
695
- );
696
- }
697
- const parsed = await safeParseJSON({ text: await response.text() });
698
- if (!parsed.success || !isRecord(parsed.value)) {
699
- throw new Error("OpenCode GitLab direct access returned invalid JSON.");
700
- }
701
- const token = parsed.value.token;
702
- const headers = parsed.value.headers;
703
- if (typeof token !== "string" || token.length === 0 || !isRecord(headers) || Object.values(headers).some((value) => typeof value !== "string")) {
704
- throw new Error(
705
- "OpenCode GitLab direct access returned invalid credentials."
706
- );
707
- }
708
- return {
709
- accessToken: token,
710
- headers,
711
- aiGatewayUrl: aiGatewayUrl.replace(/\/+$/, "")
712
- };
499
+ async function requestOpenCodeGitLabDirectAccess({ accessToken, instanceUrl = "https://gitlab.com", aiGatewayUrl = GITLAB_AI_GATEWAY_URL, fetch: fetchImplementation = globalThis.fetch }) {
500
+ const response = await fetchImplementation(`${instanceUrl.replace(/\/+$/, "")}/api/v4/ai/third_party_agents/direct_access`, {
501
+ method: "POST",
502
+ headers: {
503
+ Authorization: `Bearer ${accessToken}`,
504
+ "Content-Type": "application/json"
505
+ },
506
+ body: JSON.stringify({ feature_flags: { DuoAgentPlatformNext: true } })
507
+ });
508
+ if (!response.ok) throw new Error(`OpenCode GitLab direct access request failed with status ${response.status}.`);
509
+ const parsed = await safeParseJSON({ text: await response.text() });
510
+ if (!parsed.success || !isRecord(parsed.value)) throw new Error("OpenCode GitLab direct access returned invalid JSON.");
511
+ const token = parsed.value.token;
512
+ const headers = parsed.value.headers;
513
+ if (typeof token !== "string" || token.length === 0 || !isRecord(headers) || Object.values(headers).some((value) => typeof value !== "string")) throw new Error("OpenCode GitLab direct access returned invalid credentials.");
514
+ return {
515
+ accessToken: token,
516
+ headers,
517
+ aiGatewayUrl: aiGatewayUrl.replace(/\/+$/, "")
518
+ };
713
519
  }
714
- function createOpenCodeGitLabSubscriptionRequestTransformations({
715
- directAccess,
716
- sandboxAccessToken
717
- }) {
718
- const headers = Object.fromEntries(
719
- Object.entries(directAccess.headers).filter(
720
- ([name]) => name.toLowerCase() !== "authorization" && name.toLowerCase() !== "x-api-key"
721
- )
722
- );
723
- const transformHeaders = {
724
- ...headers,
725
- Authorization: `Bearer ${directAccess.accessToken}`
726
- };
727
- return [
728
- `${directAccess.aiGatewayUrl}/ai/v1/proxy/anthropic`,
729
- `${directAccess.aiGatewayUrl}/ai/v1/proxy/openai/v1`
730
- ].map(
731
- (matchUrl) => createCredentialRequestTransformation2({
732
- matchUrl,
733
- matchHeaders: {
734
- Authorization: `Bearer ${sandboxAccessToken}`
735
- },
736
- transformHeaders
737
- })
738
- );
520
+ function createOpenCodeGitLabSubscriptionRequestTransformations({ directAccess, sandboxAccessToken }) {
521
+ const transformHeaders = {
522
+ ...Object.fromEntries(Object.entries(directAccess.headers).filter(([name]) => name.toLowerCase() !== "authorization" && name.toLowerCase() !== "x-api-key")),
523
+ Authorization: `Bearer ${directAccess.accessToken}`
524
+ };
525
+ return [`${directAccess.aiGatewayUrl}/ai/v1/proxy/anthropic`, `${directAccess.aiGatewayUrl}/ai/v1/proxy/openai/v1`].map((matchUrl) => createCredentialRequestTransformation({
526
+ matchUrl,
527
+ matchHeaders: { Authorization: `Bearer ${sandboxAccessToken}` },
528
+ transformHeaders
529
+ }));
739
530
  }
740
- function createOpenCodeGitLabSubscriptionConfig({
741
- openCodeConfig,
742
- sandboxAccessToken,
743
- aiGatewayUrl,
744
- headers
745
- }) {
746
- const providerOptions = headers == null ? {} : { headers };
747
- const providers = {
748
- [GITLAB_ANTHROPIC_PROVIDER_ID]: {
749
- name: "GitLab Anthropic",
750
- npm: "@ai-sdk/anthropic",
751
- api: `${aiGatewayUrl}/ai/v1/proxy/anthropic/v1`,
752
- options: { authToken: sandboxAccessToken, ...providerOptions },
753
- models: createOpenCodeGitLabModels(GITLAB_ANTHROPIC_PROVIDER_ID)
754
- },
755
- [GITLAB_OPENAI_CHAT_PROVIDER_ID]: {
756
- name: "GitLab OpenAI Chat",
757
- npm: "@ai-sdk/openai-compatible",
758
- api: `${aiGatewayUrl}/ai/v1/proxy/openai/v1`,
759
- options: { apiKey: sandboxAccessToken, ...providerOptions },
760
- models: createOpenCodeGitLabModels(GITLAB_OPENAI_CHAT_PROVIDER_ID)
761
- },
762
- [GITLAB_OPENAI_RESPONSES_PROVIDER_ID]: {
763
- name: "GitLab OpenAI Responses",
764
- npm: "@ai-sdk/openai",
765
- api: `${aiGatewayUrl}/ai/v1/proxy/openai/v1`,
766
- options: { apiKey: sandboxAccessToken, ...providerOptions },
767
- models: createOpenCodeGitLabModels(GITLAB_OPENAI_RESPONSES_PROVIDER_ID)
768
- }
769
- };
770
- const configuredProviders = isRecord(openCodeConfig?.provider) ? openCodeConfig.provider : {};
771
- return {
772
- ...openCodeConfig,
773
- provider: { ...configuredProviders, ...providers }
774
- };
531
+ function createOpenCodeGitLabSubscriptionConfig({ openCodeConfig, sandboxAccessToken, aiGatewayUrl, headers }) {
532
+ const providerOptions = headers == null ? {} : { headers };
533
+ const providers = {
534
+ [GITLAB_ANTHROPIC_PROVIDER_ID]: {
535
+ name: "GitLab Anthropic",
536
+ npm: "@ai-sdk/anthropic",
537
+ api: `${aiGatewayUrl}/ai/v1/proxy/anthropic/v1`,
538
+ options: {
539
+ authToken: sandboxAccessToken,
540
+ ...providerOptions
541
+ },
542
+ models: createOpenCodeGitLabModels(GITLAB_ANTHROPIC_PROVIDER_ID)
543
+ },
544
+ [GITLAB_OPENAI_CHAT_PROVIDER_ID]: {
545
+ name: "GitLab OpenAI Chat",
546
+ npm: "@ai-sdk/openai-compatible",
547
+ api: `${aiGatewayUrl}/ai/v1/proxy/openai/v1`,
548
+ options: {
549
+ apiKey: sandboxAccessToken,
550
+ ...providerOptions
551
+ },
552
+ models: createOpenCodeGitLabModels(GITLAB_OPENAI_CHAT_PROVIDER_ID)
553
+ },
554
+ [GITLAB_OPENAI_RESPONSES_PROVIDER_ID]: {
555
+ name: "GitLab OpenAI Responses",
556
+ npm: "@ai-sdk/openai",
557
+ api: `${aiGatewayUrl}/ai/v1/proxy/openai/v1`,
558
+ options: {
559
+ apiKey: sandboxAccessToken,
560
+ ...providerOptions
561
+ },
562
+ models: createOpenCodeGitLabModels(GITLAB_OPENAI_RESPONSES_PROVIDER_ID)
563
+ }
564
+ };
565
+ const configuredProviders = isRecord(openCodeConfig?.provider) ? openCodeConfig.provider : {};
566
+ return {
567
+ ...openCodeConfig,
568
+ provider: {
569
+ ...configuredProviders,
570
+ ...providers
571
+ }
572
+ };
775
573
  }
776
- function resolveOpenCodeGitLabSubscriptionModel({
777
- model,
778
- provider
779
- }) {
780
- if (model == null) {
781
- if (provider === "gitlab") {
782
- throw new HarnessCapabilityUnsupportedError({
783
- harnessId: "opencode",
784
- message: "Harness 'opencode' requires an explicit GitLab model when native GitLab subscription credentials are brokered."
785
- });
786
- }
787
- return void 0;
788
- }
789
- const modelProvider = model.includes("/") ? model.split("/")[0] : provider;
790
- if (modelProvider !== "gitlab") return model;
791
- const modelId = model.includes("/") ? model.slice(model.indexOf("/") + 1) : model;
792
- const mapping = OPEN_CODE_GITLAB_MODELS[modelId];
793
- if (mapping == null) {
794
- throw new HarnessCapabilityUnsupportedError({
795
- harnessId: "opencode",
796
- message: `Harness 'opencode' cannot broker native GitLab subscription credentials for unknown model '${modelId}'.`
797
- });
798
- }
799
- if (mapping.providerId === "workflow") {
800
- throw new HarnessCapabilityUnsupportedError({
801
- harnessId: "opencode",
802
- message: `Harness 'opencode' cannot broker native GitLab subscription credentials for workflow model '${modelId}' because it authenticates over WebSocket. Use a sandbox without credential brokering to use the credential-forwarding fallback.`
803
- });
804
- }
805
- return `${mapping.providerId}/${modelId}`;
574
+ function resolveOpenCodeGitLabSubscriptionModel({ model, provider }) {
575
+ if (model == null) {
576
+ if (provider === "gitlab") throw new HarnessCapabilityUnsupportedError({
577
+ harnessId: "opencode",
578
+ message: "Harness 'opencode' requires an explicit GitLab model when native GitLab subscription credentials are brokered."
579
+ });
580
+ return;
581
+ }
582
+ if ((model.includes("/") ? model.split("/")[0] : provider) !== "gitlab") return model;
583
+ const modelId = model.includes("/") ? model.slice(model.indexOf("/") + 1) : model;
584
+ const mapping = OPEN_CODE_GITLAB_MODELS[modelId];
585
+ if (mapping == null) throw new HarnessCapabilityUnsupportedError({
586
+ harnessId: "opencode",
587
+ message: `Harness 'opencode' cannot broker native GitLab subscription credentials for unknown model '${modelId}'.`
588
+ });
589
+ if (mapping.providerId === "workflow") throw new HarnessCapabilityUnsupportedError({
590
+ harnessId: "opencode",
591
+ message: `Harness 'opencode' cannot broker native GitLab subscription credentials for workflow model '${modelId}' because it authenticates over WebSocket. Use a sandbox without credential brokering to use the credential-forwarding fallback.`
592
+ });
593
+ return `${mapping.providerId}/${modelId}`;
806
594
  }
807
595
  function createOpenCodeGitLabModels(providerId) {
808
- return Object.fromEntries(
809
- Object.entries(OPEN_CODE_GITLAB_MODELS).flatMap(
810
- ([id, model]) => model.providerId === providerId ? [[id, { id: model.modelId, name: id }]] : []
811
- )
812
- );
596
+ return Object.fromEntries(Object.entries(OPEN_CODE_GITLAB_MODELS).flatMap(([id, model]) => model.providerId === providerId ? [[id, {
597
+ id: model.modelId,
598
+ name: id
599
+ }]] : []));
813
600
  }
814
- function createOpenCodeSubscriptionAuthContent({
815
- authentication,
816
- accessToken
817
- }) {
818
- const { providerId } = authentication;
819
- const value = providerId === "opencode-go" ? { type: "api", key: accessToken } : {
820
- type: "oauth",
821
- access: accessToken,
822
- refresh: providerId === "github-copilot" ? accessToken : "host-managed",
823
- expires: Number.MAX_SAFE_INTEGER,
824
- ...authentication.accountId == null ? {} : { accountId: authentication.accountId },
825
- ...authentication.enterpriseUrl == null ? {} : { enterpriseUrl: authentication.enterpriseUrl }
826
- };
827
- return JSON.stringify({ [providerId]: value });
601
+ function createOpenCodeSubscriptionAuthContent({ authentication, accessToken }) {
602
+ const { providerId } = authentication;
603
+ const value = providerId === "opencode-go" ? {
604
+ type: "api",
605
+ key: accessToken
606
+ } : {
607
+ type: "oauth",
608
+ access: accessToken,
609
+ refresh: providerId === "github-copilot" ? accessToken : "host-managed",
610
+ expires: Number.MAX_SAFE_INTEGER,
611
+ ...authentication.accountId == null ? {} : { accountId: authentication.accountId },
612
+ ...authentication.enterpriseUrl == null ? {} : { enterpriseUrl: authentication.enterpriseUrl }
613
+ };
614
+ return JSON.stringify({ [providerId]: value });
828
615
  }
829
- function createOpenCodeSubscriptionRequestTransformations({
830
- authentication,
831
- sandboxAccessToken
832
- }) {
833
- const matchUrl = resolveRequestUrl(authentication);
834
- const transformHeaders = {
835
- Authorization: `Bearer ${authentication.accessToken}`,
836
- ...authentication.providerId === "openai" && authentication.accountId ? { "ChatGPT-Account-Id": authentication.accountId } : {}
837
- };
838
- const transformations = [
839
- createCredentialRequestTransformation2({
840
- matchUrl,
841
- matchHeaders: {
842
- Authorization: `Bearer ${sandboxAccessToken}`
843
- },
844
- transformHeaders
845
- })
846
- ];
847
- if (authentication.providerId === "poe" || authentication.providerId === "opencode-go") {
848
- transformations.push(
849
- createCredentialRequestTransformation2({
850
- matchUrl,
851
- matchHeaders: { "x-api-key": sandboxAccessToken },
852
- transformHeaders: { "x-api-key": authentication.accessToken }
853
- })
854
- );
855
- }
856
- return transformations;
616
+ function createOpenCodeSubscriptionRequestTransformations({ authentication, sandboxAccessToken }) {
617
+ const matchUrl = resolveRequestUrl(authentication);
618
+ const transformHeaders = {
619
+ Authorization: `Bearer ${authentication.accessToken}`,
620
+ ...authentication.providerId === "openai" && authentication.accountId ? { "ChatGPT-Account-Id": authentication.accountId } : {}
621
+ };
622
+ const transformations = [createCredentialRequestTransformation({
623
+ matchUrl,
624
+ matchHeaders: { Authorization: `Bearer ${sandboxAccessToken}` },
625
+ transformHeaders
626
+ })];
627
+ if (authentication.providerId === "poe" || authentication.providerId === "opencode-go") transformations.push(createCredentialRequestTransformation({
628
+ matchUrl,
629
+ matchHeaders: { "x-api-key": sandboxAccessToken },
630
+ transformHeaders: { "x-api-key": authentication.accessToken }
631
+ }));
632
+ return transformations;
857
633
  }
858
- function resolveRefreshSettings({
859
- providerId,
860
- candidate,
861
- env
862
- }) {
863
- if (providerId === "openai") {
864
- return {
865
- tokenUrl: "https://auth.openai.com/oauth/token",
866
- clientId: OPENAI_CLIENT_ID
867
- };
868
- }
869
- if (providerId === "xai") {
870
- return {
871
- tokenUrl: "https://auth.x.ai/oauth2/token",
872
- clientId: XAI_CLIENT_ID
873
- };
874
- }
875
- const instanceUrl = (typeof candidate.enterpriseUrl === "string" ? candidate.enterpriseUrl : env.GITLAB_INSTANCE_URL) ?? "https://gitlab.com";
876
- return {
877
- tokenUrl: `${instanceUrl.replace(/\/+$/, "")}/oauth/token`,
878
- clientId: env.GITLAB_OAUTH_CLIENT_ID ?? GITLAB_CLIENT_ID
879
- };
634
+ function resolveRefreshSettings({ providerId, candidate, env }) {
635
+ if (providerId === "openai") return {
636
+ tokenUrl: "https://auth.openai.com/oauth/token",
637
+ clientId: OPENAI_CLIENT_ID
638
+ };
639
+ if (providerId === "xai") return {
640
+ tokenUrl: "https://auth.x.ai/oauth2/token",
641
+ clientId: XAI_CLIENT_ID
642
+ };
643
+ return {
644
+ tokenUrl: `${((typeof candidate.enterpriseUrl === "string" ? candidate.enterpriseUrl : env.GITLAB_INSTANCE_URL) ?? "https://gitlab.com").replace(/\/+$/, "")}/oauth/token`,
645
+ clientId: env.GITLAB_OAUTH_CLIENT_ID ?? GITLAB_CLIENT_ID
646
+ };
880
647
  }
881
648
  function resolveRequestUrl(authentication) {
882
- switch (authentication.providerId) {
883
- case "openai":
884
- return "https://chatgpt.com/backend-api/codex";
885
- case "xai":
886
- return "https://api.x.ai/v1";
887
- case "github-copilot":
888
- return authentication.enterpriseUrl == null ? "https://api.githubcopilot.com" : `https://copilot-api.${normalizeDomain(authentication.enterpriseUrl)}`;
889
- case "poe":
890
- return "https://api.poe.com";
891
- case "opencode-go":
892
- return "https://opencode.ai/zen/go/v1";
893
- case "gitlab":
894
- return authentication.enterpriseUrl ?? "https://gitlab.com";
895
- }
649
+ switch (authentication.providerId) {
650
+ case "openai": return "https://chatgpt.com/backend-api/codex";
651
+ case "xai": return "https://api.x.ai/v1";
652
+ case "github-copilot": return authentication.enterpriseUrl == null ? "https://api.githubcopilot.com" : `https://copilot-api.${normalizeDomain(authentication.enterpriseUrl)}`;
653
+ case "poe": return "https://api.poe.com";
654
+ case "opencode-go": return "https://opencode.ai/zen/go/v1";
655
+ case "gitlab": return authentication.enterpriseUrl ?? "https://gitlab.com";
656
+ }
896
657
  }
897
658
  function normalizeDomain(value) {
898
- try {
899
- return new URL(value.includes("://") ? value : `https://${value}`).host;
900
- } catch {
901
- return value.replace(/^https?:\/\//, "").replace(/\/+$/, "");
902
- }
659
+ try {
660
+ return new URL(value.includes("://") ? value : `https://${value}`).host;
661
+ } catch {
662
+ return value.replace(/^https?:\/\//, "").replace(/\/+$/, "");
663
+ }
903
664
  }
904
665
  async function extractOpenAIAccountId(accessToken) {
905
- const payload = await parseJwtPayload({ token: accessToken });
906
- const auth = payload?.["https://api.openai.com/auth"];
907
- if (!isRecord(auth)) return void 0;
908
- const accountId = auth.chatgpt_account_id;
909
- return typeof accountId === "string" ? accountId : void 0;
666
+ const auth = (await parseJwtPayload({ token: accessToken }))?.["https://api.openai.com/auth"];
667
+ if (!isRecord(auth)) return void 0;
668
+ const accountId = auth.chatgpt_account_id;
669
+ return typeof accountId === "string" ? accountId : void 0;
910
670
  }
911
- async function readStore({
912
- env,
913
- homeDirectory,
914
- platform
915
- }) {
916
- if (env.OPENCODE_AUTH_CONTENT != null) {
917
- const parsed2 = await safeParseJSON({ text: env.OPENCODE_AUTH_CONTENT });
918
- return parsed2.success && isRecord(parsed2.value) ? { value: parsed2.value } : void 0;
919
- }
920
- const authPath = env.XDG_DATA_HOME != null ? join(env.XDG_DATA_HOME, "opencode", "auth.json") : isWindows(platform) ? join(homeDirectory, ".opencode", "auth.json") : join(homeDirectory, ".local", "share", "opencode", "auth.json");
921
- const text = await readFile(authPath, "utf8").catch(() => void 0);
922
- if (text == null) return void 0;
923
- const parsed = await safeParseJSON({ text });
924
- if (!parsed.success || !isRecord(parsed.value)) return void 0;
925
- return {
926
- value: parsed.value,
927
- write: (value) => writeStore({ authPath, value })
928
- };
671
+ async function readStore({ env, homeDirectory, platform }) {
672
+ if (env.OPENCODE_AUTH_CONTENT != null) {
673
+ const parsed = await safeParseJSON({ text: env.OPENCODE_AUTH_CONTENT });
674
+ return parsed.success && isRecord(parsed.value) ? { value: parsed.value } : void 0;
675
+ }
676
+ const authPath = env.XDG_DATA_HOME != null ? join(env.XDG_DATA_HOME, "opencode", "auth.json") : isWindows(platform) ? join(homeDirectory, ".opencode", "auth.json") : join(homeDirectory, ".local", "share", "opencode", "auth.json");
677
+ const text = await readFile(authPath, "utf8").catch(() => void 0);
678
+ if (text == null) return void 0;
679
+ const parsed = await safeParseJSON({ text });
680
+ if (!parsed.success || !isRecord(parsed.value)) return void 0;
681
+ return {
682
+ value: parsed.value,
683
+ write: (value) => writeStore({
684
+ authPath,
685
+ value
686
+ })
687
+ };
929
688
  }
930
- async function writeStore({
931
- authPath,
932
- value
933
- }) {
934
- await mkdir(dirname(authPath), { recursive: true });
935
- const temporaryPath = `${authPath}.${process.pid}.tmp`;
936
- await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}
937
- `, {
938
- mode: 384
939
- });
940
- await chmod(temporaryPath, 384);
941
- await rename(temporaryPath, authPath);
689
+ async function writeStore({ authPath, value }) {
690
+ await mkdir(dirname(authPath), { recursive: true });
691
+ const temporaryPath = `${authPath}.${process.pid}.tmp`;
692
+ await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
693
+ await chmod(temporaryPath, 384);
694
+ await rename(temporaryPath, authPath);
942
695
  }
943
696
  function isSubscriptionProvider(value) {
944
- return value === "openai" || value === "xai" || value === "github-copilot" || value === "poe" || value === "opencode-go" || value === "gitlab";
697
+ return value === "openai" || value === "xai" || value === "github-copilot" || value === "poe" || value === "opencode-go" || value === "gitlab";
945
698
  }
946
-
947
- // src/opencode-bridge-protocol.ts
948
- import {
949
- harnessV1BridgeInboundCommandSchemas,
950
- harnessV1BridgeOutboundMessageSchema,
951
- harnessV1BridgeReadySchema,
952
- harnessV1BridgeStartBaseSchema
953
- } from "@ai-sdk/harness";
954
- import { z } from "zod/v4";
955
- var outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
956
- var startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
957
- operation: z.enum(["prompt", "compact"]).optional(),
958
- provider: z.string().optional(),
959
- variant: z.string().optional(),
960
- instructions: z.string().optional(),
961
- skillsChanged: z.boolean().optional(),
962
- resumeSessionId: z.string().optional(),
963
- openCodeConfig: z.record(z.string(), z.unknown()).optional(),
964
- mcpServers: z.record(z.string(), z.unknown()).optional(),
965
- headers: z.record(z.string(), z.string()).optional()
699
+ //#endregion
700
+ //#region src/opencode-bridge-protocol.ts
701
+ const outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
702
+ const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
703
+ operation: z.enum(["prompt", "compact"]).optional(),
704
+ provider: z.string().optional(),
705
+ variant: z.string().optional(),
706
+ instructions: z.string().optional(),
707
+ skillsChanged: z.boolean().optional(),
708
+ resumeSessionId: z.string().optional(),
709
+ openCodeConfig: z.record(z.string(), z.unknown()).optional(),
710
+ mcpServers: z.record(z.string(), z.unknown()).optional(),
711
+ headers: z.record(z.string(), z.string()).optional()
966
712
  });
967
- var inboundMessageSchema = z.discriminatedUnion("type", [
968
- startMessageSchema,
969
- ...harnessV1BridgeInboundCommandSchemas
970
- ]);
971
-
972
- // src/version.ts
973
- var VERSION = true ? "1.0.134" : "0.0.0-test";
974
-
975
- // src/opencode-harness.ts
976
- var OPENCODE_CLIENT_APP = `ai-sdk-harness-opencode/${VERSION}`;
977
- var optionalStringRecord = z2.record(z2.string(), z2.unknown()).optional();
978
- var OPENCODE_BUILTIN_TOOLS = {
979
- askUserQuestions: {
980
- ...HARNESS_V1_BUILTIN_TOOLS.askUserQuestions,
981
- nativeName: "question",
982
- toolUseKind: "readonly"
983
- },
984
- read: commonTool("read", {
985
- nativeName: "view",
986
- toolUseKind: "readonly",
987
- description: "Read file contents",
988
- inputSchema: z2.looseObject({
989
- file_path: z2.string().optional(),
990
- path: z2.string().optional()
991
- })
992
- }),
993
- write: commonTool("write", {
994
- nativeName: "write",
995
- toolUseKind: "edit",
996
- description: "Write content to a file",
997
- inputSchema: z2.looseObject({
998
- file_path: z2.string().optional(),
999
- path: z2.string().optional(),
1000
- content: z2.string().optional()
1001
- })
1002
- }),
1003
- edit: commonTool("edit", {
1004
- nativeName: "edit",
1005
- toolUseKind: "edit",
1006
- description: "Edit a file by replacing text",
1007
- inputSchema: z2.looseObject({
1008
- file_path: z2.string().optional(),
1009
- path: z2.string().optional(),
1010
- old_string: z2.string().optional(),
1011
- new_string: z2.string().optional()
1012
- })
1013
- }),
1014
- bash: commonTool("bash", {
1015
- nativeName: "bash",
1016
- toolUseKind: "bash",
1017
- description: "Execute a shell command",
1018
- inputSchema: z2.looseObject({
1019
- command: z2.string().optional()
1020
- })
1021
- }),
1022
- glob: commonTool("glob", {
1023
- nativeName: "glob",
1024
- toolUseKind: "readonly",
1025
- description: "Find files matching a glob pattern",
1026
- inputSchema: z2.looseObject({
1027
- pattern: z2.string().optional(),
1028
- path: z2.string().optional()
1029
- })
1030
- }),
1031
- grep: commonTool("grep", {
1032
- nativeName: "grep",
1033
- toolUseKind: "readonly",
1034
- description: "Search file contents with regex",
1035
- inputSchema: z2.looseObject({
1036
- pattern: z2.string().optional(),
1037
- path: z2.string().optional()
1038
- })
1039
- }),
1040
- ls: tool({
1041
- description: "List directory contents",
1042
- inputSchema: z2.looseObject({
1043
- path: z2.string().optional()
1044
- })
1045
- }),
1046
- webfetch: tool({
1047
- description: "Fetch a URL",
1048
- inputSchema: z2.looseObject({
1049
- url: z2.string().optional(),
1050
- prompt: z2.string().optional()
1051
- })
1052
- }),
1053
- skill: tool({
1054
- description: "Load an OpenCode skill by name",
1055
- inputSchema: z2.looseObject({
1056
- name: z2.string().optional()
1057
- })
1058
- }),
1059
- todowrite: tool({
1060
- description: "Replace the OpenCode session todo list",
1061
- inputSchema: z2.looseObject({
1062
- todos: z2.array(
1063
- z2.looseObject({
1064
- content: z2.string().optional(),
1065
- status: z2.string().optional(),
1066
- priority: z2.string().optional()
1067
- })
1068
- ).optional()
1069
- })
1070
- }),
1071
- agent: tool({
1072
- description: "Run an OpenCode subagent",
1073
- inputSchema: z2.looseObject({
1074
- agent: z2.string().optional(),
1075
- prompt: z2.string().optional(),
1076
- description: z2.string().optional(),
1077
- metadata: optionalStringRecord
1078
- })
1079
- })
713
+ z.discriminatedUnion("type", [startMessageSchema, ...harnessV1BridgeInboundCommandSchemas]);
714
+ //#endregion
715
+ //#region src/version.ts
716
+ const VERSION = "1.0.136";
717
+ //#endregion
718
+ //#region src/opencode-harness.ts
719
+ /**
720
+ * Value to use in User-Agent and `x-client-app` headers.
721
+ */
722
+ const OPENCODE_CLIENT_APP = `ai-sdk-harness-opencode/${VERSION}`;
723
+ const optionalStringRecord = z.record(z.string(), z.unknown()).optional();
724
+ const OPENCODE_BUILTIN_TOOLS = {
725
+ askUserQuestions: {
726
+ ...HARNESS_V1_BUILTIN_TOOLS.askUserQuestions,
727
+ nativeName: "question",
728
+ toolUseKind: "readonly"
729
+ },
730
+ read: commonTool("read", {
731
+ nativeName: "view",
732
+ toolUseKind: "readonly",
733
+ description: "Read file contents",
734
+ inputSchema: z.looseObject({
735
+ file_path: z.string().optional(),
736
+ path: z.string().optional()
737
+ })
738
+ }),
739
+ write: commonTool("write", {
740
+ nativeName: "write",
741
+ toolUseKind: "edit",
742
+ description: "Write content to a file",
743
+ inputSchema: z.looseObject({
744
+ file_path: z.string().optional(),
745
+ path: z.string().optional(),
746
+ content: z.string().optional()
747
+ })
748
+ }),
749
+ edit: commonTool("edit", {
750
+ nativeName: "edit",
751
+ toolUseKind: "edit",
752
+ description: "Edit a file by replacing text",
753
+ inputSchema: z.looseObject({
754
+ file_path: z.string().optional(),
755
+ path: z.string().optional(),
756
+ old_string: z.string().optional(),
757
+ new_string: z.string().optional()
758
+ })
759
+ }),
760
+ bash: commonTool("bash", {
761
+ nativeName: "bash",
762
+ toolUseKind: "bash",
763
+ description: "Execute a shell command",
764
+ inputSchema: z.looseObject({ command: z.string().optional() })
765
+ }),
766
+ glob: commonTool("glob", {
767
+ nativeName: "glob",
768
+ toolUseKind: "readonly",
769
+ description: "Find files matching a glob pattern",
770
+ inputSchema: z.looseObject({
771
+ pattern: z.string().optional(),
772
+ path: z.string().optional()
773
+ })
774
+ }),
775
+ grep: commonTool("grep", {
776
+ nativeName: "grep",
777
+ toolUseKind: "readonly",
778
+ description: "Search file contents with regex",
779
+ inputSchema: z.looseObject({
780
+ pattern: z.string().optional(),
781
+ path: z.string().optional()
782
+ })
783
+ }),
784
+ ls: tool({
785
+ description: "List directory contents",
786
+ inputSchema: z.looseObject({ path: z.string().optional() })
787
+ }),
788
+ webfetch: tool({
789
+ description: "Fetch a URL",
790
+ inputSchema: z.looseObject({
791
+ url: z.string().optional(),
792
+ prompt: z.string().optional()
793
+ })
794
+ }),
795
+ skill: tool({
796
+ description: "Load an OpenCode skill by name",
797
+ inputSchema: z.looseObject({ name: z.string().optional() })
798
+ }),
799
+ todowrite: tool({
800
+ description: "Replace the OpenCode session todo list",
801
+ inputSchema: z.looseObject({ todos: z.array(z.looseObject({
802
+ content: z.string().optional(),
803
+ status: z.string().optional(),
804
+ priority: z.string().optional()
805
+ })).optional() })
806
+ }),
807
+ agent: tool({
808
+ description: "Run an OpenCode subagent",
809
+ inputSchema: z.looseObject({
810
+ agent: z.string().optional(),
811
+ prompt: z.string().optional(),
812
+ description: z.string().optional(),
813
+ metadata: optionalStringRecord
814
+ })
815
+ })
1080
816
  };
1081
- var openCodeBridgeCoordsSchema = z2.object({
1082
- port: z2.number(),
1083
- token: z2.string(),
1084
- lastSeenEventId: z2.number(),
1085
- sandboxId: z2.string().optional()
817
+ const openCodeBridgeCoordsSchema = z.object({
818
+ port: z.number(),
819
+ token: z.string(),
820
+ lastSeenEventId: z.number(),
821
+ sandboxId: z.string().optional()
1086
822
  });
1087
- var openCodeResumeStateSchema = z2.object({
1088
- openCodeSessionId: z2.string().optional(),
1089
- bridge: openCodeBridgeCoordsSchema.optional(),
1090
- sandboxCredentialEnvironment: z2.record(z2.string(), z2.string()).optional()
823
+ const openCodeResumeStateSchema = z.object({
824
+ openCodeSessionId: z.string().optional(),
825
+ bridge: openCodeBridgeCoordsSchema.optional(),
826
+ sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional()
1091
827
  });
1092
828
  function createOpenCode(settings = {}) {
1093
- if (settings.mcpServers != null && Object.prototype.hasOwnProperty.call(settings.mcpServers, "harness-tools")) {
1094
- throw new Error(
1095
- 'OpenCode MCP server name "harness-tools" is reserved for HarnessAgent tools.'
1096
- );
1097
- }
1098
- return {
1099
- specificationVersion: "harness-v1",
1100
- harnessId: "opencode",
1101
- builtinTools: OPENCODE_BUILTIN_TOOLS,
1102
- supportsBuiltinToolApprovals: true,
1103
- lifecycleStateSchema: openCodeResumeStateSchema,
1104
- getBootstrap: getOpenCodeBootstrap,
1105
- doStart: async (startOpts) => {
1106
- const sandboxSession = startOpts.sandboxSession;
1107
- const toolSafeSandboxSession = getRestrictedSandboxSession(sandboxSession);
1108
- const sandboxId = "id" in sandboxSession ? sandboxSession.id : void 0;
1109
- validateBasicSandboxSettings({
1110
- sandboxSession,
1111
- port: settings.port,
1112
- portEndpoint: settings.portEndpoint
1113
- });
1114
- if (settings.mintBridgeToken != null && sandboxId == null) {
1115
- throw new HarnessCapabilityUnsupportedError2({
1116
- harnessId: "opencode",
1117
- message: "The OpenCode harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id."
1118
- });
1119
- }
1120
- const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
1121
- const isResume = lifecycleState != null;
1122
- const isContinue = startOpts.continueFrom != null;
1123
- const resumeData = isResume && typeof lifecycleState?.data === "object" ? lifecycleState.data : void 0;
1124
- const resumeSessionId = typeof resumeData?.openCodeSessionId === "string" && resumeData.openCodeSessionId.length > 0 ? resumeData.openCodeSessionId : void 0;
1125
- const coords = resumeData?.bridge;
1126
- const authentication = await resolveOpenCodeAuthentication({
1127
- auth: settings.auth,
1128
- provider: settings.provider
1129
- });
1130
- const authenticationMode = authentication.authenticationMode;
1131
- const resolvedAuthEnvironment = authentication.environment;
1132
- let resolvedOpenCodeConfig = settings.openCodeConfig;
1133
- let transformModel;
1134
- let gitLabSubscriptionBrokered = false;
1135
- let sandboxAuthEnvironment = resolvedAuthEnvironment;
1136
- let sandboxCredentialEnvironment;
1137
- let credentialsBrokered = false;
1138
- if ("addRequestTransformations" in sandboxSession && sandboxSession.addRequestTransformations != null) {
1139
- sandboxCredentialEnvironment = resumeData?.sandboxCredentialEnvironment ?? await createSandboxCredentialEnvironment({
1140
- environment: resolvedAuthEnvironment,
1141
- credentialEnvironmentVariables: OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
1142
- credentialForwarding: settings.credentialForwarding
1143
- });
1144
- sandboxAuthEnvironment = {
1145
- ...resolvedAuthEnvironment,
1146
- ...sandboxCredentialEnvironment
1147
- };
1148
- const transformationSources = {
1149
- env: resolvedAuthEnvironment,
1150
- sandboxEnv: sandboxAuthEnvironment,
1151
- auth: authenticationMode
1152
- };
1153
- const sandboxSubscriptionAccessToken = sandboxAuthEnvironment[OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE];
1154
- let requestTransformations;
1155
- if (authentication.subscription?.providerId === "gitlab" && sandboxSubscriptionAccessToken != null) {
1156
- const directAccess = await requestOpenCodeGitLabDirectAccess({
1157
- accessToken: authentication.subscription.accessToken,
1158
- ...authentication.subscription.enterpriseUrl == null ? {} : { instanceUrl: authentication.subscription.enterpriseUrl },
1159
- ...process.env.GITLAB_AI_GATEWAY_URL == null ? {} : { aiGatewayUrl: process.env.GITLAB_AI_GATEWAY_URL }
1160
- });
1161
- requestTransformations = createOpenCodeGitLabSubscriptionRequestTransformations({
1162
- directAccess,
1163
- sandboxAccessToken: sandboxSubscriptionAccessToken
1164
- });
1165
- resolvedOpenCodeConfig = createOpenCodeGitLabSubscriptionConfig({
1166
- openCodeConfig: settings.openCodeConfig,
1167
- sandboxAccessToken: sandboxSubscriptionAccessToken,
1168
- aiGatewayUrl: directAccess.aiGatewayUrl,
1169
- ...startOpts.headers == null ? {} : { headers: startOpts.headers }
1170
- });
1171
- transformModel = (model) => resolveOpenCodeGitLabSubscriptionModel({
1172
- model,
1173
- provider: settings.provider
1174
- });
1175
- gitLabSubscriptionBrokered = true;
1176
- } else {
1177
- requestTransformations = authentication.subscription == null || sandboxSubscriptionAccessToken == null ? createOpenCodeRequestTransformations(transformationSources) : createOpenCodeSubscriptionRequestTransformations({
1178
- authentication: authentication.subscription,
1179
- sandboxAccessToken: sandboxSubscriptionAccessToken
1180
- });
1181
- }
1182
- if (requestTransformations.length > 0) {
1183
- await sandboxSession.addRequestTransformations(
1184
- requestTransformations
1185
- );
1186
- }
1187
- credentialsBrokered = true;
1188
- }
1189
- const sandboxHomeDir = await resolveSandboxHomeDir({
1190
- sandbox: toolSafeSandboxSession,
1191
- abortSignal: startOpts.abortSignal
1192
- });
1193
- const stateDir = harnessStateDirectoryPath({ sandboxHomeDir });
1194
- const bootstrapDir = path.posix.resolve(stateDir, OPENCODE_BOOTSTRAP_DIR);
1195
- const workDir = startOpts.sessionWorkDir;
1196
- const skillsDir = path.posix.join(sandboxHomeDir, ".agents", "skills");
1197
- const sessionDataDir = harnessSessionDataDirectoryPath({
1198
- stateDirectory: stateDir,
1199
- sessionId: startOpts.sessionId
1200
- });
1201
- const bridgeStateDir = `${sessionDataDir}/bridge`;
1202
- const timeoutMs = settings.startupTimeoutMs ?? 12e4;
1203
- const report = startOpts.observability?.report;
1204
- const onDiagnostic = report ? (frame) => report(
1205
- harnessV1DiagnosticFromBridgeFrame(frame, {
1206
- sessionId: startOpts.sessionId,
1207
- timestamp: Date.now()
1208
- })
1209
- ) : void 0;
1210
- const onBridgeError = createBridgeErrorHandler({
1211
- harnessId: "opencode",
1212
- sessionId: startOpts.sessionId
1213
- });
1214
- if (coords) {
1215
- try {
1216
- const endpoint2 = await resolveBridgeEndpoint({
1217
- sandboxSession,
1218
- override: settings.portEndpoint,
1219
- port: coords.port
1220
- });
1221
- const attachEndpoint = withBridgeToken({
1222
- endpoint: endpoint2,
1223
- token: coords.token
1224
- });
1225
- let supportsUserMessageResponses2 = false;
1226
- const attachChannel = new SandboxChannel({
1227
- connect: ({ abortSignal }) => openWebSocket({
1228
- endpoint: attachEndpoint,
1229
- helloTimeoutMs: Math.min(timeoutMs, 5e3),
1230
- abortSignal,
1231
- onHello: (supported) => {
1232
- supportsUserMessageResponses2 = supported;
1233
- }
1234
- }),
1235
- outboundSchema: outboundMessageSchema,
1236
- initialLastSeenEventId: coords.lastSeenEventId,
1237
- onDiagnostic,
1238
- onBridgeError,
1239
- reconnect: settings.reconnect
1240
- });
1241
- await attachChannel.open(isContinue ? { resume: true } : void 0);
1242
- return createSession({
1243
- sessionId: startOpts.sessionId,
1244
- channel: attachChannel,
1245
- proc: void 0,
1246
- provider: settings.provider,
1247
- reasoningVariant: settings.reasoningVariant,
1248
- openCodeConfig: resolvedOpenCodeConfig,
1249
- mcpServers: settings.mcpServers,
1250
- headers: startOpts.headers,
1251
- openCodeSessionId: resumeSessionId,
1252
- isResume: true,
1253
- seedResumeSessionOnFirstPrompt: false,
1254
- rerunContinue: false,
1255
- bridgePort: coords.port,
1256
- bridgeToken: coords.token,
1257
- sandboxId,
1258
- sandboxCredentialEnvironment,
1259
- debug: startOpts.observability?.debug,
1260
- permissionMode: startOpts.permissionMode,
1261
- builtinToolFiltering: startOpts.builtinToolFiltering,
1262
- sandbox: toolSafeSandboxSession,
1263
- sandboxHomeDir,
1264
- transformModel,
1265
- supportsUserMessageResponses: () => supportsUserMessageResponses2
1266
- });
1267
- } catch {
1268
- }
1269
- }
1270
- let respawnStrategy = isResume ? "rerun" : void 0;
1271
- if (coords && isContinue) {
1272
- const logRaw = await Promise.resolve(
1273
- toolSafeSandboxSession.readTextFile({
1274
- path: `${bridgeStateDir}/event-log.ndjson`,
1275
- abortSignal: startOpts.abortSignal
1276
- })
1277
- ).catch(() => null);
1278
- if (await classifyDiskLog(logRaw) === "replay") {
1279
- respawnStrategy = "replay";
1280
- }
1281
- }
1282
- const port = resolveBridgePort({
1283
- sandboxSession,
1284
- override: settings.port
1285
- });
1286
- const token = settings.mintBridgeToken == null ? createBridgeToken() : settings.mintBridgeToken(sandboxId);
1287
- const forwardedAuthEnvironment = credentialsBrokered ? sandboxAuthEnvironment : await applyCredentialForwarding({
1288
- environment: sandboxAuthEnvironment,
1289
- credentialEnvironmentVariables: OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
1290
- credentialForwarding: settings.credentialForwarding
1291
- });
1292
- if (!credentialsBrokered) {
1293
- warnCredentialBrokeringUnavailable({
1294
- environment: resolvedAuthEnvironment,
1295
- forwardedEnvironment: forwardedAuthEnvironment,
1296
- credentialEnvironmentVariables: OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES
1297
- });
1298
- }
1299
- const subscriptionAccessToken = forwardedAuthEnvironment[OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE];
1300
- const forwardableAuthEnvironment = Object.fromEntries(
1301
- Object.entries(forwardedAuthEnvironment).filter(
1302
- ([name]) => name !== OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE
1303
- )
1304
- );
1305
- const env = {
1306
- ...forwardableAuthEnvironment,
1307
- ...gitLabSubscriptionBrokered || authentication.subscription == null || subscriptionAccessToken == null ? {} : {
1308
- OPENCODE_AUTH_CONTENT: createOpenCodeSubscriptionAuthContent({
1309
- authentication: authentication.subscription,
1310
- accessToken: subscriptionAccessToken
1311
- })
1312
- },
1313
- AI_SDK_HARNESS_CLIENT_APP: OPENCODE_CLIENT_APP,
1314
- BRIDGE_CHANNEL_TOKEN: token,
1315
- BRIDGE_WS_PORT: String(port),
1316
- ...respawnStrategy === "replay" ? { BRIDGE_REPLAY_FROM_DISK: "1" } : {}
1317
- };
1318
- if (respawnStrategy === void 0) {
1319
- await toolSafeSandboxSession.run({
1320
- command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
1321
- abortSignal: startOpts.abortSignal
1322
- });
1323
- }
1324
- await markBridgeStarting({
1325
- sandbox: toolSafeSandboxSession,
1326
- bridgeStateDir,
1327
- bridgeType: "opencode",
1328
- abortSignal: startOpts.abortSignal
1329
- });
1330
- const proc = await toolSafeSandboxSession.spawn({
1331
- command: `node ${shellQuote(`${bootstrapDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(bootstrapDir)} --skills-dir ${shellQuote(skillsDir)}`,
1332
- env,
1333
- abortSignal: startOpts.abortSignal
1334
- });
1335
- const stderrTail = [];
1336
- const bridgeStderrDone = forwardBridgeProcessStream({
1337
- stream: proc.stderr,
1338
- streamName: "stderr",
1339
- source: "opencode",
1340
- collectTail: stderrTail
1341
- });
1342
- const { port: boundPort } = await waitForBridgeReady({
1343
- proc,
1344
- sandbox: toolSafeSandboxSession,
1345
- bridgeStateDir,
1346
- bridgeType: "opencode",
1347
- timeoutMs,
1348
- abortSignal: startOpts.abortSignal,
1349
- createTimeoutError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
1350
- message: "OpenCode bridge did not become ready in time.",
1351
- proc: proc2,
1352
- stdoutTail,
1353
- stderrTail,
1354
- stderrDone: bridgeStderrDone
1355
- }),
1356
- createExitError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
1357
- message: "OpenCode bridge exited before becoming ready.",
1358
- proc: proc2,
1359
- stdoutTail,
1360
- stderrTail,
1361
- stderrDone: bridgeStderrDone
1362
- })
1363
- });
1364
- void drainBridgeProcessStream(proc.stdout);
1365
- const endpoint = await resolveBridgeEndpoint({
1366
- sandboxSession,
1367
- override: settings.portEndpoint,
1368
- port: boundPort
1369
- });
1370
- const bridgeEndpoint = withBridgeToken({ endpoint, token });
1371
- let supportsUserMessageResponses = false;
1372
- const channel = new SandboxChannel({
1373
- connect: ({ abortSignal }) => openWebSocket({
1374
- endpoint: bridgeEndpoint,
1375
- helloTimeoutMs: Math.min(timeoutMs, 5e3),
1376
- abortSignal,
1377
- onHello: (supported) => {
1378
- supportsUserMessageResponses = supported;
1379
- }
1380
- }),
1381
- outboundSchema: outboundMessageSchema,
1382
- onDiagnostic,
1383
- onBridgeError,
1384
- reconnect: settings.reconnect,
1385
- ...respawnStrategy === "replay" ? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 } : {}
1386
- });
1387
- await channel.open(
1388
- respawnStrategy === "replay" ? { resume: true } : void 0
1389
- );
1390
- return createSession({
1391
- sessionId: startOpts.sessionId,
1392
- channel,
1393
- proc,
1394
- provider: settings.provider,
1395
- reasoningVariant: settings.reasoningVariant,
1396
- openCodeConfig: resolvedOpenCodeConfig,
1397
- mcpServers: settings.mcpServers,
1398
- headers: startOpts.headers,
1399
- openCodeSessionId: resumeSessionId,
1400
- isResume: respawnStrategy !== void 0,
1401
- seedResumeSessionOnFirstPrompt: respawnStrategy !== void 0,
1402
- rerunContinue: respawnStrategy === "rerun",
1403
- bridgePort: boundPort,
1404
- bridgeToken: token,
1405
- sandboxId,
1406
- sandboxCredentialEnvironment,
1407
- debug: startOpts.observability?.debug,
1408
- permissionMode: startOpts.permissionMode,
1409
- builtinToolFiltering: startOpts.builtinToolFiltering,
1410
- sandbox: toolSafeSandboxSession,
1411
- sandboxHomeDir,
1412
- transformModel,
1413
- supportsUserMessageResponses: () => supportsUserMessageResponses
1414
- });
1415
- }
1416
- };
829
+ if (settings.mcpServers != null && Object.prototype.hasOwnProperty.call(settings.mcpServers, "harness-tools")) throw new Error("OpenCode MCP server name \"harness-tools\" is reserved for HarnessAgent tools.");
830
+ return {
831
+ specificationVersion: "harness-v1",
832
+ harnessId: "opencode",
833
+ builtinTools: OPENCODE_BUILTIN_TOOLS,
834
+ supportsBuiltinToolApprovals: true,
835
+ lifecycleStateSchema: openCodeResumeStateSchema,
836
+ getBootstrap: getOpenCodeBootstrap,
837
+ doStart: async (startOpts) => {
838
+ const sandboxSession = startOpts.sandboxSession;
839
+ const toolSafeSandboxSession = getRestrictedSandboxSession(sandboxSession);
840
+ const sandboxId = "id" in sandboxSession ? sandboxSession.id : void 0;
841
+ validateBasicSandboxSettings({
842
+ sandboxSession,
843
+ port: settings.port,
844
+ portEndpoint: settings.portEndpoint
845
+ });
846
+ if (settings.mintBridgeToken != null && sandboxId == null) throw new HarnessCapabilityUnsupportedError({
847
+ harnessId: "opencode",
848
+ message: "The OpenCode harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id."
849
+ });
850
+ const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
851
+ const isResume = lifecycleState != null;
852
+ const isContinue = startOpts.continueFrom != null;
853
+ const resumeData = isResume && typeof lifecycleState?.data === "object" ? lifecycleState.data : void 0;
854
+ const resumeSessionId = typeof resumeData?.openCodeSessionId === "string" && resumeData.openCodeSessionId.length > 0 ? resumeData.openCodeSessionId : void 0;
855
+ const coords = resumeData?.bridge;
856
+ const authentication = await resolveOpenCodeAuthentication({
857
+ auth: settings.auth,
858
+ provider: settings.provider
859
+ });
860
+ const authenticationMode = authentication.authenticationMode;
861
+ const resolvedAuthEnvironment = authentication.environment;
862
+ let resolvedOpenCodeConfig = settings.openCodeConfig;
863
+ let transformModel;
864
+ let gitLabSubscriptionBrokered = false;
865
+ let sandboxAuthEnvironment = resolvedAuthEnvironment;
866
+ let sandboxCredentialEnvironment;
867
+ let credentialsBrokered = false;
868
+ if ("addRequestTransformations" in sandboxSession && sandboxSession.addRequestTransformations != null) {
869
+ sandboxCredentialEnvironment = await resolveSandboxCredentialEnvironment({
870
+ environment: resolvedAuthEnvironment,
871
+ credentialEnvironmentVariables: OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
872
+ credentialForwarding: settings.credentialForwarding,
873
+ previousSandboxCredentialEnvironment: resumeData?.sandboxCredentialEnvironment
874
+ });
875
+ sandboxAuthEnvironment = {
876
+ ...resolvedAuthEnvironment,
877
+ ...sandboxCredentialEnvironment
878
+ };
879
+ const transformationSources = {
880
+ env: resolvedAuthEnvironment,
881
+ sandboxEnv: sandboxAuthEnvironment,
882
+ auth: authenticationMode
883
+ };
884
+ const sandboxSubscriptionAccessToken = sandboxAuthEnvironment[OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE];
885
+ let requestTransformations;
886
+ if (authentication.subscription?.providerId === "gitlab" && sandboxSubscriptionAccessToken != null) {
887
+ const directAccess = await requestOpenCodeGitLabDirectAccess({
888
+ accessToken: authentication.subscription.accessToken,
889
+ ...authentication.subscription.enterpriseUrl == null ? {} : { instanceUrl: authentication.subscription.enterpriseUrl },
890
+ ...process.env.GITLAB_AI_GATEWAY_URL == null ? {} : { aiGatewayUrl: process.env.GITLAB_AI_GATEWAY_URL }
891
+ });
892
+ requestTransformations = createOpenCodeGitLabSubscriptionRequestTransformations({
893
+ directAccess,
894
+ sandboxAccessToken: sandboxSubscriptionAccessToken
895
+ });
896
+ resolvedOpenCodeConfig = createOpenCodeGitLabSubscriptionConfig({
897
+ openCodeConfig: settings.openCodeConfig,
898
+ sandboxAccessToken: sandboxSubscriptionAccessToken,
899
+ aiGatewayUrl: directAccess.aiGatewayUrl,
900
+ ...startOpts.headers == null ? {} : { headers: startOpts.headers }
901
+ });
902
+ transformModel = (model) => resolveOpenCodeGitLabSubscriptionModel({
903
+ model,
904
+ provider: settings.provider
905
+ });
906
+ gitLabSubscriptionBrokered = true;
907
+ } else requestTransformations = authentication.subscription == null || sandboxSubscriptionAccessToken == null ? createOpenCodeRequestTransformations(transformationSources) : createOpenCodeSubscriptionRequestTransformations({
908
+ authentication: authentication.subscription,
909
+ sandboxAccessToken: sandboxSubscriptionAccessToken
910
+ });
911
+ if (requestTransformations.length > 0) await sandboxSession.addRequestTransformations(requestTransformations);
912
+ credentialsBrokered = true;
913
+ }
914
+ const sandboxHomeDir = await resolveSandboxHomeDir({
915
+ sandbox: toolSafeSandboxSession,
916
+ abortSignal: startOpts.abortSignal
917
+ });
918
+ const stateDir = harnessStateDirectoryPath({ sandboxHomeDir });
919
+ const bootstrapDir = path.posix.resolve(stateDir, OPENCODE_BOOTSTRAP_DIR);
920
+ const workDir = startOpts.sessionWorkDir;
921
+ const skillsDir = path.posix.join(sandboxHomeDir, ".agents", "skills");
922
+ const bridgeStateDir = `${harnessSessionDataDirectoryPath({
923
+ stateDirectory: stateDir,
924
+ sessionId: startOpts.sessionId
925
+ })}/bridge`;
926
+ const timeoutMs = settings.startupTimeoutMs ?? 12e4;
927
+ const report = startOpts.observability?.report;
928
+ const onDiagnostic = report ? (frame) => report(harnessV1DiagnosticFromBridgeFrame(frame, {
929
+ sessionId: startOpts.sessionId,
930
+ timestamp: Date.now()
931
+ })) : void 0;
932
+ const onBridgeError = createBridgeErrorHandler({
933
+ harnessId: "opencode",
934
+ sessionId: startOpts.sessionId
935
+ });
936
+ if (coords) try {
937
+ const endpoint = await resolveBridgeEndpoint({
938
+ sandboxSession,
939
+ override: settings.portEndpoint,
940
+ port: coords.port
941
+ });
942
+ const attachEndpoint = withBridgeToken({
943
+ endpoint,
944
+ token: coords.token
945
+ });
946
+ let supportsUserMessageResponses = false;
947
+ const attachChannel = new SandboxChannel({
948
+ connect: ({ abortSignal }) => openWebSocket({
949
+ endpoint: attachEndpoint,
950
+ helloTimeoutMs: Math.min(timeoutMs, 5e3),
951
+ abortSignal,
952
+ onHello: (supported) => {
953
+ supportsUserMessageResponses = supported;
954
+ }
955
+ }),
956
+ outboundSchema: outboundMessageSchema,
957
+ initialLastSeenEventId: coords.lastSeenEventId,
958
+ onDiagnostic,
959
+ onBridgeError,
960
+ reconnect: settings.reconnect
961
+ });
962
+ await attachChannel.open(isContinue ? { resume: true } : void 0);
963
+ return createSession({
964
+ sessionId: startOpts.sessionId,
965
+ channel: attachChannel,
966
+ proc: void 0,
967
+ provider: settings.provider,
968
+ reasoningVariant: settings.reasoningVariant,
969
+ openCodeConfig: resolvedOpenCodeConfig,
970
+ mcpServers: settings.mcpServers,
971
+ headers: startOpts.headers,
972
+ openCodeSessionId: resumeSessionId,
973
+ isResume: true,
974
+ seedResumeSessionOnFirstPrompt: false,
975
+ rerunContinue: false,
976
+ bridgePort: coords.port,
977
+ bridgeToken: coords.token,
978
+ sandboxId,
979
+ sandboxCredentialEnvironment,
980
+ debug: startOpts.observability?.debug,
981
+ permissionMode: startOpts.permissionMode,
982
+ builtinToolFiltering: startOpts.builtinToolFiltering,
983
+ sandbox: toolSafeSandboxSession,
984
+ sandboxHomeDir,
985
+ transformModel,
986
+ supportsUserMessageResponses: () => supportsUserMessageResponses
987
+ });
988
+ } catch {}
989
+ let respawnStrategy = isResume ? "rerun" : void 0;
990
+ if (coords && isContinue) {
991
+ const logRaw = await Promise.resolve(toolSafeSandboxSession.readTextFile({
992
+ path: `${bridgeStateDir}/event-log.ndjson`,
993
+ abortSignal: startOpts.abortSignal
994
+ })).catch(() => null);
995
+ if (await classifyDiskLog(logRaw) === "replay") respawnStrategy = "replay";
996
+ }
997
+ const port = resolveBridgePort({
998
+ sandboxSession,
999
+ override: settings.port
1000
+ });
1001
+ const token = settings.mintBridgeToken == null ? createBridgeToken() : settings.mintBridgeToken(sandboxId);
1002
+ const forwardedAuthEnvironment = credentialsBrokered ? sandboxAuthEnvironment : await applyCredentialForwarding({
1003
+ environment: sandboxAuthEnvironment,
1004
+ credentialEnvironmentVariables: OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
1005
+ credentialForwarding: settings.credentialForwarding
1006
+ });
1007
+ if (!credentialsBrokered) warnCredentialBrokeringUnavailable({
1008
+ environment: resolvedAuthEnvironment,
1009
+ forwardedEnvironment: forwardedAuthEnvironment,
1010
+ credentialEnvironmentVariables: OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES
1011
+ });
1012
+ const subscriptionAccessToken = forwardedAuthEnvironment[OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE];
1013
+ const env = {
1014
+ ...Object.fromEntries(Object.entries(forwardedAuthEnvironment).filter(([name]) => name !== OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE)),
1015
+ ...gitLabSubscriptionBrokered || authentication.subscription == null || subscriptionAccessToken == null ? {} : { OPENCODE_AUTH_CONTENT: createOpenCodeSubscriptionAuthContent({
1016
+ authentication: authentication.subscription,
1017
+ accessToken: subscriptionAccessToken
1018
+ }) },
1019
+ AI_SDK_HARNESS_CLIENT_APP: OPENCODE_CLIENT_APP,
1020
+ BRIDGE_CHANNEL_TOKEN: token,
1021
+ BRIDGE_WS_PORT: String(port),
1022
+ ...respawnStrategy === "replay" ? { BRIDGE_REPLAY_FROM_DISK: "1" } : {}
1023
+ };
1024
+ if (respawnStrategy === void 0) await toolSafeSandboxSession.run({
1025
+ command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
1026
+ abortSignal: startOpts.abortSignal
1027
+ });
1028
+ await markBridgeStarting({
1029
+ sandbox: toolSafeSandboxSession,
1030
+ bridgeStateDir,
1031
+ bridgeType: "opencode",
1032
+ abortSignal: startOpts.abortSignal
1033
+ });
1034
+ const proc = await toolSafeSandboxSession.spawn({
1035
+ command: `node ${shellQuote(`${bootstrapDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(bootstrapDir)} --skills-dir ${shellQuote(skillsDir)}`,
1036
+ env,
1037
+ abortSignal: startOpts.abortSignal
1038
+ });
1039
+ const stderrTail = [];
1040
+ const bridgeStderrDone = forwardBridgeProcessStream({
1041
+ stream: proc.stderr,
1042
+ streamName: "stderr",
1043
+ source: "opencode",
1044
+ collectTail: stderrTail
1045
+ });
1046
+ const { port: boundPort } = await waitForBridgeReady({
1047
+ proc,
1048
+ sandbox: toolSafeSandboxSession,
1049
+ bridgeStateDir,
1050
+ bridgeType: "opencode",
1051
+ timeoutMs,
1052
+ abortSignal: startOpts.abortSignal,
1053
+ createTimeoutError: ({ proc, stdoutTail }) => createBridgeStartupError({
1054
+ message: "OpenCode bridge did not become ready in time.",
1055
+ proc,
1056
+ stdoutTail,
1057
+ stderrTail,
1058
+ stderrDone: bridgeStderrDone
1059
+ }),
1060
+ createExitError: ({ proc, stdoutTail }) => createBridgeStartupError({
1061
+ message: "OpenCode bridge exited before becoming ready.",
1062
+ proc,
1063
+ stdoutTail,
1064
+ stderrTail,
1065
+ stderrDone: bridgeStderrDone
1066
+ })
1067
+ });
1068
+ drainBridgeProcessStream(proc.stdout);
1069
+ const endpoint = await resolveBridgeEndpoint({
1070
+ sandboxSession,
1071
+ override: settings.portEndpoint,
1072
+ port: boundPort
1073
+ });
1074
+ const bridgeEndpoint = withBridgeToken({
1075
+ endpoint,
1076
+ token
1077
+ });
1078
+ let supportsUserMessageResponses = false;
1079
+ const channel = new SandboxChannel({
1080
+ connect: ({ abortSignal }) => openWebSocket({
1081
+ endpoint: bridgeEndpoint,
1082
+ helloTimeoutMs: Math.min(timeoutMs, 5e3),
1083
+ abortSignal,
1084
+ onHello: (supported) => {
1085
+ supportsUserMessageResponses = supported;
1086
+ }
1087
+ }),
1088
+ outboundSchema: outboundMessageSchema,
1089
+ onDiagnostic,
1090
+ onBridgeError,
1091
+ reconnect: settings.reconnect,
1092
+ ...respawnStrategy === "replay" ? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 } : {}
1093
+ });
1094
+ await channel.open(respawnStrategy === "replay" ? { resume: true } : void 0);
1095
+ return createSession({
1096
+ sessionId: startOpts.sessionId,
1097
+ channel,
1098
+ proc,
1099
+ provider: settings.provider,
1100
+ reasoningVariant: settings.reasoningVariant,
1101
+ openCodeConfig: resolvedOpenCodeConfig,
1102
+ mcpServers: settings.mcpServers,
1103
+ headers: startOpts.headers,
1104
+ openCodeSessionId: resumeSessionId,
1105
+ isResume: respawnStrategy !== void 0,
1106
+ seedResumeSessionOnFirstPrompt: respawnStrategy !== void 0,
1107
+ rerunContinue: respawnStrategy === "rerun",
1108
+ bridgePort: boundPort,
1109
+ bridgeToken: token,
1110
+ sandboxId,
1111
+ sandboxCredentialEnvironment,
1112
+ debug: startOpts.observability?.debug,
1113
+ permissionMode: startOpts.permissionMode,
1114
+ builtinToolFiltering: startOpts.builtinToolFiltering,
1115
+ sandbox: toolSafeSandboxSession,
1116
+ sandboxHomeDir,
1117
+ transformModel,
1118
+ supportsUserMessageResponses: () => supportsUserMessageResponses
1119
+ });
1120
+ }
1121
+ };
1417
1122
  }
1418
- function resolveBridgePort({
1419
- sandboxSession,
1420
- override
1421
- }) {
1422
- if (override !== void 0) return override;
1423
- if ("ports" in sandboxSession && sandboxSession.ports.length > 0) {
1424
- return sandboxSession.ports[0];
1425
- }
1426
- throw new HarnessCapabilityUnsupportedError2({
1427
- harnessId: "opencode",
1428
- message: "The OpenCode harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createOpenCode({ port })`."
1429
- });
1123
+ function resolveBridgePort({ sandboxSession, override }) {
1124
+ if (override !== void 0) return override;
1125
+ if ("ports" in sandboxSession && sandboxSession.ports.length > 0) return sandboxSession.ports[0];
1126
+ throw new HarnessCapabilityUnsupportedError({
1127
+ harnessId: "opencode",
1128
+ message: "The OpenCode harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createOpenCode({ port })`."
1129
+ });
1430
1130
  }
1431
- function validateBasicSandboxSettings({
1432
- sandboxSession,
1433
- port,
1434
- portEndpoint
1435
- }) {
1436
- if ("getPortEndpoint" in sandboxSession) return;
1437
- if (port == null) {
1438
- throw new HarnessCapabilityUnsupportedError2({
1439
- harnessId: "opencode",
1440
- message: "The OpenCode harness requires an explicit `port` when using a basic sandbox session."
1441
- });
1442
- }
1443
- if (portEndpoint == null) {
1444
- throw new HarnessCapabilityUnsupportedError2({
1445
- harnessId: "opencode",
1446
- message: "The OpenCode harness requires an explicit `portEndpoint` when using a basic sandbox session."
1447
- });
1448
- }
1131
+ function validateBasicSandboxSettings({ sandboxSession, port, portEndpoint }) {
1132
+ if ("getPortEndpoint" in sandboxSession) return;
1133
+ if (port == null) throw new HarnessCapabilityUnsupportedError({
1134
+ harnessId: "opencode",
1135
+ message: "The OpenCode harness requires an explicit `port` when using a basic sandbox session."
1136
+ });
1137
+ if (portEndpoint == null) throw new HarnessCapabilityUnsupportedError({
1138
+ harnessId: "opencode",
1139
+ message: "The OpenCode harness requires an explicit `portEndpoint` when using a basic sandbox session."
1140
+ });
1449
1141
  }
1450
- async function resolveBridgeEndpoint({
1451
- sandboxSession,
1452
- override,
1453
- port
1454
- }) {
1455
- if (override != null) return override;
1456
- if ("getPortEndpoint" in sandboxSession) {
1457
- return sandboxSession.getPortEndpoint({ port, protocol: "ws" });
1458
- }
1459
- throw new HarnessCapabilityUnsupportedError2({
1460
- harnessId: "opencode",
1461
- message: "The OpenCode harness requires an explicit `portEndpoint` when using a basic sandbox session."
1462
- });
1142
+ async function resolveBridgeEndpoint({ sandboxSession, override, port }) {
1143
+ if (override != null) return override;
1144
+ if ("getPortEndpoint" in sandboxSession) return sandboxSession.getPortEndpoint({
1145
+ port,
1146
+ protocol: "ws"
1147
+ });
1148
+ throw new HarnessCapabilityUnsupportedError({
1149
+ harnessId: "opencode",
1150
+ message: "The OpenCode harness requires an explicit `portEndpoint` when using a basic sandbox session."
1151
+ });
1463
1152
  }
1464
- function openWebSocket({
1465
- endpoint,
1466
- helloTimeoutMs,
1467
- abortSignal,
1468
- onHello
1469
- }) {
1470
- return new Promise((resolve, reject) => {
1471
- const abortReason = () => abortSignal.reason ?? new Error("WebSocket connection aborted");
1472
- if (abortSignal.aborted) {
1473
- reject(abortReason());
1474
- return;
1475
- }
1476
- const ws = new WebSocket(endpoint.url, {
1477
- headers: endpoint.headers == null ? void 0 : { ...endpoint.headers }
1478
- });
1479
- let opened = false;
1480
- let receivedHello = false;
1481
- let settled = false;
1482
- let helloTimer;
1483
- let onAbort;
1484
- const cleanup = () => {
1485
- clearTimeout(helloTimer);
1486
- ws.off("open", onOpen);
1487
- ws.off("message", onMessage);
1488
- ws.off("close", onClose);
1489
- ws.off("error", onError);
1490
- if (onAbort != null) {
1491
- abortSignal.removeEventListener("abort", onAbort);
1492
- }
1493
- };
1494
- const settle = (error) => {
1495
- if (settled) return;
1496
- settled = true;
1497
- cleanup();
1498
- if (error == null) {
1499
- resolve(ws);
1500
- } else {
1501
- const suppressError = () => {
1502
- };
1503
- const socketWithOnce = ws;
1504
- socketWithOnce.once?.("error", suppressError);
1505
- try {
1506
- ws.terminate();
1507
- } catch {
1508
- try {
1509
- ws.close();
1510
- } catch {
1511
- }
1512
- }
1513
- reject(error);
1514
- }
1515
- };
1516
- const tryResolve = () => {
1517
- if (opened && receivedHello) settle();
1518
- };
1519
- const onOpen = () => {
1520
- opened = true;
1521
- tryResolve();
1522
- };
1523
- const onMessage = (raw) => {
1524
- void (async () => {
1525
- const parsed = await safeParseJSON2({
1526
- text: webSocketMessageToString(raw)
1527
- });
1528
- if (!parsed.success) return;
1529
- const value = parsed.value;
1530
- if (typeof value !== "object" || value == null || Array.isArray(value) || value.type !== "bridge-hello") {
1531
- return;
1532
- }
1533
- const capabilities = value.capabilities;
1534
- onHello(capabilities?.experimental_userMessageResponses === true);
1535
- receivedHello = true;
1536
- tryResolve();
1537
- })();
1538
- };
1539
- const onClose = () => settle(new Error("OpenCode bridge closed before sending bridge-hello."));
1540
- const onError = (err) => {
1541
- settle(err);
1542
- };
1543
- onAbort = () => settle(abortReason());
1544
- ws.on("open", onOpen);
1545
- ws.on("message", onMessage);
1546
- ws.on("close", onClose);
1547
- ws.on("error", onError);
1548
- abortSignal.addEventListener("abort", onAbort, { once: true });
1549
- if (abortSignal.aborted) {
1550
- onAbort();
1551
- return;
1552
- }
1553
- helloTimer = setTimeout(
1554
- () => settle(
1555
- new Error(
1556
- `OpenCode bridge did not send bridge-hello within ${helloTimeoutMs}ms.`
1557
- )
1558
- ),
1559
- helloTimeoutMs
1560
- );
1561
- helloTimer.unref?.();
1562
- });
1153
+ function openWebSocket({ endpoint, helloTimeoutMs, abortSignal, onHello }) {
1154
+ return new Promise((resolve, reject) => {
1155
+ const abortReason = () => abortSignal.reason ?? /* @__PURE__ */ new Error("WebSocket connection aborted");
1156
+ if (abortSignal.aborted) {
1157
+ reject(abortReason());
1158
+ return;
1159
+ }
1160
+ const ws = new WebSocket(endpoint.url, { headers: endpoint.headers == null ? void 0 : { ...endpoint.headers } });
1161
+ let opened = false;
1162
+ let receivedHello = false;
1163
+ let settled = false;
1164
+ let helloTimer;
1165
+ let onAbort;
1166
+ const cleanup = () => {
1167
+ clearTimeout(helloTimer);
1168
+ ws.off("open", onOpen);
1169
+ ws.off("message", onMessage);
1170
+ ws.off("close", onClose);
1171
+ ws.off("error", onError);
1172
+ if (onAbort != null) abortSignal.removeEventListener("abort", onAbort);
1173
+ };
1174
+ const settle = (error) => {
1175
+ if (settled) return;
1176
+ settled = true;
1177
+ cleanup();
1178
+ if (error == null) resolve(ws);
1179
+ else {
1180
+ const suppressError = () => {};
1181
+ ws.once?.("error", suppressError);
1182
+ try {
1183
+ ws.terminate();
1184
+ } catch {
1185
+ try {
1186
+ ws.close();
1187
+ } catch {}
1188
+ }
1189
+ reject(error);
1190
+ }
1191
+ };
1192
+ const tryResolve = () => {
1193
+ if (opened && receivedHello) settle();
1194
+ };
1195
+ const onOpen = () => {
1196
+ opened = true;
1197
+ tryResolve();
1198
+ };
1199
+ const onMessage = (raw) => {
1200
+ (async () => {
1201
+ const parsed = await safeParseJSON({ text: webSocketMessageToString(raw) });
1202
+ if (!parsed.success) return;
1203
+ const value = parsed.value;
1204
+ if (typeof value !== "object" || value == null || Array.isArray(value) || value.type !== "bridge-hello") return;
1205
+ const capabilities = value.capabilities;
1206
+ onHello(capabilities?.experimental_userMessageResponses === true);
1207
+ receivedHello = true;
1208
+ tryResolve();
1209
+ })();
1210
+ };
1211
+ const onClose = () => settle(/* @__PURE__ */ new Error("OpenCode bridge closed before sending bridge-hello."));
1212
+ const onError = (err) => {
1213
+ settle(err);
1214
+ };
1215
+ onAbort = () => settle(abortReason());
1216
+ ws.on("open", onOpen);
1217
+ ws.on("message", onMessage);
1218
+ ws.on("close", onClose);
1219
+ ws.on("error", onError);
1220
+ abortSignal.addEventListener("abort", onAbort, { once: true });
1221
+ if (abortSignal.aborted) {
1222
+ onAbort();
1223
+ return;
1224
+ }
1225
+ helloTimer = setTimeout(() => settle(/* @__PURE__ */ new Error(`OpenCode bridge did not send bridge-hello within ${helloTimeoutMs}ms.`)), helloTimeoutMs);
1226
+ helloTimer.unref?.();
1227
+ });
1563
1228
  }
1564
1229
  function webSocketMessageToString(raw) {
1565
- if (typeof raw === "string") return raw;
1566
- if (Buffer.isBuffer(raw)) return raw.toString("utf8");
1567
- if (Array.isArray(raw)) return Buffer.concat(raw).toString("utf8");
1568
- if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString("utf8");
1569
- if (ArrayBuffer.isView(raw)) {
1570
- return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString(
1571
- "utf8"
1572
- );
1573
- }
1574
- return String(raw);
1230
+ if (typeof raw === "string") return raw;
1231
+ if (Buffer.isBuffer(raw)) return raw.toString("utf8");
1232
+ if (Array.isArray(raw)) return Buffer.concat(raw).toString("utf8");
1233
+ if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString("utf8");
1234
+ if (ArrayBuffer.isView(raw)) return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString("utf8");
1235
+ return String(raw);
1575
1236
  }
1576
- function createSession({
1577
- sessionId,
1578
- channel,
1579
- proc,
1580
- provider,
1581
- reasoningVariant,
1582
- openCodeConfig,
1583
- mcpServers,
1584
- headers,
1585
- openCodeSessionId,
1586
- isResume,
1587
- seedResumeSessionOnFirstPrompt,
1588
- rerunContinue,
1589
- bridgePort,
1590
- bridgeToken,
1591
- sandboxId,
1592
- sandboxCredentialEnvironment,
1593
- debug,
1594
- permissionMode,
1595
- builtinToolFiltering,
1596
- sandbox,
1597
- sandboxHomeDir,
1598
- transformModel,
1599
- supportsUserMessageResponses
1600
- }) {
1601
- let stopped = false;
1602
- let stopPromise;
1603
- let latestOpenCodeSessionId = openCodeSessionId;
1604
- let pendingResumeSessionId = seedResumeSessionOnFirstPrompt ? openCodeSessionId : void 0;
1605
- let selectedModel;
1606
- let activeTurn = false;
1607
- const pendingCompactionParts = [];
1608
- channel.on("bridge-thread", (msg) => {
1609
- latestOpenCodeSessionId = msg.threadId;
1610
- });
1611
- const wireTurn = (turnOpts) => {
1612
- activeTurn = true;
1613
- let pendingResolve;
1614
- let pendingReject;
1615
- const done = new Promise((resolve, reject) => {
1616
- pendingResolve = resolve;
1617
- pendingReject = reject;
1618
- });
1619
- const userMessageSubmitter = supportsUserMessageResponses() ? experimental_createBridgeUserMessageSubmitter({
1620
- send: (message) => channel.send(message),
1621
- onResponse: (listener) => channel.on("user-message-response", listener),
1622
- onReconnect: (listener) => channel.onReconnect(listener)
1623
- }) : void 0;
1624
- const unsubs = [];
1625
- const forward = (event) => {
1626
- try {
1627
- turnOpts.emit(event);
1628
- } catch {
1629
- }
1630
- };
1631
- const eventTypes = [
1632
- "stream-start",
1633
- "text-start",
1634
- "text-delta",
1635
- "text-end",
1636
- "reasoning-start",
1637
- "reasoning-delta",
1638
- "reasoning-end",
1639
- "tool-call",
1640
- "tool-approval-request",
1641
- "tool-result",
1642
- "file-change",
1643
- "finish-step",
1644
- "compaction",
1645
- "raw"
1646
- ];
1647
- let isSettled = false;
1648
- const settleSuccess = () => {
1649
- if (isSettled) return;
1650
- isSettled = true;
1651
- activeTurn = false;
1652
- userMessageSubmitter?.close();
1653
- for (const u of unsubs) u();
1654
- pendingResolve();
1655
- };
1656
- const settleError = (err) => {
1657
- if (isSettled) return;
1658
- isSettled = true;
1659
- activeTurn = false;
1660
- userMessageSubmitter?.close(err);
1661
- for (const u of unsubs) u();
1662
- pendingReject(err);
1663
- };
1664
- for (const type of eventTypes) {
1665
- unsubs.push(
1666
- channel.on(type, (msg) => {
1667
- forward(msg);
1668
- })
1669
- );
1670
- }
1671
- unsubs.push(
1672
- channel.on("finish", (msg) => {
1673
- forward(msg);
1674
- settleSuccess();
1675
- })
1676
- );
1677
- unsubs.push(
1678
- channel.on("error", (msg) => {
1679
- forward(msg);
1680
- settleError(msg.error);
1681
- })
1682
- );
1683
- const onClose = (_code, reason) => {
1684
- if (isSettled) return;
1685
- if (reason === "suspended") {
1686
- settleSuccess();
1687
- return;
1688
- }
1689
- settleError(
1690
- new Error("OpenCode bridge closed before the turn finished.")
1691
- );
1692
- };
1693
- channel.onClose(onClose);
1694
- const onAbort = () => {
1695
- if (isSettled) return;
1696
- try {
1697
- channel.send({ type: "abort" });
1698
- } catch {
1699
- }
1700
- settleError(
1701
- turnOpts.abortSignal?.reason ?? new DOMException("Aborted", "AbortError")
1702
- );
1703
- };
1704
- if (turnOpts.abortSignal) {
1705
- if (turnOpts.abortSignal.aborted) {
1706
- onAbort();
1707
- } else {
1708
- turnOpts.abortSignal.addEventListener("abort", onAbort, {
1709
- once: true
1710
- });
1711
- }
1712
- }
1713
- while (pendingCompactionParts.length > 0) {
1714
- forward(pendingCompactionParts.shift());
1715
- }
1716
- return {
1717
- submitToolResult: async (input) => {
1718
- channel.send({
1719
- type: "tool-result",
1720
- toolCallId: input.toolCallId,
1721
- output: input.output,
1722
- isError: input.isError,
1723
- ...input.toolResult !== void 0 ? { toolResult: input.toolResult } : {}
1724
- });
1725
- },
1726
- submitToolApproval: async (input) => {
1727
- channel.send({
1728
- type: "tool-approval-response",
1729
- approvalId: input.approvalId,
1730
- approved: input.approved,
1731
- reason: input.reason
1732
- });
1733
- },
1734
- ...userMessageSubmitter == null ? {} : {
1735
- submitUserMessage: async (text) => {
1736
- await userMessageSubmitter.submit(text);
1737
- }
1738
- },
1739
- done
1740
- };
1741
- };
1742
- const startBase = (turnModel) => ({
1743
- model: transformModel == null ? turnModel : transformModel(turnModel),
1744
- provider,
1745
- ...reasoningVariant ? { variant: reasoningVariant } : {},
1746
- ...openCodeConfig == null ? {} : { openCodeConfig },
1747
- ...mcpServers == null ? {} : { mcpServers },
1748
- ...headers == null ? {} : { headers },
1749
- ...permissionMode ? { permissionMode } : {},
1750
- ...builtinToolFiltering ? { builtinToolFiltering } : {},
1751
- ...pendingResumeSessionId ? { resumeSessionId: pendingResumeSessionId } : latestOpenCodeSessionId ? { resumeSessionId: latestOpenCodeSessionId } : {},
1752
- ...debug ? { debug } : {}
1753
- });
1754
- const prepareTurn = async (opts) => {
1755
- if (opts.responseFormat?.type === "json" && opts.responseFormat.schema == null) {
1756
- throw new HarnessCapabilityUnsupportedError2({
1757
- message: "Harness 'opencode' requires a JSON schema for structured output.",
1758
- harnessId: "opencode"
1759
- });
1760
- }
1761
- const skillWriteResult = await writeSkills({
1762
- sandbox,
1763
- homePath: sandboxHomeDir,
1764
- skillsDir: ".agents/skills",
1765
- skills: opts.skills,
1766
- abortSignal: opts.abortSignal,
1767
- invalidSkillNameMessage: ({ name }) => `Invalid OpenCode skill name: ${name}`,
1768
- invalidSkillFilePathMessage: ({ skillName, filePath }) => `Invalid OpenCode skill file path for ${skillName}: ${filePath}`
1769
- });
1770
- const control = wireTurn({
1771
- emit: opts.emit,
1772
- abortSignal: opts.abortSignal
1773
- });
1774
- return { control, skillWriteResult };
1775
- };
1776
- return {
1777
- sessionId,
1778
- isResume,
1779
- doPromptTurn: async (promptOpts) => {
1780
- const { control, skillWriteResult } = await prepareTurn(promptOpts);
1781
- const turnModel = promptOpts.model ?? selectedModel;
1782
- if (turnModel) selectedModel = turnModel;
1783
- channel.send({
1784
- type: "start",
1785
- operation: "prompt",
1786
- prompt: extractUserText(promptOpts.prompt),
1787
- tools: (promptOpts.tools ?? []).map((t) => ({
1788
- name: t.name,
1789
- description: t.description,
1790
- inputSchema: t.inputSchema
1791
- })),
1792
- ...promptOpts.responseFormat == null ? {} : { responseFormat: promptOpts.responseFormat },
1793
- ...promptOpts.instructions ? { instructions: promptOpts.instructions } : {},
1794
- skillsChanged: skillWriteResult.changed,
1795
- ...startBase(turnModel)
1796
- });
1797
- pendingResumeSessionId = void 0;
1798
- return control;
1799
- },
1800
- doContinueTurn: async (continueOpts) => {
1801
- const { control, skillWriteResult } = await prepareTurn(continueOpts);
1802
- if (rerunContinue) {
1803
- const turnModel = continueOpts.model ?? selectedModel;
1804
- if (turnModel) selectedModel = turnModel;
1805
- channel.send({
1806
- type: "start",
1807
- operation: "prompt",
1808
- prompt: "Continue.",
1809
- tools: (continueOpts.tools ?? []).map((t) => ({
1810
- name: t.name,
1811
- description: t.description,
1812
- inputSchema: t.inputSchema
1813
- })),
1814
- ...continueOpts.responseFormat == null ? {} : { responseFormat: continueOpts.responseFormat },
1815
- ...continueOpts.instructions ? { instructions: continueOpts.instructions } : {},
1816
- skillsChanged: skillWriteResult.changed,
1817
- ...startBase(turnModel)
1818
- });
1819
- pendingResumeSessionId = void 0;
1820
- }
1821
- return control;
1822
- },
1823
- doCompact: async (customInstructions) => {
1824
- if (customInstructions?.trim()) {
1825
- throw new HarnessCapabilityUnsupportedError2({
1826
- harnessId: "opencode",
1827
- message: "Harness 'opencode' supports native manual compaction, but OpenCode does not expose custom compaction instructions through the supported API."
1828
- });
1829
- }
1830
- if (activeTurn) {
1831
- throw new HarnessCapabilityUnsupportedError2({
1832
- harnessId: "opencode",
1833
- message: "Harness 'opencode' supports manual compaction between turns; compacting during an active turn is not supported by the bridge transport."
1834
- });
1835
- }
1836
- await runCompactOperation({
1837
- channel,
1838
- model: transformModel == null ? selectedModel : transformModel(selectedModel),
1839
- provider,
1840
- permissionMode,
1841
- debug,
1842
- openCodeConfig,
1843
- mcpServers,
1844
- headers,
1845
- resumeSessionId: latestOpenCodeSessionId,
1846
- onCompaction: (part) => pendingCompactionParts.push(part)
1847
- });
1848
- },
1849
- doDetach: async () => {
1850
- if (stopped) {
1851
- throw new Error(
1852
- `OpenCode session ${sessionId} is already stopped; cannot detach.`
1853
- );
1854
- }
1855
- stopped = true;
1856
- const lastSeenEventId = await channel.suspend();
1857
- return {
1858
- type: "resume-session",
1859
- harnessId: "opencode",
1860
- specificationVersion: "harness-v1",
1861
- data: {
1862
- ...latestOpenCodeSessionId ? { openCodeSessionId: latestOpenCodeSessionId } : {},
1863
- ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1864
- bridge: {
1865
- port: bridgePort,
1866
- token: bridgeToken,
1867
- lastSeenEventId,
1868
- ...sandboxId == null ? {} : { sandboxId }
1869
- }
1870
- }
1871
- };
1872
- },
1873
- doDestroy: async () => {
1874
- if (stopped) return stopPromise;
1875
- stopped = true;
1876
- stopPromise = (async () => {
1877
- channel.beginClose();
1878
- try {
1879
- if (!channel.isClosed()) {
1880
- channel.send({ type: "destroy" });
1881
- }
1882
- } catch {
1883
- }
1884
- let stopTimer;
1885
- try {
1886
- if (proc) {
1887
- await Promise.race([
1888
- proc.wait(),
1889
- new Promise((resolve) => {
1890
- stopTimer = setTimeout(resolve, 5e3);
1891
- stopTimer.unref?.();
1892
- })
1893
- ]);
1894
- }
1895
- } finally {
1896
- if (stopTimer) clearTimeout(stopTimer);
1897
- try {
1898
- await proc?.kill();
1899
- } catch {
1900
- }
1901
- channel.close();
1902
- }
1903
- })();
1904
- return stopPromise;
1905
- },
1906
- doStop: async () => {
1907
- if (stopped) {
1908
- throw new Error(
1909
- `OpenCode session ${sessionId} is already stopped; cannot stop.`
1910
- );
1911
- }
1912
- stopped = true;
1913
- channel.beginClose();
1914
- const data = channel.isClosed() ? {} : await new Promise((resolve, reject) => {
1915
- const timer = setTimeout(() => {
1916
- unsub();
1917
- reject(
1918
- new Error(
1919
- `OpenCode session ${sessionId} did not reply to stop within 5s.`
1920
- )
1921
- );
1922
- }, 5e3);
1923
- timer.unref?.();
1924
- const unsub = channel.on("bridge-stop", (msg) => {
1925
- clearTimeout(timer);
1926
- unsub();
1927
- resolve(msg.data);
1928
- });
1929
- try {
1930
- channel.send({ type: "stop" });
1931
- } catch (err) {
1932
- clearTimeout(timer);
1933
- unsub();
1934
- reject(err);
1935
- }
1936
- });
1937
- let stopTimer;
1938
- try {
1939
- if (proc) {
1940
- await Promise.race([
1941
- proc.wait(),
1942
- new Promise((resolve) => {
1943
- stopTimer = setTimeout(resolve, 5e3);
1944
- stopTimer.unref?.();
1945
- })
1946
- ]);
1947
- }
1948
- } finally {
1949
- if (stopTimer) clearTimeout(stopTimer);
1950
- try {
1951
- await proc?.kill();
1952
- } catch {
1953
- }
1954
- channel.close();
1955
- }
1956
- const lifecycleData = data != null && typeof data === "object" && !Array.isArray(data) ? { ...data } : {};
1957
- const payload = {
1958
- type: "resume-session",
1959
- harnessId: "opencode",
1960
- specificationVersion: "harness-v1",
1961
- data: {
1962
- ...lifecycleData,
1963
- ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment }
1964
- }
1965
- };
1966
- return payload;
1967
- },
1968
- doSuspendTurn: async () => {
1969
- if (stopped) {
1970
- throw new Error(
1971
- `OpenCode session ${sessionId} is stopped; cannot suspend.`
1972
- );
1973
- }
1974
- stopped = true;
1975
- const lastSeenEventId = await channel.suspend();
1976
- const payload = {
1977
- type: "continue-turn",
1978
- harnessId: "opencode",
1979
- specificationVersion: "harness-v1",
1980
- data: {
1981
- ...latestOpenCodeSessionId ? { openCodeSessionId: latestOpenCodeSessionId } : {},
1982
- ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1983
- bridge: {
1984
- port: bridgePort,
1985
- token: bridgeToken,
1986
- lastSeenEventId,
1987
- ...sandboxId == null ? {} : { sandboxId }
1988
- }
1989
- }
1990
- };
1991
- return payload;
1992
- }
1993
- };
1237
+ function createSession({ sessionId, channel, proc, provider, reasoningVariant, openCodeConfig, mcpServers, headers, openCodeSessionId, isResume, seedResumeSessionOnFirstPrompt, rerunContinue, bridgePort, bridgeToken, sandboxId, sandboxCredentialEnvironment, debug, permissionMode, builtinToolFiltering, sandbox, sandboxHomeDir, transformModel, supportsUserMessageResponses }) {
1238
+ let stopped = false;
1239
+ let stopPromise;
1240
+ let latestOpenCodeSessionId = openCodeSessionId;
1241
+ let pendingResumeSessionId = seedResumeSessionOnFirstPrompt ? openCodeSessionId : void 0;
1242
+ let selectedModel;
1243
+ let activeTurn = false;
1244
+ const pendingCompactionParts = [];
1245
+ channel.on("bridge-thread", (msg) => {
1246
+ latestOpenCodeSessionId = msg.threadId;
1247
+ });
1248
+ const wireTurn = (turnOpts) => {
1249
+ activeTurn = true;
1250
+ let pendingResolve;
1251
+ let pendingReject;
1252
+ const done = new Promise((resolve, reject) => {
1253
+ pendingResolve = resolve;
1254
+ pendingReject = reject;
1255
+ });
1256
+ const userMessageSubmitter = supportsUserMessageResponses() ? experimental_createBridgeUserMessageSubmitter({
1257
+ send: (message) => channel.send(message),
1258
+ onResponse: (listener) => channel.on("user-message-response", listener),
1259
+ onReconnect: (listener) => channel.onReconnect(listener)
1260
+ }) : void 0;
1261
+ const unsubs = [];
1262
+ const forward = (event) => {
1263
+ try {
1264
+ turnOpts.emit(event);
1265
+ } catch {}
1266
+ };
1267
+ const eventTypes = [
1268
+ "stream-start",
1269
+ "text-start",
1270
+ "text-delta",
1271
+ "text-end",
1272
+ "reasoning-start",
1273
+ "reasoning-delta",
1274
+ "reasoning-end",
1275
+ "tool-call",
1276
+ "tool-approval-request",
1277
+ "tool-result",
1278
+ "file-change",
1279
+ "finish-step",
1280
+ "compaction",
1281
+ "raw"
1282
+ ];
1283
+ let isSettled = false;
1284
+ const settleSuccess = () => {
1285
+ if (isSettled) return;
1286
+ isSettled = true;
1287
+ activeTurn = false;
1288
+ userMessageSubmitter?.close();
1289
+ for (const u of unsubs) u();
1290
+ pendingResolve();
1291
+ };
1292
+ const settleError = (err) => {
1293
+ if (isSettled) return;
1294
+ isSettled = true;
1295
+ activeTurn = false;
1296
+ userMessageSubmitter?.close(err);
1297
+ for (const u of unsubs) u();
1298
+ pendingReject(err);
1299
+ };
1300
+ for (const type of eventTypes) unsubs.push(channel.on(type, (msg) => {
1301
+ forward(msg);
1302
+ }));
1303
+ unsubs.push(channel.on("finish", (msg) => {
1304
+ forward(msg);
1305
+ settleSuccess();
1306
+ }));
1307
+ unsubs.push(channel.on("error", (msg) => {
1308
+ forward(msg);
1309
+ settleError(msg.error);
1310
+ }));
1311
+ const onClose = (_code, reason) => {
1312
+ if (isSettled) return;
1313
+ if (reason === "suspended") {
1314
+ settleSuccess();
1315
+ return;
1316
+ }
1317
+ settleError(/* @__PURE__ */ new Error("OpenCode bridge closed before the turn finished."));
1318
+ };
1319
+ channel.onClose(onClose);
1320
+ const onAbort = () => {
1321
+ if (isSettled) return;
1322
+ try {
1323
+ channel.send({ type: "abort" });
1324
+ } catch {}
1325
+ settleError(turnOpts.abortSignal?.reason ?? new DOMException("Aborted", "AbortError"));
1326
+ };
1327
+ if (turnOpts.abortSignal) {
1328
+ if (turnOpts.abortSignal.aborted) onAbort();
1329
+ else turnOpts.abortSignal.addEventListener("abort", onAbort, { once: true });
1330
+ }
1331
+ while (pendingCompactionParts.length > 0) forward(pendingCompactionParts.shift());
1332
+ return {
1333
+ submitToolResult: async (input) => {
1334
+ channel.send({
1335
+ type: "tool-result",
1336
+ toolCallId: input.toolCallId,
1337
+ output: input.output,
1338
+ isError: input.isError,
1339
+ ...input.toolResult !== void 0 ? { toolResult: input.toolResult } : {}
1340
+ });
1341
+ },
1342
+ submitToolApproval: async (input) => {
1343
+ channel.send({
1344
+ type: "tool-approval-response",
1345
+ approvalId: input.approvalId,
1346
+ approved: input.approved,
1347
+ reason: input.reason
1348
+ });
1349
+ },
1350
+ ...userMessageSubmitter == null ? {} : { submitUserMessage: async (text) => {
1351
+ await userMessageSubmitter.submit(text);
1352
+ } },
1353
+ done
1354
+ };
1355
+ };
1356
+ const startBase = (turnModel) => ({
1357
+ model: transformModel == null ? turnModel : transformModel(turnModel),
1358
+ provider,
1359
+ ...reasoningVariant ? { variant: reasoningVariant } : {},
1360
+ ...openCodeConfig == null ? {} : { openCodeConfig },
1361
+ ...mcpServers == null ? {} : { mcpServers },
1362
+ ...headers == null ? {} : { headers },
1363
+ ...permissionMode ? { permissionMode } : {},
1364
+ ...builtinToolFiltering ? { builtinToolFiltering } : {},
1365
+ ...pendingResumeSessionId ? { resumeSessionId: pendingResumeSessionId } : latestOpenCodeSessionId ? { resumeSessionId: latestOpenCodeSessionId } : {},
1366
+ ...debug ? { debug } : {}
1367
+ });
1368
+ const prepareTurn = async (opts) => {
1369
+ if (opts.responseFormat?.type === "json" && opts.responseFormat.schema == null) throw new HarnessCapabilityUnsupportedError({
1370
+ message: "Harness 'opencode' requires a JSON schema for structured output.",
1371
+ harnessId: "opencode"
1372
+ });
1373
+ const skillWriteResult = await writeSkills({
1374
+ sandbox,
1375
+ homePath: sandboxHomeDir,
1376
+ skillsDir: ".agents/skills",
1377
+ skills: opts.skills,
1378
+ abortSignal: opts.abortSignal,
1379
+ invalidSkillNameMessage: ({ name }) => `Invalid OpenCode skill name: ${name}`,
1380
+ invalidSkillFilePathMessage: ({ skillName, filePath }) => `Invalid OpenCode skill file path for ${skillName}: ${filePath}`
1381
+ });
1382
+ return {
1383
+ control: wireTurn({
1384
+ emit: opts.emit,
1385
+ abortSignal: opts.abortSignal
1386
+ }),
1387
+ skillWriteResult
1388
+ };
1389
+ };
1390
+ return {
1391
+ sessionId,
1392
+ isResume,
1393
+ doPromptTurn: async (promptOpts) => {
1394
+ const { control, skillWriteResult } = await prepareTurn(promptOpts);
1395
+ const turnModel = promptOpts.model ?? selectedModel;
1396
+ if (turnModel) selectedModel = turnModel;
1397
+ channel.send({
1398
+ type: "start",
1399
+ operation: "prompt",
1400
+ prompt: extractUserText(promptOpts.prompt),
1401
+ tools: (promptOpts.tools ?? []).map((t) => ({
1402
+ name: t.name,
1403
+ description: t.description,
1404
+ inputSchema: t.inputSchema
1405
+ })),
1406
+ ...promptOpts.responseFormat == null ? {} : { responseFormat: promptOpts.responseFormat },
1407
+ ...promptOpts.instructions ? { instructions: promptOpts.instructions } : {},
1408
+ skillsChanged: skillWriteResult.changed,
1409
+ ...startBase(turnModel)
1410
+ });
1411
+ pendingResumeSessionId = void 0;
1412
+ return control;
1413
+ },
1414
+ doContinueTurn: async (continueOpts) => {
1415
+ const { control, skillWriteResult } = await prepareTurn(continueOpts);
1416
+ if (rerunContinue) {
1417
+ const turnModel = continueOpts.model ?? selectedModel;
1418
+ if (turnModel) selectedModel = turnModel;
1419
+ channel.send({
1420
+ type: "start",
1421
+ operation: "prompt",
1422
+ prompt: "Continue.",
1423
+ tools: (continueOpts.tools ?? []).map((t) => ({
1424
+ name: t.name,
1425
+ description: t.description,
1426
+ inputSchema: t.inputSchema
1427
+ })),
1428
+ ...continueOpts.responseFormat == null ? {} : { responseFormat: continueOpts.responseFormat },
1429
+ ...continueOpts.instructions ? { instructions: continueOpts.instructions } : {},
1430
+ skillsChanged: skillWriteResult.changed,
1431
+ ...startBase(turnModel)
1432
+ });
1433
+ pendingResumeSessionId = void 0;
1434
+ }
1435
+ return control;
1436
+ },
1437
+ doCompact: async (customInstructions) => {
1438
+ if (customInstructions?.trim()) throw new HarnessCapabilityUnsupportedError({
1439
+ harnessId: "opencode",
1440
+ message: "Harness 'opencode' supports native manual compaction, but OpenCode does not expose custom compaction instructions through the supported API."
1441
+ });
1442
+ if (activeTurn) throw new HarnessCapabilityUnsupportedError({
1443
+ harnessId: "opencode",
1444
+ message: "Harness 'opencode' supports manual compaction between turns; compacting during an active turn is not supported by the bridge transport."
1445
+ });
1446
+ await runCompactOperation({
1447
+ channel,
1448
+ model: transformModel == null ? selectedModel : transformModel(selectedModel),
1449
+ provider,
1450
+ permissionMode,
1451
+ debug,
1452
+ openCodeConfig,
1453
+ mcpServers,
1454
+ headers,
1455
+ resumeSessionId: latestOpenCodeSessionId,
1456
+ onCompaction: (part) => pendingCompactionParts.push(part)
1457
+ });
1458
+ },
1459
+ doDetach: async () => {
1460
+ if (stopped) throw new Error(`OpenCode session ${sessionId} is already stopped; cannot detach.`);
1461
+ stopped = true;
1462
+ const lastSeenEventId = await channel.suspend();
1463
+ return {
1464
+ type: "resume-session",
1465
+ harnessId: "opencode",
1466
+ specificationVersion: "harness-v1",
1467
+ data: {
1468
+ ...latestOpenCodeSessionId ? { openCodeSessionId: latestOpenCodeSessionId } : {},
1469
+ ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1470
+ bridge: {
1471
+ port: bridgePort,
1472
+ token: bridgeToken,
1473
+ lastSeenEventId,
1474
+ ...sandboxId == null ? {} : { sandboxId }
1475
+ }
1476
+ }
1477
+ };
1478
+ },
1479
+ doDestroy: async () => {
1480
+ if (stopped) return stopPromise;
1481
+ stopped = true;
1482
+ stopPromise = (async () => {
1483
+ channel.beginClose();
1484
+ try {
1485
+ if (!channel.isClosed()) channel.send({ type: "destroy" });
1486
+ } catch {}
1487
+ let stopTimer;
1488
+ try {
1489
+ if (proc) await Promise.race([proc.wait(), new Promise((resolve) => {
1490
+ stopTimer = setTimeout(resolve, 5e3);
1491
+ stopTimer.unref?.();
1492
+ })]);
1493
+ } finally {
1494
+ if (stopTimer) clearTimeout(stopTimer);
1495
+ try {
1496
+ await proc?.kill();
1497
+ } catch {}
1498
+ channel.close();
1499
+ }
1500
+ })();
1501
+ return stopPromise;
1502
+ },
1503
+ doStop: async () => {
1504
+ if (stopped) throw new Error(`OpenCode session ${sessionId} is already stopped; cannot stop.`);
1505
+ stopped = true;
1506
+ channel.beginClose();
1507
+ const data = channel.isClosed() ? {} : await new Promise((resolve, reject) => {
1508
+ const timer = setTimeout(() => {
1509
+ unsub();
1510
+ reject(/* @__PURE__ */ new Error(`OpenCode session ${sessionId} did not reply to stop within 5s.`));
1511
+ }, 5e3);
1512
+ timer.unref?.();
1513
+ const unsub = channel.on("bridge-stop", (msg) => {
1514
+ clearTimeout(timer);
1515
+ unsub();
1516
+ resolve(msg.data);
1517
+ });
1518
+ try {
1519
+ channel.send({ type: "stop" });
1520
+ } catch (err) {
1521
+ clearTimeout(timer);
1522
+ unsub();
1523
+ reject(err);
1524
+ }
1525
+ });
1526
+ let stopTimer;
1527
+ try {
1528
+ if (proc) await Promise.race([proc.wait(), new Promise((resolve) => {
1529
+ stopTimer = setTimeout(resolve, 5e3);
1530
+ stopTimer.unref?.();
1531
+ })]);
1532
+ } finally {
1533
+ if (stopTimer) clearTimeout(stopTimer);
1534
+ try {
1535
+ await proc?.kill();
1536
+ } catch {}
1537
+ channel.close();
1538
+ }
1539
+ return {
1540
+ type: "resume-session",
1541
+ harnessId: "opencode",
1542
+ specificationVersion: "harness-v1",
1543
+ data: {
1544
+ ...data != null && typeof data === "object" && !Array.isArray(data) ? { ...data } : {},
1545
+ ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment }
1546
+ }
1547
+ };
1548
+ },
1549
+ doSuspendTurn: async () => {
1550
+ if (stopped) throw new Error(`OpenCode session ${sessionId} is stopped; cannot suspend.`);
1551
+ stopped = true;
1552
+ const lastSeenEventId = await channel.suspend();
1553
+ return {
1554
+ type: "continue-turn",
1555
+ harnessId: "opencode",
1556
+ specificationVersion: "harness-v1",
1557
+ data: {
1558
+ ...latestOpenCodeSessionId ? { openCodeSessionId: latestOpenCodeSessionId } : {},
1559
+ ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1560
+ bridge: {
1561
+ port: bridgePort,
1562
+ token: bridgeToken,
1563
+ lastSeenEventId,
1564
+ ...sandboxId == null ? {} : { sandboxId }
1565
+ }
1566
+ }
1567
+ };
1568
+ }
1569
+ };
1994
1570
  }
1995
- async function runCompactOperation({
1996
- channel,
1997
- model,
1998
- provider,
1999
- permissionMode,
2000
- debug,
2001
- openCodeConfig,
2002
- mcpServers,
2003
- headers,
2004
- resumeSessionId,
2005
- onCompaction
2006
- }) {
2007
- let pendingResolve;
2008
- let pendingReject;
2009
- const done = new Promise((resolve, reject) => {
2010
- pendingResolve = resolve;
2011
- pendingReject = reject;
2012
- });
2013
- const unsubs = [
2014
- channel.on("compaction", (msg) => onCompaction(msg)),
2015
- channel.on("finish", () => {
2016
- for (const u of unsubs) u();
2017
- pendingResolve();
2018
- }),
2019
- channel.on("error", (msg) => {
2020
- for (const u of unsubs) u();
2021
- pendingReject(msg.error);
2022
- })
2023
- ];
2024
- channel.send({
2025
- type: "start",
2026
- operation: "compact",
2027
- prompt: "",
2028
- tools: [],
2029
- model,
2030
- provider,
2031
- ...openCodeConfig == null ? {} : { openCodeConfig },
2032
- ...mcpServers == null ? {} : { mcpServers },
2033
- ...headers == null ? {} : { headers },
2034
- ...permissionMode ? { permissionMode } : {},
2035
- ...resumeSessionId ? { resumeSessionId } : {},
2036
- ...debug ? { debug } : {}
2037
- });
2038
- await done;
1571
+ async function runCompactOperation({ channel, model, provider, permissionMode, debug, openCodeConfig, mcpServers, headers, resumeSessionId, onCompaction }) {
1572
+ let pendingResolve;
1573
+ let pendingReject;
1574
+ const done = new Promise((resolve, reject) => {
1575
+ pendingResolve = resolve;
1576
+ pendingReject = reject;
1577
+ });
1578
+ const unsubs = [
1579
+ channel.on("compaction", (msg) => onCompaction(msg)),
1580
+ channel.on("finish", () => {
1581
+ for (const u of unsubs) u();
1582
+ pendingResolve();
1583
+ }),
1584
+ channel.on("error", (msg) => {
1585
+ for (const u of unsubs) u();
1586
+ pendingReject(msg.error);
1587
+ })
1588
+ ];
1589
+ channel.send({
1590
+ type: "start",
1591
+ operation: "compact",
1592
+ prompt: "",
1593
+ tools: [],
1594
+ model,
1595
+ provider,
1596
+ ...openCodeConfig == null ? {} : { openCodeConfig },
1597
+ ...mcpServers == null ? {} : { mcpServers },
1598
+ ...headers == null ? {} : { headers },
1599
+ ...permissionMode ? { permissionMode } : {},
1600
+ ...resumeSessionId ? { resumeSessionId } : {},
1601
+ ...debug ? { debug } : {}
1602
+ });
1603
+ await done;
2039
1604
  }
2040
1605
  function extractUserText(prompt) {
2041
- if (typeof prompt === "string") return prompt;
2042
- const { content } = prompt;
2043
- if (typeof content === "string") return content;
2044
- const parts = [];
2045
- for (const part of content) {
2046
- if (part.type !== "text") {
2047
- throw new HarnessCapabilityUnsupportedError2({
2048
- harnessId: "opencode",
2049
- message: `The OpenCode harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`
2050
- });
2051
- }
2052
- parts.push(part.text);
2053
- }
2054
- return parts.join("\n\n");
1606
+ if (typeof prompt === "string") return prompt;
1607
+ const { content } = prompt;
1608
+ if (typeof content === "string") return content;
1609
+ const parts = [];
1610
+ for (const part of content) {
1611
+ if (part.type !== "text") throw new HarnessCapabilityUnsupportedError({
1612
+ harnessId: "opencode",
1613
+ message: `The OpenCode harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`
1614
+ });
1615
+ parts.push(part.text);
1616
+ }
1617
+ return parts.join("\n\n");
2055
1618
  }
1619
+ //#endregion
1620
+ //#region src/index.ts
1621
+ /**
1622
+ * Default `openCode` harness instance with no overrides. Equivalent to
1623
+ * `createOpenCode()`.
1624
+ */
1625
+ const openCode = createOpenCode();
1626
+ //#endregion
1627
+ export { VERSION, createOpenCode, openCode };
2056
1628
 
2057
- // src/index.ts
2058
- var openCode = createOpenCode();
2059
- export {
2060
- VERSION,
2061
- createOpenCode,
2062
- openCode
2063
- };
2064
1629
  //# sourceMappingURL=index.js.map