@ai-sdk/harness-claude-code 1.0.95 → 1.0.97

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.
@@ -3,12 +3,14 @@ import { readFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import type {
6
+ HarnessV1Authentication,
6
7
  HarnessV1RequestTransformation,
7
8
  HarnessV1RequestTransformationSources,
8
9
  } from '@ai-sdk/harness';
9
10
  import {
10
11
  createCredentialRequestTransformation,
11
12
  getAiGatewayAuthFromEnv,
13
+ isHarnessAuthenticationEnvironment,
12
14
  } from '@ai-sdk/harness/utils';
13
15
 
14
16
  export const CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES = [
@@ -67,36 +69,14 @@ export function createClaudeCodeRequestTransformations({
67
69
 
68
70
  export type ClaudeCodeResolvedAuthenticationMode = 'direct' | 'ai-gateway';
69
71
 
70
- export type ClaudeCodeAuthenticationMode =
71
- | ClaudeCodeResolvedAuthenticationMode
72
- | 'auto';
73
-
74
- /**
75
- * @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.
76
- */
77
- export type LegacyClaudeCodeAuthOptions = {
78
- readonly anthropic?: {
79
- readonly apiKey?: string;
80
- readonly authToken?: string;
81
- readonly baseUrl?: string;
82
- };
83
- readonly gateway?: {
84
- readonly apiKey?: string;
85
- readonly baseUrl?: string;
86
- };
87
- };
88
-
89
- export type ClaudeCodeAuthOptions =
90
- | ClaudeCodeAuthenticationMode
91
- | LegacyClaudeCodeAuthOptions;
72
+ export type ClaudeCodeAuthenticationMode = HarnessV1Authentication;
92
73
 
93
74
  /**
94
75
  * Resolve the environment-variable blob the bridge needs to authenticate
95
76
  * with Anthropic (directly or via the Vercel AI Gateway). Precedence:
96
77
  *
97
- * 1. Explicit `auth.anthropic` — pin to direct Anthropic auth.
98
- * 2. Explicit `auth.gateway` — pin to gateway-routed auth.
99
- * 3. Auto-detect from the host process env: gateway first
78
+ * 1. An explicit authentication mode pins the selected route.
79
+ * 2. Auto-detect from the host process env: gateway first
100
80
  * (`AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN`), then direct
101
81
  * (`ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`).
102
82
  */
@@ -112,46 +92,50 @@ export type ResolveClaudeCodeEnvOptions = {
112
92
  };
113
93
 
114
94
  export function resolveClaudeCodeEnv(
115
- auth: ClaudeCodeAuthOptions | undefined,
95
+ auth: ClaudeCodeAuthenticationMode | undefined,
116
96
  processEnv: Record<string, string | undefined> = process.env,
117
97
  options: ResolveClaudeCodeEnvOptions = {},
118
98
  ): Record<string, string> {
119
- const normalizedAuth = normalizeClaudeCodeAuthToLegacyAuth(auth);
120
-
121
- const readApiKey = options.readApiKeyHelper ?? readApiKeyHelper;
122
- if (normalizedAuth?.anthropic) {
99
+ const suppliedEnvironment = isHarnessAuthenticationEnvironment(auth);
100
+ const authenticationEnvironment = suppliedEnvironment ? auth : processEnv;
101
+
102
+ const readApiKey =
103
+ suppliedEnvironment || auth === 'direct'
104
+ ? () => undefined
105
+ : (options.readApiKeyHelper ?? readApiKeyHelper);
106
+ if (auth === 'direct') {
123
107
  return pickAnthropic({
124
- explicit: normalizedAuth.anthropic,
125
- processEnv,
108
+ processEnv: authenticationEnvironment,
126
109
  readApiKey,
127
110
  });
128
111
  }
129
112
 
130
- const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env: processEnv });
131
- if (normalizedAuth?.gateway) {
132
- return pickGateway({
133
- explicit: normalizedAuth.gateway,
134
- gatewayAuthFromEnv,
135
- });
136
- }
137
- if (gatewayAuthFromEnv.apiKey) {
138
- return pickGateway({
139
- explicit: {},
140
- gatewayAuthFromEnv,
141
- });
113
+ const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({
114
+ env: authenticationEnvironment,
115
+ });
116
+ if (auth === 'ai-gateway' || gatewayAuthFromEnv.apiKey) {
117
+ return pickGateway({ gatewayAuthFromEnv });
142
118
  }
143
119
 
144
- return pickAnthropic({ processEnv, readApiKey });
120
+ return pickAnthropic({
121
+ processEnv: authenticationEnvironment,
122
+ readApiKey,
123
+ });
145
124
  }
146
125
 
147
126
  export function resolveClaudeCodeAuthenticationMode(
148
- auth: ClaudeCodeAuthOptions | undefined,
127
+ auth: ClaudeCodeAuthenticationMode | undefined,
149
128
  processEnv: Record<string, string | undefined> = process.env,
150
129
  ): ClaudeCodeResolvedAuthenticationMode {
151
- if (auth === 'direct' || (typeof auth !== 'string' && auth?.anthropic)) {
130
+ if (isHarnessAuthenticationEnvironment(auth)) {
131
+ return getAiGatewayAuthFromEnv({ env: auth }).apiKey
132
+ ? 'ai-gateway'
133
+ : 'direct';
134
+ }
135
+ if (auth === 'direct') {
152
136
  return 'direct';
153
137
  }
154
- if (auth === 'ai-gateway' || (typeof auth !== 'string' && auth?.gateway)) {
138
+ if (auth === 'ai-gateway') {
155
139
  return 'ai-gateway';
156
140
  }
157
141
  return getAiGatewayAuthFromEnv({ env: processEnv }).apiKey
@@ -159,46 +143,20 @@ export function resolveClaudeCodeAuthenticationMode(
159
143
  : 'direct';
160
144
  }
161
145
 
162
- function normalizeClaudeCodeAuthToLegacyAuth(
163
- auth: ClaudeCodeAuthOptions | undefined,
164
- ): LegacyClaudeCodeAuthOptions | undefined {
165
- if (auth == null || auth === 'auto') {
166
- return undefined;
167
- }
168
- if (typeof auth === 'string') {
169
- switch (auth) {
170
- case 'direct':
171
- return { anthropic: {} };
172
- case 'ai-gateway':
173
- return { gateway: {} };
174
- default:
175
- return undefined;
176
- }
177
- }
178
-
179
- console.warn(
180
- '[claude-code] Passing an object to auth options is deprecated. Use a string mode ("auto" | "direct" | "ai-gateway") instead, and pass credentials via environment variables.',
181
- );
182
- return auth;
183
- }
184
-
185
146
  function pickAnthropic({
186
- explicit,
187
147
  processEnv,
188
148
  readApiKey,
189
149
  }: {
190
- explicit?: NonNullable<LegacyClaudeCodeAuthOptions['anthropic']>;
191
150
  processEnv: Record<string, string | undefined>;
192
151
  readApiKey: () => string | undefined;
193
152
  }): Record<string, string> {
194
153
  const env: Record<string, string> = {};
195
- const helperKey = explicit ? undefined : readApiKey();
196
- const apiKey = explicit?.apiKey ?? processEnv.ANTHROPIC_API_KEY ?? helperKey;
197
- const authToken =
198
- explicit?.authToken ?? processEnv.ANTHROPIC_AUTH_TOKEN ?? helperKey;
154
+ const helperKey = readApiKey();
155
+ const apiKey = processEnv.ANTHROPIC_API_KEY ?? helperKey;
156
+ const authToken = processEnv.ANTHROPIC_AUTH_TOKEN ?? helperKey;
199
157
  if (apiKey) env.ANTHROPIC_API_KEY = apiKey;
200
158
  if (authToken) env.ANTHROPIC_AUTH_TOKEN = authToken;
201
- const baseUrl = explicit?.baseUrl ?? processEnv.ANTHROPIC_BASE_URL;
159
+ const baseUrl = processEnv.ANTHROPIC_BASE_URL;
202
160
  if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl;
203
161
  return env;
204
162
  }
@@ -240,14 +198,12 @@ function readApiKeyHelper(): string | undefined {
240
198
  }
241
199
 
242
200
  function pickGateway({
243
- explicit,
244
201
  gatewayAuthFromEnv,
245
202
  }: {
246
- explicit: NonNullable<LegacyClaudeCodeAuthOptions['gateway']>;
247
203
  gatewayAuthFromEnv: ReturnType<typeof getAiGatewayAuthFromEnv>;
248
204
  }): Record<string, string> {
249
- const apiKey = explicit.apiKey ?? gatewayAuthFromEnv.apiKey;
250
- const baseUrl = explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl;
205
+ const apiKey = gatewayAuthFromEnv.apiKey;
206
+ const baseUrl = gatewayAuthFromEnv.baseUrl;
251
207
  const env: Record<string, string> = {};
252
208
  if (apiKey) {
253
209
  env.AI_GATEWAY_API_KEY = apiKey;
@@ -56,7 +56,7 @@ import {
56
56
  createClaudeCodeRequestTransformations,
57
57
  resolveClaudeCodeAuthenticationMode,
58
58
  resolveClaudeCodeEnv,
59
- type ClaudeCodeAuthOptions,
59
+ type ClaudeCodeAuthenticationMode,
60
60
  } from './claude-code-auth';
61
61
  import {
62
62
  outboundMessageSchema,
@@ -75,7 +75,7 @@ type ClaudeCodeRespawnStrategy = 'replay' | 'rerun';
75
75
  const CLAUDE_CODE_CLIENT_APP = `ai-sdk/harness-claude-code/${VERSION}`;
76
76
 
77
77
  export type ClaudeCodeHarnessSettings = {
78
- readonly auth?: ClaudeCodeAuthOptions;
78
+ readonly auth?: ClaudeCodeAuthenticationMode;
79
79
  /**
80
80
  * Customizes each credential value before it is forwarded into a sandbox
81
81
  * process. This does not restrict which credentials the harness adapter can
@@ -90,6 +90,8 @@ export type ClaudeCodeHarnessSettings = {
90
90
  /**
91
91
  * Anthropic model id the underlying `claude` CLI should use. Leaving this
92
92
  * unset defers to the CLI's default.
93
+ *
94
+ * @deprecated Use `model` on `HarnessAgent` instead.
93
95
  */
94
96
  readonly model?: string;
95
97
  /**
@@ -829,6 +831,7 @@ export function createClaudeCode(
829
831
  lifecycleStateSchema: claudeCodeResumeStateSchema,
830
832
  getBootstrap: getClaudeCodeBootstrap,
831
833
  doStart: async startOpts => {
834
+ const model = startOpts.model ?? settings.model;
832
835
  const sandboxSession = startOpts.sandboxSession;
833
836
  const toolSafeSandboxSession =
834
837
  getRestrictedSandboxSession(sandboxSession);
@@ -920,6 +923,10 @@ export function createClaudeCode(
920
923
  );
921
924
 
922
925
  const workDir = startOpts.sessionWorkDir;
926
+ const sandboxHomeDir = await resolveSandboxHomeDir({
927
+ sandbox: toolSafeSandboxSession,
928
+ abortSignal: startOpts.abortSignal,
929
+ });
923
930
  const sessionDataDir = `${defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
924
931
  const bridgeStateDir = `${sessionDataDir}/bridge`;
925
932
  const timeoutMs = settings.startupTimeoutMs ?? 120_000;
@@ -991,7 +998,7 @@ export function createClaudeCode(
991
998
  // process handle. The session lifecycle method decides whether the
992
999
  // sandbox is left running, stopped, or destroyed.
993
1000
  proc: undefined,
994
- model: settings.model,
1001
+ model,
995
1002
  maxTurns: settings.maxTurns,
996
1003
  env: sandboxClaudeEnvironment,
997
1004
  thinking,
@@ -1006,7 +1013,8 @@ export function createClaudeCode(
1006
1013
  debug: startOpts.observability?.debug,
1007
1014
  permissionMode: startOpts.permissionMode,
1008
1015
  builtinToolFiltering: startOpts.builtinToolFiltering,
1009
- skills: startOpts.skills ?? [],
1016
+ sandbox: toolSafeSandboxSession,
1017
+ sandboxHomeDir,
1010
1018
  mcpServers: settings.mcpServers,
1011
1019
  supportsUserMessageResponses: () => supportsUserMessageResponses,
1012
1020
  });
@@ -1039,13 +1047,6 @@ export function createClaudeCode(
1039
1047
  }
1040
1048
  }
1041
1049
 
1042
- const sandboxHomeDir =
1043
- startOpts.skills && startOpts.skills.length > 0
1044
- ? await resolveSandboxHomeDir({
1045
- sandbox: toolSafeSandboxSession,
1046
- abortSignal: startOpts.abortSignal,
1047
- })
1048
- : undefined;
1049
1050
  const port = resolveBridgePort({
1050
1051
  sandboxSession,
1051
1052
  override: settings.port,
@@ -1057,35 +1058,21 @@ export function createClaudeCode(
1057
1058
  const env = {
1058
1059
  BRIDGE_CHANNEL_TOKEN: token,
1059
1060
  BRIDGE_WS_PORT: String(port),
1060
- ...(sandboxHomeDir ? { HOME: sandboxHomeDir } : {}),
1061
1061
  ...(respawnStrategy === 'replay'
1062
1062
  ? { BRIDGE_REPLAY_FROM_DISK: '1' }
1063
1063
  : {}),
1064
1064
  };
1065
1065
 
1066
1066
  /*
1067
- * On a fresh start the workdir, skill files, and bridge-state directory
1068
- * must be created. On any resume they already exist in the
1069
- * sandbox snapshot, so skip the rewrite. The env is sent fresh on every
1070
- * spawn — `BRIDGE_CHANNEL_TOKEN` rotates per start.
1067
+ * On a fresh start the workdir and bridge-state directory must be
1068
+ * created. The env is sent fresh on every spawn —
1069
+ * `BRIDGE_CHANNEL_TOKEN` rotates per start.
1071
1070
  */
1072
1071
  if (respawnStrategy === undefined) {
1073
1072
  await toolSafeSandboxSession.run({
1074
1073
  command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
1075
1074
  abortSignal: startOpts.abortSignal,
1076
1075
  });
1077
-
1078
- if (startOpts.skills && startOpts.skills.length > 0) {
1079
- if (!sandboxHomeDir) {
1080
- throw new Error('Unable to resolve sandbox HOME directory.');
1081
- }
1082
- await writeClaudeCodeSkills({
1083
- sandbox: toolSafeSandboxSession,
1084
- homeDir: sandboxHomeDir,
1085
- skills: startOpts.skills,
1086
- abortSignal: startOpts.abortSignal,
1087
- });
1088
- }
1089
1076
  }
1090
1077
 
1091
1078
  await markBridgeStarting({
@@ -1162,7 +1149,7 @@ export function createClaudeCode(
1162
1149
  sessionId: startOpts.sessionId,
1163
1150
  channel,
1164
1151
  proc,
1165
- model: settings.model,
1152
+ model,
1166
1153
  maxTurns: settings.maxTurns,
1167
1154
  env: sandboxClaudeEnvironment,
1168
1155
  thinking,
@@ -1177,7 +1164,8 @@ export function createClaudeCode(
1177
1164
  debug: startOpts.observability?.debug,
1178
1165
  permissionMode: startOpts.permissionMode,
1179
1166
  builtinToolFiltering: startOpts.builtinToolFiltering,
1180
- skills: startOpts.skills ?? [],
1167
+ sandbox: toolSafeSandboxSession,
1168
+ sandboxHomeDir,
1181
1169
  mcpServers: settings.mcpServers,
1182
1170
  supportsUserMessageResponses: () => supportsUserMessageResponses,
1183
1171
  });
@@ -1506,7 +1494,8 @@ function createSession({
1506
1494
  debug,
1507
1495
  permissionMode,
1508
1496
  builtinToolFiltering,
1509
- skills,
1497
+ sandbox,
1498
+ sandboxHomeDir,
1510
1499
  mcpServers,
1511
1500
  supportsUserMessageResponses,
1512
1501
  }: {
@@ -1529,7 +1518,8 @@ function createSession({
1529
1518
  debug: HarnessV1DebugConfig | undefined;
1530
1519
  permissionMode: HarnessV1PermissionMode | undefined;
1531
1520
  builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
1532
- skills: ReadonlyArray<HarnessV1Skill>;
1521
+ sandbox: SandboxSession;
1522
+ sandboxHomeDir: string;
1533
1523
  mcpServers: Record<string, unknown> | undefined;
1534
1524
  supportsUserMessageResponses: () => boolean;
1535
1525
  }): HarnessV1Session {
@@ -1705,11 +1695,27 @@ function createSession({
1705
1695
  harnessId: 'claude-code',
1706
1696
  });
1707
1697
  }
1698
+ await writeClaudeCodeSkills({
1699
+ sandbox,
1700
+ homeDir: sandboxHomeDir,
1701
+ skills: promptOpts.skills,
1702
+ abortSignal: promptOpts.abortSignal,
1703
+ });
1708
1704
  const control = wireTurn({
1709
1705
  emit: promptOpts.emit,
1710
1706
  abortSignal: promptOpts.abortSignal,
1711
1707
  });
1712
1708
 
1709
+ /*
1710
+ * A signal that was already aborted has settled the turn inside
1711
+ * `wireTurn`. Sending `start` anyway would run a full unattended turn
1712
+ * in the bridge — consuming tokens and possibly acting on the
1713
+ * workspace — after the caller has already observed the abort.
1714
+ */
1715
+ if (promptOpts.abortSignal?.aborted) {
1716
+ return control;
1717
+ }
1718
+
1713
1719
  const startMessage = {
1714
1720
  type: 'start' as const,
1715
1721
  prompt: extractUserText(promptOpts.prompt),
@@ -1729,8 +1735,8 @@ function createSession({
1729
1735
  ...(env !== undefined ? { env } : {}),
1730
1736
  thinking,
1731
1737
  ...(effort !== undefined ? { effort } : {}),
1732
- ...(skills.length > 0
1733
- ? { skills: skills.map(skill => skill.name) }
1738
+ ...(promptOpts.skills.length > 0
1739
+ ? { skills: promptOpts.skills.map(skill => skill.name) }
1734
1740
  : {}),
1735
1741
  ...(mcpServers == null ? {} : { mcpServers }),
1736
1742
  ...(permissionMode ? { permissionMode } : {}),
@@ -1754,6 +1760,12 @@ function createSession({
1754
1760
  harnessId: 'claude-code',
1755
1761
  });
1756
1762
  }
1763
+ await writeClaudeCodeSkills({
1764
+ sandbox,
1765
+ homeDir: sandboxHomeDir,
1766
+ skills: continueOpts.skills,
1767
+ abortSignal: continueOpts.abortSignal,
1768
+ });
1757
1769
  const control = wireTurn({
1758
1770
  emit: continueOpts.emit,
1759
1771
  abortSignal: continueOpts.abortSignal,
@@ -1772,7 +1784,9 @@ function createSession({
1772
1784
  * recomputed. This is the rare bridge-died fallback; the common slice path
1773
1785
  * is `attach`.
1774
1786
  */
1775
- if (rerunContinue) {
1787
+ // Same as `doPromptTurn`: an already-aborted signal settled the turn in
1788
+ // `wireTurn`, so don't re-drive the thread nobody is waiting on.
1789
+ if (rerunContinue && !continueOpts.abortSignal?.aborted) {
1776
1790
  pendingResumeFlag = false;
1777
1791
  channel.send({
1778
1792
  type: 'start' as const,
@@ -1800,8 +1814,8 @@ function createSession({
1800
1814
  ...(env !== undefined ? { env } : {}),
1801
1815
  thinking,
1802
1816
  ...(effort !== undefined ? { effort } : {}),
1803
- ...(skills.length > 0
1804
- ? { skills: skills.map(skill => skill.name) }
1817
+ ...(continueOpts.skills.length > 0
1818
+ ? { skills: continueOpts.skills.map(skill => skill.name) }
1805
1819
  : {}),
1806
1820
  ...(mcpServers == null ? {} : { mcpServers }),
1807
1821
  ...(permissionMode ? { permissionMode } : {}),
package/src/index.ts CHANGED
@@ -10,8 +10,5 @@ export const claudeCode = createClaudeCode();
10
10
  export { createClaudeCode } from './claude-code-harness';
11
11
  export { VERSION } from './version';
12
12
  export type { ClaudeCodeHarnessSettings } from './claude-code-harness';
13
- export type {
14
- ClaudeCodeAuthenticationMode,
15
- ClaudeCodeAuthOptions,
16
- } from './claude-code-auth';
13
+ export type { ClaudeCodeAuthenticationMode } from './claude-code-auth';
17
14
  export type { ClaudeCodeThinkingConfig } from './claude-code-thinking';