@ai-sdk/harness-claude-code 1.0.166 → 1.0.168

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-claude-code",
3
- "version": "1.0.166",
3
+ "version": "1.0.168",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,9 +26,9 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.162",
30
- "@ai-sdk/provider": "4.0.33",
31
- "@ai-sdk/provider-utils": "5.0.67",
29
+ "@ai-sdk/harness": "1.0.164",
30
+ "@ai-sdk/provider": "4.0.34",
31
+ "@ai-sdk/provider-utils": "5.0.68",
32
32
  "ws": "^8.21.0"
33
33
  },
34
34
  "peerDependencies": {
@@ -16,7 +16,6 @@ import {
16
16
  type HarnessV1ResponseFormat,
17
17
  type HarnessV1PortEndpoint,
18
18
  type HarnessV1ResumeSessionState,
19
- type HarnessV1NetworkSandboxSession,
20
19
  type HarnessV1Session,
21
20
  type HarnessV1Skill,
22
21
  type HarnessV1StreamPart,
@@ -28,6 +27,7 @@ import {
28
27
  classifyDiskLog,
29
28
  resolveSandboxCredentialEnvironment,
30
29
  createBridgeToken,
30
+ createBridgeWebSocketConnect,
31
31
  experimental_createBridgeUserMessageSubmitter,
32
32
  createBridgeErrorHandler,
33
33
  createBridgeStartupError,
@@ -36,18 +36,19 @@ import {
36
36
  forwardBridgeProcessStream,
37
37
  getRestrictedSandboxSession,
38
38
  markBridgeStarting,
39
+ resolveBridgeEndpoint,
40
+ resolveBridgePort,
39
41
  resolveSandboxHomeDir,
40
42
  SandboxChannel,
41
43
  shellQuote,
42
- sleep,
44
+ validateBridgeSandboxSettings,
43
45
  warnCredentialBrokeringUnavailable,
44
46
  waitForBridgeReady,
45
- withBridgeToken,
46
47
  writeSkills,
48
+ type BridgeHello,
47
49
  type SandboxChannelReconnectOptions,
48
50
  } from '@ai-sdk/harness/utils';
49
51
  import {
50
- safeParseJSON,
51
52
  tool,
52
53
  type Experimental_SandboxSession as SandboxSession,
53
54
  type Experimental_SandboxProcess,
@@ -862,18 +863,13 @@ export function createClaudeCode(
862
863
  const toolSafeSandboxSession =
863
864
  getRestrictedSandboxSession(sandboxSession);
864
865
  const sandboxId = 'id' in sandboxSession ? sandboxSession.id : undefined;
865
- validateBasicSandboxSettings({
866
+ validateBridgeSandboxSettings({
866
867
  sandboxSession,
867
- port: settings.port,
868
- portEndpoint: settings.portEndpoint,
868
+ sandboxId,
869
+ portOverride: settings.port,
870
+ portEndpointOverride: settings.portEndpoint,
871
+ mintBridgeToken: settings.mintBridgeToken,
869
872
  });
870
- if (settings.mintBridgeToken != null && sandboxId == null) {
871
- throw new HarnessCapabilityUnsupportedError({
872
- harnessId: 'claude-code',
873
- message:
874
- 'The Claude Code harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id.',
875
- });
876
- }
877
873
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
878
874
  const isResume = lifecycleState != null;
879
875
  const isContinue = startOpts.continueFrom != null;
@@ -1000,26 +996,10 @@ export function createClaudeCode(
1000
996
  sessionId: startOpts.sessionId,
1001
997
  });
1002
998
  let supportsUserMessageResponses = false;
1003
-
1004
- // Builds the `connect` thunk a `SandboxChannel` re-invokes on every
1005
- // (re)connect: open the socket, then wait for `bridge-hello` so the
1006
- // end-to-end link is proven live before any frame is sent.
1007
- const buildConnect =
1008
- (endpoint: HarnessV1PortEndpoint) =>
1009
- async ({
1010
- abortSignal,
1011
- }: {
1012
- abortSignal: AbortSignal;
1013
- }): Promise<WebSocket> => {
1014
- return openBridgeWebSocket({
1015
- endpoint,
1016
- timeoutMs,
1017
- abortSignal,
1018
- onHello: supportsResponses => {
1019
- supportsUserMessageResponses = supportsResponses;
1020
- },
1021
- });
1022
- };
999
+ const onHello = (hello: BridgeHello) => {
1000
+ supportsUserMessageResponses =
1001
+ hello.capabilities?.experimental_userMessageResponses === true;
1002
+ };
1023
1003
 
1024
1004
  /*
1025
1005
  * Rung 1 — ATTACH. When lifecycle state carries live bridge coordinates,
@@ -1031,17 +1011,18 @@ export function createClaudeCode(
1031
1011
  */
1032
1012
  if (coords) {
1033
1013
  try {
1034
- const endpoint = await resolveBridgeEndpoint({
1014
+ const attachEndpoint = await resolveBridgeEndpoint({
1035
1015
  sandboxSession,
1036
- override: settings.portEndpoint,
1037
1016
  port: coords.port,
1038
- });
1039
- const attachEndpoint = withBridgeToken({
1040
- endpoint,
1017
+ portEndpointOverride: settings.portEndpoint,
1041
1018
  token: coords.token,
1042
1019
  });
1043
1020
  const attachChannel: ClaudeCodeChannel = new SandboxChannel({
1044
- connect: buildConnect(attachEndpoint),
1021
+ connect: createBridgeWebSocketConnect({
1022
+ endpoint: attachEndpoint,
1023
+ webSocketConstructor: WebSocket,
1024
+ onHello,
1025
+ }),
1045
1026
  outboundSchema: outboundMessageSchema,
1046
1027
  initialLastSeenEventId: coords.lastSeenEventId,
1047
1028
  onDiagnostic,
@@ -1118,7 +1099,7 @@ export function createClaudeCode(
1118
1099
 
1119
1100
  const port = resolveBridgePort({
1120
1101
  sandboxSession,
1121
- override: settings.port,
1102
+ portOverride: settings.port,
1122
1103
  });
1123
1104
  const token =
1124
1105
  settings.mintBridgeToken == null
@@ -1191,15 +1172,21 @@ export function createClaudeCode(
1191
1172
  });
1192
1173
  void drainBridgeProcessStream(proc.stdout);
1193
1174
 
1194
- const endpoint = await resolveBridgeEndpoint({
1175
+ const bridgeEndpoint = await resolveBridgeEndpoint({
1195
1176
  sandboxSession,
1196
- override: settings.portEndpoint,
1197
1177
  port: boundPort,
1178
+ portEndpointOverride: settings.portEndpoint,
1179
+ token,
1198
1180
  });
1199
- const bridgeEndpoint = withBridgeToken({ endpoint, token });
1200
1181
 
1201
1182
  const channel: ClaudeCodeChannel = new SandboxChannel({
1202
- connect: buildConnect(bridgeEndpoint),
1183
+ connect: createBridgeWebSocketConnect({
1184
+ endpoint: bridgeEndpoint,
1185
+ webSocketConstructor: WebSocket,
1186
+ openRetryTimeoutMs: timeoutMs,
1187
+ startAbortSignal: startOpts.abortSignal,
1188
+ onHello,
1189
+ }),
1203
1190
  outboundSchema: outboundMessageSchema,
1204
1191
  onDiagnostic,
1205
1192
  onBridgeError,
@@ -1255,71 +1242,6 @@ export function createClaudeCode(
1255
1242
  };
1256
1243
  }
1257
1244
 
1258
- function resolveBridgePort({
1259
- sandboxSession,
1260
- override,
1261
- }: {
1262
- sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
1263
- override: number | undefined;
1264
- }): number {
1265
- if (override !== undefined) return override;
1266
- if ('ports' in sandboxSession && sandboxSession.ports.length > 0) {
1267
- return sandboxSession.ports[0];
1268
- }
1269
- throw new HarnessCapabilityUnsupportedError({
1270
- harnessId: 'claude-code',
1271
- message:
1272
- 'The claude-code harness needs a TCP port exposed by the sandbox. ' +
1273
- 'Create the sandbox with `ports: [<port>]` or pass `createClaudeCode({ port })`.',
1274
- });
1275
- }
1276
-
1277
- function validateBasicSandboxSettings({
1278
- sandboxSession,
1279
- port,
1280
- portEndpoint,
1281
- }: {
1282
- sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
1283
- port: number | undefined;
1284
- portEndpoint: HarnessV1PortEndpoint | undefined;
1285
- }): void {
1286
- if ('getPortEndpoint' in sandboxSession) return;
1287
- if (port == null) {
1288
- throw new HarnessCapabilityUnsupportedError({
1289
- harnessId: 'claude-code',
1290
- message:
1291
- 'The Claude Code harness requires an explicit `port` when using a basic sandbox session.',
1292
- });
1293
- }
1294
- if (portEndpoint == null) {
1295
- throw new HarnessCapabilityUnsupportedError({
1296
- harnessId: 'claude-code',
1297
- message:
1298
- 'The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session.',
1299
- });
1300
- }
1301
- }
1302
-
1303
- async function resolveBridgeEndpoint({
1304
- sandboxSession,
1305
- override,
1306
- port,
1307
- }: {
1308
- sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
1309
- override: HarnessV1PortEndpoint | undefined;
1310
- port: number;
1311
- }): Promise<HarnessV1PortEndpoint> {
1312
- if (override != null) return override;
1313
- if ('getPortEndpoint' in sandboxSession) {
1314
- return sandboxSession.getPortEndpoint({ port, protocol: 'ws' });
1315
- }
1316
- throw new HarnessCapabilityUnsupportedError({
1317
- harnessId: 'claude-code',
1318
- message:
1319
- 'The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session.',
1320
- });
1321
- }
1322
-
1323
1245
  /**
1324
1246
  * Materialise skill files into
1325
1247
  * `$HOME/.claude/skills/<name>/SKILL.md`. The `claude` CLI
@@ -1327,221 +1249,6 @@ async function resolveBridgeEndpoint({
1327
1249
  * be in place before the bridge is spawned without mutating the session
1328
1250
  * workdir. Each file uses the YAML-frontmatter shape the CLI expects.
1329
1251
  */
1330
- /**
1331
- * Wait for the bridge's `bridge-hello` message to arrive on the freshly
1332
- * opened WebSocket before any other host-side code touches it.
1333
- *
1334
- * Some sandbox runtimes (Vercel in particular) complete the WS upgrade
1335
- * with the host long before the connection is actually forwarded to the
1336
- * sandbox-side bridge. Anything sent in that gap is silently dropped —
1337
- * including the `start` message we send next. The bridge emits
1338
- * `bridge-hello` the instant it accepts the connection, so receiving it
1339
- * is the only reliable evidence that the end-to-end link is live.
1340
- */
1341
- function openWebSocketAndWaitForBridgeHello({
1342
- endpoint,
1343
- openTimeoutMs,
1344
- getHelloTimeoutMs,
1345
- abortSignal,
1346
- onHello,
1347
- }: {
1348
- endpoint: HarnessV1PortEndpoint;
1349
- openTimeoutMs: number;
1350
- getHelloTimeoutMs: () => number;
1351
- abortSignal: AbortSignal;
1352
- onHello: (supportsUserMessageResponses: boolean) => void;
1353
- }): Promise<WebSocket> {
1354
- return new Promise<WebSocket>((resolve, reject) => {
1355
- const abortReason = () =>
1356
- abortSignal.reason ?? new Error('WebSocket connection aborted');
1357
- if (abortSignal.aborted) {
1358
- reject(abortReason());
1359
- return;
1360
- }
1361
-
1362
- const ws = new WebSocket(endpoint.url, {
1363
- headers: endpoint.headers == null ? undefined : { ...endpoint.headers },
1364
- });
1365
- let opened = false;
1366
- let sawBridgeHello = false;
1367
- let settled = false;
1368
- let openTimer: ReturnType<typeof setTimeout> | undefined;
1369
- let helloTimer: ReturnType<typeof setTimeout> | undefined;
1370
- let onAbort: (() => void) | undefined;
1371
-
1372
- const cleanup = ({
1373
- keepTerminationListeners = false,
1374
- }: {
1375
- keepTerminationListeners?: boolean;
1376
- } = {}) => {
1377
- if (openTimer) clearTimeout(openTimer);
1378
- if (helloTimer) clearTimeout(helloTimer);
1379
- ws.off('open', onOpen);
1380
- ws.off('message', onMessage);
1381
- if (onAbort != null) {
1382
- abortSignal.removeEventListener('abort', onAbort);
1383
- }
1384
- if (!keepTerminationListeners) {
1385
- ws.off('close', onClose);
1386
- ws.off('error', onError);
1387
- }
1388
- };
1389
- const settle = (err?: unknown) => {
1390
- if (settled) return;
1391
- settled = true;
1392
- if (err) {
1393
- cleanup({ keepTerminationListeners: true });
1394
- try {
1395
- ws.terminate();
1396
- } catch {
1397
- cleanup();
1398
- }
1399
- reject(err);
1400
- } else {
1401
- cleanup();
1402
- resolve(ws);
1403
- }
1404
- };
1405
- const tryResolve = () => {
1406
- if (opened && sawBridgeHello) settle();
1407
- };
1408
- const startHelloTimer = () => {
1409
- if (helloTimer) return;
1410
- const helloTimeoutMs = getHelloTimeoutMs();
1411
- helloTimer = setTimeout(
1412
- () =>
1413
- settle(
1414
- new Error(
1415
- `claude-code bridge did not send bridge-hello within ${helloTimeoutMs}ms`,
1416
- ),
1417
- ),
1418
- helloTimeoutMs,
1419
- );
1420
- helloTimer.unref?.();
1421
- };
1422
- const onOpen = () => {
1423
- opened = true;
1424
- if (openTimer) {
1425
- clearTimeout(openTimer);
1426
- openTimer = undefined;
1427
- }
1428
- startHelloTimer();
1429
- tryResolve();
1430
- };
1431
- const onMessage = (raw: unknown) => {
1432
- void (async () => {
1433
- const parsed = await safeParseJSON({
1434
- text: webSocketMessageToString(raw),
1435
- });
1436
- if (!parsed.success || settled) return;
1437
- const value = parsed.value;
1438
- if (
1439
- typeof value === 'object' &&
1440
- value !== null &&
1441
- !Array.isArray(value) &&
1442
- (value as { type?: unknown }).type === 'bridge-hello'
1443
- ) {
1444
- const capabilities = (
1445
- value as {
1446
- capabilities?: { experimental_userMessageResponses?: unknown };
1447
- }
1448
- ).capabilities;
1449
- onHello(capabilities?.experimental_userMessageResponses === true);
1450
- sawBridgeHello = true;
1451
- tryResolve();
1452
- }
1453
- })();
1454
- };
1455
- const onClose = () => {
1456
- settle(
1457
- new Error('claude-code bridge closed before sending bridge-hello'),
1458
- );
1459
- cleanup();
1460
- };
1461
- const onError = (err: Error) => settle(err);
1462
- onAbort = () => settle(abortReason());
1463
- openTimer = setTimeout(
1464
- () =>
1465
- settle(new Error(`WebSocket open timed out after ${openTimeoutMs}ms`)),
1466
- openTimeoutMs,
1467
- );
1468
- openTimer.unref?.();
1469
- ws.on('open', onOpen);
1470
- ws.on('message', onMessage);
1471
- ws.on('close', onClose);
1472
- ws.on('error', onError);
1473
- abortSignal.addEventListener('abort', onAbort, { once: true });
1474
- });
1475
- }
1476
-
1477
- async function openBridgeWebSocket({
1478
- endpoint,
1479
- timeoutMs,
1480
- abortSignal,
1481
- onHello,
1482
- }: {
1483
- endpoint: HarnessV1PortEndpoint;
1484
- timeoutMs: number;
1485
- abortSignal: AbortSignal;
1486
- onHello: (supportsUserMessageResponses: boolean) => void;
1487
- }): Promise<WebSocket> {
1488
- const deadline = Date.now() + timeoutMs;
1489
- let attempt = 0;
1490
- let lastError: unknown;
1491
-
1492
- while (!abortSignal.aborted && Date.now() < deadline) {
1493
- attempt++;
1494
- try {
1495
- const remaining = Math.max(1, deadline - Date.now());
1496
- return await openWebSocketAndWaitForBridgeHello({
1497
- endpoint,
1498
- openTimeoutMs: Math.min(10_000, remaining),
1499
- getHelloTimeoutMs: () =>
1500
- Math.min(5_000, Math.max(1, deadline - Date.now())),
1501
- abortSignal,
1502
- onHello,
1503
- });
1504
- } catch (err) {
1505
- if (abortSignal.aborted) {
1506
- throw abortSignal.reason ?? new Error('WebSocket connection aborted');
1507
- }
1508
- lastError = err;
1509
- const remaining = deadline - Date.now();
1510
- if (remaining <= 0) break;
1511
- await sleep({
1512
- ms: Math.min(250 * attempt, 1_000, remaining),
1513
- abortSignal,
1514
- });
1515
- }
1516
- }
1517
-
1518
- if (abortSignal.aborted) {
1519
- throw abortSignal.reason ?? new Error('WebSocket connection aborted');
1520
- }
1521
-
1522
- throw new Error(
1523
- `claude-code bridge did not complete WebSocket handshake within ${timeoutMs}ms after ${attempt} attempt(s). Last error: ${formatUnknownError(lastError)}`,
1524
- );
1525
- }
1526
-
1527
- function webSocketMessageToString(raw: unknown): string {
1528
- if (typeof raw === 'string') return raw;
1529
- if (Buffer.isBuffer(raw)) return raw.toString('utf8');
1530
- if (Array.isArray(raw)) return Buffer.concat(raw).toString('utf8');
1531
- if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
1532
- if (ArrayBuffer.isView(raw)) {
1533
- return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString(
1534
- 'utf8',
1535
- );
1536
- }
1537
- return String(raw);
1538
- }
1539
-
1540
- function formatUnknownError(error: unknown): string {
1541
- if (error instanceof Error) return error.message;
1542
- return String(error);
1543
- }
1544
-
1545
1252
  function createSession({
1546
1253
  sessionId,
1547
1254
  channel,