@ai-sdk/harness-claude-code 1.0.74 → 1.0.76

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.
@@ -348,6 +348,15 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
348
348
  systemPrompt: createClaudeCodeSystemPrompt(start.instructions),
349
349
  thinking: start.thinking,
350
350
  ...(start.effort !== undefined ? { effort: start.effort } : {}),
351
+ ...(start.responseFormat?.type === 'json' &&
352
+ start.responseFormat.schema != null
353
+ ? {
354
+ outputFormat: {
355
+ type: 'json_schema' as const,
356
+ schema: start.responseFormat.schema,
357
+ },
358
+ }
359
+ : {}),
351
360
  includePartialMessages: true,
352
361
  // The `PostCompact` hook carries the compaction summary, which the
353
362
  // `compact_boundary` system message does not. Latch it for the unified
@@ -425,6 +434,20 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
425
434
  if (typeof msg.total_cost_usd === 'number') {
426
435
  totalCostUsd = (totalCostUsd ?? 0) + msg.total_cost_usd;
427
436
  }
437
+ if (
438
+ start.responseFormat?.type === 'json' &&
439
+ msg.structured_output !== undefined
440
+ ) {
441
+ const id = randomUUID();
442
+ emit({ type: 'text-start', id });
443
+ emit({
444
+ type: 'text-delta',
445
+ id,
446
+ delta: JSON.stringify(msg.structured_output),
447
+ });
448
+ emit({ type: 'text-end', id });
449
+ streamEventState.stepOpen = true;
450
+ }
428
451
  if (streamEventState.stepOpen) {
429
452
  emitFinishStep({
430
453
  state: streamEventState,
@@ -43,7 +43,7 @@ function toZodType(schema: JsonSchemaObject | undefined): z.ZodTypeAny {
43
43
  let zType =
44
44
  zodForConst(schema) ??
45
45
  zodForEnum(schema) ??
46
- zodForNullableUnion(schema) ??
46
+ zodForUnion(schema) ??
47
47
  zodForType(schema);
48
48
 
49
49
  if (isNullable(schema)) zType = zType.nullable();
@@ -90,16 +90,16 @@ function zodForEnum(schema: JsonSchemaObject): z.ZodTypeAny | undefined {
90
90
  return zodForLiterals(schema.enum);
91
91
  }
92
92
 
93
- function zodForNullableUnion(
94
- schema: JsonSchemaObject,
95
- ): z.ZodTypeAny | undefined {
93
+ function zodForUnion(schema: JsonSchemaObject): z.ZodTypeAny | undefined {
96
94
  const unionSchemas = schema.anyOf ?? schema.oneOf;
97
95
  if (!unionSchemas || unionSchemas.length < 2) return undefined;
98
96
 
99
- const nonNullSchemas = unionSchemas.filter(item => !isNullOnlySchema(item));
100
- if (nonNullSchemas.length !== 1) return undefined;
101
-
102
- return toZodType(nonNullSchemas[0]).nullable();
97
+ const options = unionSchemas.map(item => toZodType(item)) as unknown as [
98
+ z.ZodTypeAny,
99
+ z.ZodTypeAny,
100
+ ...z.ZodTypeAny[],
101
+ ];
102
+ return z.union(options);
103
103
  }
104
104
 
105
105
  function zodForLiterals(values: JsonLiteral[]): z.ZodTypeAny {
@@ -127,19 +127,6 @@ function isNullable(schema: JsonSchemaObject): boolean {
127
127
  );
128
128
  }
129
129
 
130
- function isNullOnlySchema(schema: JsonSchemaObject): boolean {
131
- return (
132
- schema.type === 'null' ||
133
- (Array.isArray(schema.type) &&
134
- schema.type.length === 1 &&
135
- schema.type[0] === 'null') ||
136
- schema.const === null ||
137
- (Array.isArray(schema.enum) &&
138
- schema.enum.length === 1 &&
139
- schema.enum[0] === null)
140
- );
141
- }
142
-
143
130
  function isJsonSchemaObject(input: unknown): input is JsonSchemaObject {
144
131
  return input != null && typeof input === 'object' && !Array.isArray(input);
145
132
  }
@@ -2,9 +2,47 @@ import { execFileSync } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { getAiGatewayAuthFromEnv } from '@ai-sdk/harness/utils';
5
+ import type { HarnessV1RequestTransformation } from '@ai-sdk/harness';
6
+ import {
7
+ createCredentialRequestTransformation,
8
+ getAiGatewayAuthFromEnv,
9
+ } from '@ai-sdk/harness/utils';
6
10
 
7
- export type ClaudeCodeAuthenticationMode = 'auto' | 'direct' | 'ai-gateway';
11
+ export const CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES = [
12
+ 'AI_GATEWAY_API_KEY',
13
+ 'ANTHROPIC_API_KEY',
14
+ 'ANTHROPIC_AUTH_TOKEN',
15
+ ] as const;
16
+
17
+ export function createClaudeCodeRequestTransformations(
18
+ env: Record<string, string>,
19
+ auth: ClaudeCodeResolvedAuthenticationMode,
20
+ ): HarnessV1RequestTransformation[] {
21
+ const headers: Record<string, string> = {};
22
+ if (env.ANTHROPIC_API_KEY) {
23
+ headers['x-api-key'] = env.ANTHROPIC_API_KEY;
24
+ }
25
+ if (env.ANTHROPIC_AUTH_TOKEN) {
26
+ headers.Authorization = `Bearer ${env.ANTHROPIC_AUTH_TOKEN}`;
27
+ }
28
+ return Object.keys(headers).length === 0
29
+ ? []
30
+ : [
31
+ createCredentialRequestTransformation({
32
+ baseUrl:
33
+ auth === 'ai-gateway'
34
+ ? env.ANTHROPIC_BASE_URL
35
+ : (env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com'),
36
+ headers,
37
+ }),
38
+ ];
39
+ }
40
+
41
+ export type ClaudeCodeResolvedAuthenticationMode = 'direct' | 'ai-gateway';
42
+
43
+ export type ClaudeCodeAuthenticationMode =
44
+ | ClaudeCodeResolvedAuthenticationMode
45
+ | 'auto';
8
46
 
9
47
  /**
10
48
  * @deprecated Passing an object to auth options is deprecated. Use a `ClaudeCodeAuthenticationMode` string value ("auto" | "direct" | "ai-gateway") instead, and pass credentials via environment variables.
@@ -79,6 +117,21 @@ export function resolveClaudeCodeEnv(
79
117
  return pickAnthropic({ processEnv, readApiKey });
80
118
  }
81
119
 
120
+ export function resolveClaudeCodeAuthenticationMode(
121
+ auth: ClaudeCodeAuthOptions | undefined,
122
+ processEnv: Record<string, string | undefined> = process.env,
123
+ ): ClaudeCodeResolvedAuthenticationMode {
124
+ if (auth === 'direct' || (typeof auth !== 'string' && auth?.anthropic)) {
125
+ return 'direct';
126
+ }
127
+ if (auth === 'ai-gateway' || (typeof auth !== 'string' && auth?.gateway)) {
128
+ return 'ai-gateway';
129
+ }
130
+ return getAiGatewayAuthFromEnv({ env: processEnv }).apiKey
131
+ ? 'ai-gateway'
132
+ : 'direct';
133
+ }
134
+
82
135
  function normalizeClaudeCodeAuthToLegacyAuth(
83
136
  auth: ClaudeCodeAuthOptions | undefined,
84
137
  ): LegacyClaudeCodeAuthOptions | undefined {
@@ -0,0 +1,65 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import type { HarnessV1Bootstrap } from '@ai-sdk/harness';
4
+
5
+ /*
6
+ * Bootstrap is derived state stored under the sandbox's default working
7
+ * directory so snapshot-capable providers can preserve the installed CLI,
8
+ * bridge, and recipe marker without requiring root filesystem access.
9
+ *
10
+ * The session work dir (`startOpts.sessionWorkDir`) and the bridge-state dir
11
+ * derived from `sandboxSession.defaultWorkingDirectory` both live under the sandbox's
12
+ * default working directory — the provider's persistent mount — so the
13
+ * workdir's CLI state (Claude's `~/.claude/projects/<dir>/*.jsonl` thread
14
+ * history is keyed by working directory) and the bridge state files survive
15
+ * both detach -> attach/replay and stop -> snapshot -> resume cycles.
16
+ */
17
+ export const CLAUDE_CODE_BOOTSTRAP_DIR = '.harness-bootstrap/claude-code';
18
+
19
+ let cachedBootstrap: HarnessV1Bootstrap | undefined;
20
+
21
+ export async function getClaudeCodeBootstrap(): Promise<HarnessV1Bootstrap> {
22
+ if (cachedBootstrap != null) return cachedBootstrap;
23
+ const [pkg, lock, bridge] = await Promise.all([
24
+ readBridgeAsset('package.json'),
25
+ readBridgeAsset('pnpm-lock.yaml'),
26
+ readBridgeAsset('index.mjs'),
27
+ ]);
28
+ cachedBootstrap = {
29
+ harnessId: 'claude-code',
30
+ bootstrapDir: CLAUDE_CODE_BOOTSTRAP_DIR,
31
+ files: [
32
+ { path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/package.json`, content: pkg },
33
+ { path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
34
+ { path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
35
+ ],
36
+ commands: [
37
+ {
38
+ command: 'pnpm install --frozen-lockfile --store-dir .pnpm-store',
39
+ },
40
+ {
41
+ command:
42
+ 'if [ -f node_modules/@anthropic-ai/claude-code/install.cjs ]; then node node_modules/@anthropic-ai/claude-code/install.cjs; fi && ./node_modules/.bin/claude --version',
43
+ },
44
+ ],
45
+ };
46
+ return cachedBootstrap;
47
+ }
48
+
49
+ async function readBridgeAsset(name: string): Promise<string> {
50
+ const candidates = [
51
+ new URL(`./bridge/${name}`, import.meta.url),
52
+ new URL(`../bridge/${name}`, import.meta.url),
53
+ ];
54
+ let lastErr: unknown;
55
+ for (const url of candidates) {
56
+ try {
57
+ return await readFile(fileURLToPath(url), 'utf8');
58
+ } catch (err) {
59
+ const code = (err as NodeJS.ErrnoException).code;
60
+ if (code !== 'ENOENT') throw err;
61
+ lastErr = err;
62
+ }
63
+ }
64
+ throw lastErr ?? new Error(`bridge asset not found: ${name}`);
65
+ }
@@ -1,13 +1,10 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
3
2
  import { posix } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
3
  import {
6
4
  commonTool,
7
5
  harnessV1DiagnosticFromBridgeFrame,
8
6
  HarnessCapabilityUnsupportedError,
9
7
  type HarnessV1,
10
- type HarnessV1Bootstrap,
11
8
  type HarnessV1BuiltinToolFiltering,
12
9
  type HarnessV1BuiltinTool,
13
10
  type HarnessV1ContinueTurnState,
@@ -15,6 +12,7 @@ import {
15
12
  type HarnessV1PermissionMode,
16
13
  type HarnessV1Prompt,
17
14
  type HarnessV1PromptControl,
15
+ type HarnessV1PortEndpoint,
18
16
  type HarnessV1ResumeSessionState,
19
17
  type HarnessV1NetworkSandboxSession,
20
18
  type HarnessV1Session,
@@ -28,9 +26,11 @@ import {
28
26
  drainBridgeProcessStream,
29
27
  forwardBridgeProcessStream,
30
28
  markBridgeStarting,
29
+ maskSandboxCredentials,
31
30
  resolveSandboxHomeDir,
32
31
  SandboxChannel,
33
32
  shellQuote,
33
+ warnCredentialBrokeringUnavailable,
34
34
  waitForBridgeReady,
35
35
  writeSkills as writeHarnessSkills,
36
36
  } from '@ai-sdk/harness/utils';
@@ -43,6 +43,13 @@ import {
43
43
  import { WebSocket } from 'ws';
44
44
  import { z } from 'zod/v4';
45
45
  import {
46
+ CLAUDE_CODE_BOOTSTRAP_DIR as BOOTSTRAP_DIR,
47
+ getClaudeCodeBootstrap,
48
+ } from './claude-code-bootstrap';
49
+ import {
50
+ CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
51
+ createClaudeCodeRequestTransformations,
52
+ resolveClaudeCodeAuthenticationMode,
46
53
  resolveClaudeCodeEnv,
47
54
  type ClaudeCodeAuthOptions,
48
55
  } from './claude-code-auth';
@@ -753,20 +760,6 @@ const CLAUDE_CODE_BUILTIN_TOOLS = {
753
760
  },
754
761
  } as const satisfies Record<string, HarnessV1BuiltinTool<any, any>>;
755
762
 
756
- /*
757
- * Bootstrap is derived state stored under the sandbox's default working
758
- * directory so snapshot-capable providers can preserve the installed CLI,
759
- * bridge, and recipe marker without requiring root filesystem access.
760
- *
761
- * The session work dir (`startOpts.sessionWorkDir`) and the bridge-state dir
762
- * derived from `sandboxSession.defaultWorkingDirectory` both live under the sandbox's
763
- * default working directory — the provider's persistent mount — so the
764
- * workdir's CLI state (Claude's `~/.claude/projects/<dir>/*.jsonl` thread
765
- * history is keyed by working directory) and the bridge state files survive
766
- * both detach -> attach/replay and stop -> snapshot -> resume cycles.
767
- */
768
- const BOOTSTRAP_DIR = '.harness-bootstrap/claude-code';
769
-
770
763
  /**
771
764
  * Live bridge coordinates returned by `doDetach()` and `doSuspendTurn()`. A
772
765
  * future process uses them to reopen a socket to the still-running bridge
@@ -805,7 +798,6 @@ export function createClaudeCode(
805
798
  'Claude Code MCP server name "harness-tools" is reserved for HarnessAgent tools.',
806
799
  );
807
800
  }
808
- let cachedBootstrap: HarnessV1Bootstrap | undefined;
809
801
  const thinking = settings.thinking ?? {
810
802
  type: 'adaptive',
811
803
  display: 'summarized',
@@ -818,35 +810,41 @@ export function createClaudeCode(
818
810
  supportsBuiltinToolApprovals: true,
819
811
  supportsBuiltinToolFiltering: true,
820
812
  lifecycleStateSchema: claudeCodeResumeStateSchema,
821
- getBootstrap: async () => {
822
- if (cachedBootstrap != null) return cachedBootstrap;
823
- const [pkg, lock, bridge] = await Promise.all([
824
- readBridgeAsset('package.json'),
825
- readBridgeAsset('pnpm-lock.yaml'),
826
- readBridgeAsset('index.mjs'),
827
- ]);
828
- cachedBootstrap = {
829
- harnessId: 'claude-code',
830
- bootstrapDir: BOOTSTRAP_DIR,
831
- files: [
832
- { path: `${BOOTSTRAP_DIR}/package.json`, content: pkg },
833
- { path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
834
- { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
835
- ],
836
- commands: [
837
- {
838
- command: 'pnpm install --frozen-lockfile --store-dir .pnpm-store',
839
- },
840
- {
841
- command:
842
- 'if [ -f node_modules/@anthropic-ai/claude-code/install.cjs ]; then node node_modules/@anthropic-ai/claude-code/install.cjs; fi && ./node_modules/.bin/claude --version',
843
- },
844
- ],
845
- };
846
- return cachedBootstrap;
847
- },
813
+ getBootstrap: getClaudeCodeBootstrap,
848
814
  doStart: async startOpts => {
849
815
  const sandboxSession = startOpts.sandboxSession;
816
+ const authenticationMode = resolveClaudeCodeAuthenticationMode(
817
+ settings.auth,
818
+ );
819
+ const resolvedAuthEnvironment = resolveClaudeCodeEnv(settings.auth);
820
+ let authEnv = resolvedAuthEnvironment;
821
+ let sandboxTurnEnvironment = settings.env;
822
+ if (sandboxSession.addRequestTransformations != null) {
823
+ const requestTransformations = createClaudeCodeRequestTransformations(
824
+ {
825
+ ...resolvedAuthEnvironment,
826
+ ...settings.env,
827
+ },
828
+ authenticationMode,
829
+ );
830
+ if (requestTransformations.length > 0) {
831
+ await sandboxSession.addRequestTransformations(
832
+ requestTransformations,
833
+ );
834
+ }
835
+ authEnv = maskSandboxCredentials({
836
+ environment: resolvedAuthEnvironment,
837
+ credentialEnvironmentVariables:
838
+ CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
839
+ });
840
+ sandboxTurnEnvironment = maskSandboxCredentials({
841
+ environment: settings.env ?? {},
842
+ credentialEnvironmentVariables:
843
+ CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
844
+ });
845
+ } else {
846
+ warnCredentialBrokeringUnavailable();
847
+ }
850
848
  const session = sandboxSession.restricted();
851
849
  const sandboxId = sandboxSession.id;
852
850
  const bootstrapDir = posix.resolve(
@@ -886,9 +884,10 @@ export function createClaudeCode(
886
884
  // Builds the `connect` thunk a `SandboxChannel` re-invokes on every
887
885
  // (re)connect: open the socket, then wait for `bridge-hello` so the
888
886
  // end-to-end link is proven live before any frame is sent.
889
- const buildConnect = (wsUrl: string) => async (): Promise<WebSocket> => {
890
- return openBridgeWebSocket({ wsUrl, timeoutMs });
891
- };
887
+ const buildConnect =
888
+ (endpoint: HarnessV1PortEndpoint) => async (): Promise<WebSocket> => {
889
+ return openBridgeWebSocket({ endpoint, timeoutMs });
890
+ };
892
891
 
893
892
  /*
894
893
  * Rung 1 — ATTACH. When lifecycle state carries live bridge coordinates,
@@ -900,13 +899,16 @@ export function createClaudeCode(
900
899
  */
901
900
  if (coords) {
902
901
  try {
903
- const attachUrl =
904
- (await sandboxSession.getPortUrl({
905
- port: coords.port,
906
- protocol: 'ws',
907
- })) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
902
+ const endpoint = await sandboxSession.getPortEndpoint({
903
+ port: coords.port,
904
+ protocol: 'ws',
905
+ });
906
+ const attachEndpoint = withBridgeToken({
907
+ endpoint,
908
+ token: coords.token,
909
+ });
908
910
  const attachChannel: ClaudeCodeChannel = new SandboxChannel({
909
- connect: buildConnect(attachUrl),
911
+ connect: buildConnect(attachEndpoint),
910
912
  outboundSchema: outboundMessageSchema,
911
913
  initialLastSeenEventId: coords.lastSeenEventId,
912
914
  onDiagnostic,
@@ -922,7 +924,7 @@ export function createClaudeCode(
922
924
  proc: undefined,
923
925
  model: settings.model,
924
926
  maxTurns: settings.maxTurns,
925
- env: settings.env,
927
+ env: sandboxTurnEnvironment,
926
928
  thinking,
927
929
  effort: settings.effort,
928
930
  isResume: true,
@@ -978,7 +980,6 @@ export function createClaudeCode(
978
980
  settings.mintBridgeToken == null
979
981
  ? randomBytes(32).toString('hex')
980
982
  : settings.mintBridgeToken(sandboxId);
981
- const authEnv = resolveClaudeCodeEnv(settings.auth);
982
983
  const env = {
983
984
  ...authEnv,
984
985
  /*
@@ -1070,14 +1071,14 @@ export function createClaudeCode(
1070
1071
  });
1071
1072
  void drainBridgeProcessStream(proc.stdout);
1072
1073
 
1073
- const wsUrl =
1074
- (await sandboxSession.getPortUrl({
1075
- port: boundPort,
1076
- protocol: 'ws',
1077
- })) + `?agent_bridge_token=${encodeURIComponent(token)}`;
1074
+ const endpoint = await sandboxSession.getPortEndpoint({
1075
+ port: boundPort,
1076
+ protocol: 'ws',
1077
+ });
1078
+ const bridgeEndpoint = withBridgeToken({ endpoint, token });
1078
1079
 
1079
1080
  const channel: ClaudeCodeChannel = new SandboxChannel({
1080
- connect: buildConnect(wsUrl),
1081
+ connect: buildConnect(bridgeEndpoint),
1081
1082
  outboundSchema: outboundMessageSchema,
1082
1083
  onDiagnostic,
1083
1084
  onBridgeError,
@@ -1098,7 +1099,7 @@ export function createClaudeCode(
1098
1099
  proc,
1099
1100
  model: settings.model,
1100
1101
  maxTurns: settings.maxTurns,
1101
- env: settings.env,
1102
+ env: sandboxTurnEnvironment,
1102
1103
  thinking,
1103
1104
  effort: settings.effort,
1104
1105
  isResume: respawnStrategy !== undefined,
@@ -1162,24 +1163,6 @@ async function writeClaudeCodeSkills({
1162
1163
  });
1163
1164
  }
1164
1165
 
1165
- async function readBridgeAsset(name: string): Promise<string> {
1166
- const candidates = [
1167
- new URL(`./bridge/${name}`, import.meta.url),
1168
- new URL(`../bridge/${name}`, import.meta.url),
1169
- ];
1170
- let lastErr: unknown;
1171
- for (const url of candidates) {
1172
- try {
1173
- return await readFile(fileURLToPath(url), 'utf8');
1174
- } catch (err) {
1175
- const code = (err as NodeJS.ErrnoException).code;
1176
- if (code !== 'ENOENT') throw err;
1177
- lastErr = err;
1178
- }
1179
- }
1180
- throw lastErr ?? new Error(`bridge asset not found: ${name}`);
1181
- }
1182
-
1183
1166
  /**
1184
1167
  * Wait for the bridge's `bridge-hello` message to arrive on the freshly
1185
1168
  * opened WebSocket before any other host-side code touches it.
@@ -1192,16 +1175,18 @@ async function readBridgeAsset(name: string): Promise<string> {
1192
1175
  * is the only reliable evidence that the end-to-end link is live.
1193
1176
  */
1194
1177
  function openWebSocketAndWaitForBridgeHello({
1195
- url,
1178
+ endpoint,
1196
1179
  openTimeoutMs,
1197
1180
  getHelloTimeoutMs,
1198
1181
  }: {
1199
- url: string;
1182
+ endpoint: HarnessV1PortEndpoint;
1200
1183
  openTimeoutMs: number;
1201
1184
  getHelloTimeoutMs: () => number;
1202
1185
  }): Promise<WebSocket> {
1203
1186
  return new Promise<WebSocket>((resolve, reject) => {
1204
- const ws = new WebSocket(url);
1187
+ const ws = new WebSocket(endpoint.url, {
1188
+ headers: endpoint.headers == null ? undefined : { ...endpoint.headers },
1189
+ });
1205
1190
  let opened = false;
1206
1191
  let sawBridgeHello = false;
1207
1192
  let settled = false;
@@ -1293,10 +1278,10 @@ function openWebSocketAndWaitForBridgeHello({
1293
1278
  }
1294
1279
 
1295
1280
  async function openBridgeWebSocket({
1296
- wsUrl,
1281
+ endpoint,
1297
1282
  timeoutMs,
1298
1283
  }: {
1299
- wsUrl: string;
1284
+ endpoint: HarnessV1PortEndpoint;
1300
1285
  timeoutMs: number;
1301
1286
  }): Promise<WebSocket> {
1302
1287
  const deadline = Date.now() + timeoutMs;
@@ -1308,7 +1293,7 @@ async function openBridgeWebSocket({
1308
1293
  try {
1309
1294
  const remaining = Math.max(1, deadline - Date.now());
1310
1295
  return await openWebSocketAndWaitForBridgeHello({
1311
- url: wsUrl,
1296
+ endpoint,
1312
1297
  openTimeoutMs: Math.min(10_000, remaining),
1313
1298
  getHelloTimeoutMs: () =>
1314
1299
  Math.min(5_000, Math.max(1, deadline - Date.now())),
@@ -1339,6 +1324,18 @@ function webSocketMessageToString(raw: unknown): string {
1339
1324
  return String(raw);
1340
1325
  }
1341
1326
 
1327
+ function withBridgeToken({
1328
+ endpoint,
1329
+ token,
1330
+ }: {
1331
+ endpoint: HarnessV1PortEndpoint;
1332
+ token: string;
1333
+ }): HarnessV1PortEndpoint {
1334
+ const bridgeUrl = new URL(endpoint.url);
1335
+ bridgeUrl.searchParams.set('agent_bridge_token', token);
1336
+ return { ...endpoint, url: bridgeUrl.toString() };
1337
+ }
1338
+
1342
1339
  function sleep(ms: number): Promise<void> {
1343
1340
  return new Promise(resolve => {
1344
1341
  const timer = setTimeout(resolve, ms);
@@ -1543,6 +1540,16 @@ function createSession({
1543
1540
  isResume,
1544
1541
  modelId: model,
1545
1542
  doPromptTurn: async promptOpts => {
1543
+ if (
1544
+ promptOpts.responseFormat?.type === 'json' &&
1545
+ promptOpts.responseFormat.schema == null
1546
+ ) {
1547
+ throw new HarnessCapabilityUnsupportedError({
1548
+ message:
1549
+ "Harness 'claude-code' requires a JSON schema for structured output.",
1550
+ harnessId: 'claude-code',
1551
+ });
1552
+ }
1546
1553
  const control = wireTurn({
1547
1554
  emit: promptOpts.emit,
1548
1555
  abortSignal: promptOpts.abortSignal,
@@ -1556,6 +1563,9 @@ function createSession({
1556
1563
  description: t.description,
1557
1564
  inputSchema: t.inputSchema,
1558
1565
  })),
1566
+ ...(promptOpts.responseFormat == null
1567
+ ? {}
1568
+ : { responseFormat: promptOpts.responseFormat }),
1559
1569
  ...(promptOpts.instructions
1560
1570
  ? { instructions: promptOpts.instructions }
1561
1571
  : {}),
@@ -1579,6 +1589,16 @@ function createSession({
1579
1589
  return control;
1580
1590
  },
1581
1591
  doContinueTurn: async continueOpts => {
1592
+ if (
1593
+ continueOpts.responseFormat?.type === 'json' &&
1594
+ continueOpts.responseFormat.schema == null
1595
+ ) {
1596
+ throw new HarnessCapabilityUnsupportedError({
1597
+ message:
1598
+ "Harness 'claude-code' requires a JSON schema for structured output.",
1599
+ harnessId: 'claude-code',
1600
+ });
1601
+ }
1582
1602
  const control = wireTurn({
1583
1603
  emit: continueOpts.emit,
1584
1604
  abortSignal: continueOpts.abortSignal,
@@ -1614,6 +1634,9 @@ function createSession({
1614
1634
  description: t.description,
1615
1635
  inputSchema: t.inputSchema,
1616
1636
  })),
1637
+ ...(continueOpts.responseFormat == null
1638
+ ? {}
1639
+ : { responseFormat: continueOpts.responseFormat }),
1617
1640
  ...(continueOpts.instructions
1618
1641
  ? { instructions: continueOpts.instructions }
1619
1642
  : {}),