@ai-sdk/harness-claude-code 1.0.79 → 1.0.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/bridge/index.mjs +252 -38
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +118 -33
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge/index.ts +135 -29
- package/src/claude-code-harness.ts +152 -33
package/src/bridge/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
runBridge,
|
|
9
9
|
type BridgeEvent,
|
|
10
10
|
type BridgeTurn,
|
|
11
|
+
type Experimental_BridgeUserMessage,
|
|
12
|
+
type Experimental_BridgeUserMessageQueue,
|
|
11
13
|
} from '@ai-sdk/harness/bridge';
|
|
12
14
|
import { createCompactionLatch } from './compaction-latch';
|
|
13
15
|
import type { StartMessage } from '../claude-code-bridge-protocol';
|
|
@@ -314,7 +316,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
314
316
|
|
|
315
317
|
const queryInput = createQueryInput({
|
|
316
318
|
initialUserMessage: start.prompt,
|
|
317
|
-
|
|
319
|
+
userMessages: turn.experimental_userMessages,
|
|
318
320
|
abortSignal: abortCtl.signal,
|
|
319
321
|
});
|
|
320
322
|
const skillsOption = toClaudeSkillsOption(start.skills);
|
|
@@ -419,6 +421,10 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
419
421
|
|
|
420
422
|
const type = msg.type;
|
|
421
423
|
|
|
424
|
+
if (type === 'command_lifecycle') {
|
|
425
|
+
queryInput.handleLifecycle(msg);
|
|
426
|
+
}
|
|
427
|
+
|
|
422
428
|
emitStreamEvent(msg);
|
|
423
429
|
|
|
424
430
|
if (type === 'result') {
|
|
@@ -430,7 +436,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
430
436
|
}
|
|
431
437
|
const usage = msg.usage ?? msg.message?.usage;
|
|
432
438
|
const harnessUsage = mapUsage(usage);
|
|
433
|
-
if (harnessUsage) turnUsage = harnessUsage;
|
|
439
|
+
if (harnessUsage) turnUsage = addUsage(turnUsage, harnessUsage);
|
|
434
440
|
if (typeof msg.total_cost_usd === 'number') {
|
|
435
441
|
totalCostUsd = (totalCostUsd ?? 0) + msg.total_cost_usd;
|
|
436
442
|
}
|
|
@@ -455,8 +461,11 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
455
461
|
usage: harnessUsage ?? streamEventState.pendingStepUsage,
|
|
456
462
|
});
|
|
457
463
|
}
|
|
458
|
-
queryInput.
|
|
459
|
-
|
|
464
|
+
queryInput.observeResult();
|
|
465
|
+
if (!queryInput.hasActiveUserMessages()) {
|
|
466
|
+
queryInput.close();
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
460
469
|
} else {
|
|
461
470
|
emitTerminalError(
|
|
462
471
|
(Array.isArray(msg.errors) ? msg.errors.join('\n') : undefined) ||
|
|
@@ -467,6 +476,11 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
467
476
|
}
|
|
468
477
|
continue;
|
|
469
478
|
}
|
|
479
|
+
|
|
480
|
+
if (queryInput.hasObservedResult && !queryInput.hasActiveUserMessages()) {
|
|
481
|
+
queryInput.close();
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
470
484
|
}
|
|
471
485
|
} catch (err) {
|
|
472
486
|
if (!(abortCtl.signal.aborted && emittedTerminalError)) {
|
|
@@ -492,59 +506,122 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
492
506
|
|
|
493
507
|
function createQueryInput({
|
|
494
508
|
initialUserMessage,
|
|
495
|
-
|
|
509
|
+
userMessages,
|
|
496
510
|
abortSignal,
|
|
497
511
|
}: {
|
|
498
512
|
initialUserMessage: string;
|
|
499
|
-
|
|
513
|
+
userMessages: Experimental_BridgeUserMessageQueue;
|
|
500
514
|
abortSignal: AbortSignal;
|
|
501
515
|
}): {
|
|
502
516
|
input: AsyncIterable<unknown>;
|
|
503
|
-
close(): void;
|
|
517
|
+
close(error?: unknown): void;
|
|
518
|
+
handleLifecycle(message: ClaudeMessage): void;
|
|
519
|
+
hasActiveUserMessages(): boolean;
|
|
520
|
+
observeResult(): void;
|
|
521
|
+
readonly hasObservedResult: boolean;
|
|
504
522
|
} {
|
|
505
523
|
let closed = false;
|
|
506
|
-
|
|
524
|
+
let observedResult = false;
|
|
525
|
+
const submittedMessages = new Map<string, Experimental_BridgeUserMessage>();
|
|
526
|
+
const close = (error?: unknown): void => {
|
|
527
|
+
if (closed) return;
|
|
507
528
|
closed = true;
|
|
529
|
+
userMessages.close(error);
|
|
508
530
|
};
|
|
509
531
|
if (abortSignal.aborted) {
|
|
510
|
-
close();
|
|
532
|
+
close(abortSignal.reason);
|
|
511
533
|
} else {
|
|
512
|
-
abortSignal.addEventListener('abort', close, {
|
|
534
|
+
abortSignal.addEventListener('abort', () => close(abortSignal.reason), {
|
|
535
|
+
once: true,
|
|
536
|
+
});
|
|
513
537
|
}
|
|
514
538
|
|
|
515
|
-
const toUserMessage = (
|
|
539
|
+
const toUserMessage = (options: {
|
|
540
|
+
text: string;
|
|
541
|
+
messageId: string;
|
|
542
|
+
priority?: 'next';
|
|
543
|
+
}): unknown => ({
|
|
516
544
|
type: 'user',
|
|
517
545
|
message: {
|
|
518
546
|
role: 'user',
|
|
519
|
-
content: [{ type: 'text', text }],
|
|
547
|
+
content: [{ type: 'text', text: options.text }],
|
|
520
548
|
},
|
|
549
|
+
parent_tool_use_id: null,
|
|
550
|
+
uuid: options.messageId,
|
|
551
|
+
...(options.priority == null ? {} : { priority: options.priority }),
|
|
521
552
|
});
|
|
522
553
|
|
|
554
|
+
const messageIterator = userMessages[Symbol.asyncIterator]();
|
|
555
|
+
|
|
523
556
|
return {
|
|
524
557
|
close,
|
|
558
|
+
handleLifecycle: message => {
|
|
559
|
+
const lifecycle = message as ClaudeMessage & {
|
|
560
|
+
command_uuid?: string;
|
|
561
|
+
state?: 'queued' | 'started' | 'completed' | 'cancelled' | 'discarded';
|
|
562
|
+
};
|
|
563
|
+
if (lifecycle.command_uuid == null || lifecycle.state == null) return;
|
|
564
|
+
const submitted = submittedMessages.get(lifecycle.command_uuid);
|
|
565
|
+
if (submitted == null) return;
|
|
566
|
+
if (lifecycle.state === 'queued' || lifecycle.state === 'started') {
|
|
567
|
+
submitted.accept();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (lifecycle.state === 'cancelled' || lifecycle.state === 'discarded') {
|
|
571
|
+
submitted.reject(
|
|
572
|
+
new Error(`Claude Code ${lifecycle.state} the user message.`),
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
submittedMessages.delete(lifecycle.command_uuid);
|
|
576
|
+
},
|
|
577
|
+
hasActiveUserMessages: () =>
|
|
578
|
+
submittedMessages.size > 0 || userMessages.pendingCount > 0,
|
|
579
|
+
observeResult: () => {
|
|
580
|
+
observedResult = true;
|
|
581
|
+
},
|
|
582
|
+
get hasObservedResult() {
|
|
583
|
+
return observedResult;
|
|
584
|
+
},
|
|
525
585
|
input: {
|
|
526
586
|
[Symbol.asyncIterator]() {
|
|
527
587
|
let sentInitial = false;
|
|
528
588
|
return {
|
|
529
589
|
async next() {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
590
|
+
if (closed || abortSignal.aborted) {
|
|
591
|
+
return {
|
|
592
|
+
value: undefined,
|
|
593
|
+
done: true,
|
|
594
|
+
} as IteratorResult<unknown>;
|
|
595
|
+
}
|
|
596
|
+
if (!sentInitial) {
|
|
597
|
+
sentInitial = true;
|
|
598
|
+
return {
|
|
599
|
+
value: toUserMessage({
|
|
600
|
+
text: initialUserMessage,
|
|
601
|
+
messageId: randomUUID(),
|
|
602
|
+
}),
|
|
603
|
+
done: false,
|
|
604
|
+
};
|
|
546
605
|
}
|
|
547
|
-
|
|
606
|
+
const nextMessage = await messageIterator.next();
|
|
607
|
+
if (nextMessage.done) {
|
|
608
|
+
return {
|
|
609
|
+
value: undefined,
|
|
610
|
+
done: true,
|
|
611
|
+
} as IteratorResult<unknown>;
|
|
612
|
+
}
|
|
613
|
+
submittedMessages.set(
|
|
614
|
+
nextMessage.value.messageId,
|
|
615
|
+
nextMessage.value,
|
|
616
|
+
);
|
|
617
|
+
return {
|
|
618
|
+
value: toUserMessage({
|
|
619
|
+
text: nextMessage.value.text,
|
|
620
|
+
messageId: nextMessage.value.messageId,
|
|
621
|
+
priority: 'next',
|
|
622
|
+
}),
|
|
623
|
+
done: false,
|
|
624
|
+
};
|
|
548
625
|
},
|
|
549
626
|
};
|
|
550
627
|
},
|
|
@@ -552,6 +629,35 @@ function createQueryInput({
|
|
|
552
629
|
};
|
|
553
630
|
}
|
|
554
631
|
|
|
632
|
+
function addUsage(
|
|
633
|
+
total: Record<string, unknown> | undefined,
|
|
634
|
+
usage: Record<string, unknown>,
|
|
635
|
+
): Record<string, unknown> {
|
|
636
|
+
if (total == null) return usage;
|
|
637
|
+
const result: Record<string, unknown> = { ...total };
|
|
638
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
639
|
+
const previous = result[key];
|
|
640
|
+
if (typeof value === 'number' && typeof previous === 'number') {
|
|
641
|
+
result[key] = previous + value;
|
|
642
|
+
} else if (
|
|
643
|
+
value != null &&
|
|
644
|
+
previous != null &&
|
|
645
|
+
typeof value === 'object' &&
|
|
646
|
+
typeof previous === 'object' &&
|
|
647
|
+
!Array.isArray(value) &&
|
|
648
|
+
!Array.isArray(previous)
|
|
649
|
+
) {
|
|
650
|
+
result[key] = addUsage(
|
|
651
|
+
previous as Record<string, unknown>,
|
|
652
|
+
value as Record<string, unknown>,
|
|
653
|
+
);
|
|
654
|
+
} else {
|
|
655
|
+
result[key] = value;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
555
661
|
function parseArgs(args: string[]): {
|
|
556
662
|
workdir?: string;
|
|
557
663
|
bridgeStateDir?: string;
|
|
@@ -21,12 +21,15 @@ import {
|
|
|
21
21
|
} from '@ai-sdk/harness';
|
|
22
22
|
import {
|
|
23
23
|
classifyDiskLog,
|
|
24
|
+
experimental_createBridgeUserMessageSubmitter,
|
|
24
25
|
createBridgeErrorHandler,
|
|
25
26
|
createBridgeStartupError,
|
|
26
27
|
drainBridgeProcessStream,
|
|
27
28
|
forwardBridgeProcessStream,
|
|
29
|
+
getRestrictedSandboxSession,
|
|
28
30
|
markBridgeStarting,
|
|
29
31
|
maskSandboxCredentials,
|
|
32
|
+
resolveSandboxDefaultWorkingDirectory,
|
|
30
33
|
resolveSandboxHomeDir,
|
|
31
34
|
SandboxChannel,
|
|
32
35
|
shellQuote,
|
|
@@ -37,7 +40,7 @@ import {
|
|
|
37
40
|
import {
|
|
38
41
|
safeParseJSON,
|
|
39
42
|
tool,
|
|
40
|
-
type Experimental_SandboxSession,
|
|
43
|
+
type Experimental_SandboxSession as SandboxSession,
|
|
41
44
|
type Experimental_SandboxProcess,
|
|
42
45
|
} from '@ai-sdk/provider-utils';
|
|
43
46
|
import { WebSocket } from 'ws';
|
|
@@ -108,6 +111,11 @@ export type ClaudeCodeHarnessSettings = {
|
|
|
108
111
|
* is reserved for something else.
|
|
109
112
|
*/
|
|
110
113
|
readonly port?: number;
|
|
114
|
+
/**
|
|
115
|
+
* Override the host endpoint used to connect to the sandbox bridge. Required
|
|
116
|
+
* together with `port` when using a basic sandbox session.
|
|
117
|
+
*/
|
|
118
|
+
readonly portEndpoint?: HarnessV1PortEndpoint;
|
|
111
119
|
/** Maximum milliseconds to wait for the bridge to advertise its port. Defaults to 120000. */
|
|
112
120
|
readonly startupTimeoutMs?: number;
|
|
113
121
|
/**
|
|
@@ -813,13 +821,36 @@ export function createClaudeCode(
|
|
|
813
821
|
getBootstrap: getClaudeCodeBootstrap,
|
|
814
822
|
doStart: async startOpts => {
|
|
815
823
|
const sandboxSession = startOpts.sandboxSession;
|
|
824
|
+
const toolSafeSandboxSession =
|
|
825
|
+
getRestrictedSandboxSession(sandboxSession);
|
|
826
|
+
const sandboxId = 'id' in sandboxSession ? sandboxSession.id : undefined;
|
|
827
|
+
validateBasicSandboxSettings({
|
|
828
|
+
sandboxSession,
|
|
829
|
+
port: settings.port,
|
|
830
|
+
portEndpoint: settings.portEndpoint,
|
|
831
|
+
});
|
|
832
|
+
if (settings.mintBridgeToken != null && sandboxId == null) {
|
|
833
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
834
|
+
harnessId: 'claude-code',
|
|
835
|
+
message:
|
|
836
|
+
'The Claude Code harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id.',
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
const defaultWorkingDirectory =
|
|
840
|
+
await resolveSandboxDefaultWorkingDirectory({
|
|
841
|
+
sandboxSession,
|
|
842
|
+
abortSignal: startOpts.abortSignal,
|
|
843
|
+
});
|
|
816
844
|
const authenticationMode = resolveClaudeCodeAuthenticationMode(
|
|
817
845
|
settings.auth,
|
|
818
846
|
);
|
|
819
847
|
const resolvedAuthEnvironment = resolveClaudeCodeEnv(settings.auth);
|
|
820
848
|
let authEnv = resolvedAuthEnvironment;
|
|
821
849
|
let sandboxTurnEnvironment = settings.env;
|
|
822
|
-
if (
|
|
850
|
+
if (
|
|
851
|
+
'addRequestTransformations' in sandboxSession &&
|
|
852
|
+
sandboxSession.addRequestTransformations != null
|
|
853
|
+
) {
|
|
823
854
|
const requestTransformations = createClaudeCodeRequestTransformations(
|
|
824
855
|
{
|
|
825
856
|
...resolvedAuthEnvironment,
|
|
@@ -845,10 +876,8 @@ export function createClaudeCode(
|
|
|
845
876
|
} else {
|
|
846
877
|
warnCredentialBrokeringUnavailable();
|
|
847
878
|
}
|
|
848
|
-
const session = sandboxSession.restricted();
|
|
849
|
-
const sandboxId = sandboxSession.id;
|
|
850
879
|
const bootstrapDir = posix.resolve(
|
|
851
|
-
|
|
880
|
+
defaultWorkingDirectory,
|
|
852
881
|
BOOTSTRAP_DIR,
|
|
853
882
|
);
|
|
854
883
|
const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
|
|
@@ -859,7 +888,7 @@ export function createClaudeCode(
|
|
|
859
888
|
: undefined;
|
|
860
889
|
|
|
861
890
|
const workDir = startOpts.sessionWorkDir;
|
|
862
|
-
const sessionDataDir = `${
|
|
891
|
+
const sessionDataDir = `${defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
|
|
863
892
|
const bridgeStateDir = `${sessionDataDir}/bridge`;
|
|
864
893
|
const timeoutMs = settings.startupTimeoutMs ?? 120_000;
|
|
865
894
|
|
|
@@ -880,13 +909,20 @@ export function createClaudeCode(
|
|
|
880
909
|
harnessId: 'claude-code',
|
|
881
910
|
sessionId: startOpts.sessionId,
|
|
882
911
|
});
|
|
912
|
+
let supportsUserMessageResponses = false;
|
|
883
913
|
|
|
884
914
|
// Builds the `connect` thunk a `SandboxChannel` re-invokes on every
|
|
885
915
|
// (re)connect: open the socket, then wait for `bridge-hello` so the
|
|
886
916
|
// end-to-end link is proven live before any frame is sent.
|
|
887
917
|
const buildConnect =
|
|
888
918
|
(endpoint: HarnessV1PortEndpoint) => async (): Promise<WebSocket> => {
|
|
889
|
-
return openBridgeWebSocket({
|
|
919
|
+
return openBridgeWebSocket({
|
|
920
|
+
endpoint,
|
|
921
|
+
timeoutMs,
|
|
922
|
+
onHello: supportsResponses => {
|
|
923
|
+
supportsUserMessageResponses = supportsResponses;
|
|
924
|
+
},
|
|
925
|
+
});
|
|
890
926
|
};
|
|
891
927
|
|
|
892
928
|
/*
|
|
@@ -899,9 +935,10 @@ export function createClaudeCode(
|
|
|
899
935
|
*/
|
|
900
936
|
if (coords) {
|
|
901
937
|
try {
|
|
902
|
-
const endpoint = await
|
|
938
|
+
const endpoint = await resolveBridgeEndpoint({
|
|
939
|
+
sandboxSession,
|
|
940
|
+
override: settings.portEndpoint,
|
|
903
941
|
port: coords.port,
|
|
904
|
-
protocol: 'ws',
|
|
905
942
|
});
|
|
906
943
|
const attachEndpoint = withBridgeToken({
|
|
907
944
|
endpoint,
|
|
@@ -938,6 +975,7 @@ export function createClaudeCode(
|
|
|
938
975
|
builtinToolFiltering: startOpts.builtinToolFiltering,
|
|
939
976
|
skills: startOpts.skills ?? [],
|
|
940
977
|
mcpServers: settings.mcpServers,
|
|
978
|
+
supportsUserMessageResponses: () => supportsUserMessageResponses,
|
|
941
979
|
});
|
|
942
980
|
} catch {
|
|
943
981
|
// Bridge no longer reachable — recover by respawning below.
|
|
@@ -958,7 +996,7 @@ export function createClaudeCode(
|
|
|
958
996
|
: undefined;
|
|
959
997
|
if (coords && isContinue) {
|
|
960
998
|
const logRaw = await Promise.resolve(
|
|
961
|
-
|
|
999
|
+
toolSafeSandboxSession.readTextFile({
|
|
962
1000
|
path: `${bridgeStateDir}/event-log.ndjson`,
|
|
963
1001
|
abortSignal: startOpts.abortSignal,
|
|
964
1002
|
}),
|
|
@@ -971,15 +1009,18 @@ export function createClaudeCode(
|
|
|
971
1009
|
const sandboxHomeDir =
|
|
972
1010
|
startOpts.skills && startOpts.skills.length > 0
|
|
973
1011
|
? await resolveSandboxHomeDir({
|
|
974
|
-
sandbox:
|
|
1012
|
+
sandbox: toolSafeSandboxSession,
|
|
975
1013
|
abortSignal: startOpts.abortSignal,
|
|
976
1014
|
})
|
|
977
1015
|
: undefined;
|
|
978
|
-
const port = resolveBridgePort(
|
|
1016
|
+
const port = resolveBridgePort({
|
|
1017
|
+
sandboxSession,
|
|
1018
|
+
override: settings.port,
|
|
1019
|
+
});
|
|
979
1020
|
const token =
|
|
980
1021
|
settings.mintBridgeToken == null
|
|
981
1022
|
? randomBytes(32).toString('hex')
|
|
982
|
-
: settings.mintBridgeToken(sandboxId);
|
|
1023
|
+
: settings.mintBridgeToken(sandboxId!);
|
|
983
1024
|
const env = {
|
|
984
1025
|
...authEnv,
|
|
985
1026
|
/*
|
|
@@ -1006,7 +1047,7 @@ export function createClaudeCode(
|
|
|
1006
1047
|
* spawn — `BRIDGE_CHANNEL_TOKEN` rotates per start.
|
|
1007
1048
|
*/
|
|
1008
1049
|
if (respawnStrategy === undefined) {
|
|
1009
|
-
await
|
|
1050
|
+
await toolSafeSandboxSession.run({
|
|
1010
1051
|
command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
|
|
1011
1052
|
abortSignal: startOpts.abortSignal,
|
|
1012
1053
|
});
|
|
@@ -1016,7 +1057,7 @@ export function createClaudeCode(
|
|
|
1016
1057
|
throw new Error('Unable to resolve sandbox HOME directory.');
|
|
1017
1058
|
}
|
|
1018
1059
|
await writeClaudeCodeSkills({
|
|
1019
|
-
sandbox:
|
|
1060
|
+
sandbox: toolSafeSandboxSession,
|
|
1020
1061
|
homeDir: sandboxHomeDir,
|
|
1021
1062
|
skills: startOpts.skills,
|
|
1022
1063
|
abortSignal: startOpts.abortSignal,
|
|
@@ -1025,13 +1066,13 @@ export function createClaudeCode(
|
|
|
1025
1066
|
}
|
|
1026
1067
|
|
|
1027
1068
|
await markBridgeStarting({
|
|
1028
|
-
sandbox:
|
|
1069
|
+
sandbox: toolSafeSandboxSession,
|
|
1029
1070
|
bridgeStateDir,
|
|
1030
1071
|
bridgeType: 'claude-code',
|
|
1031
1072
|
abortSignal: startOpts.abortSignal,
|
|
1032
1073
|
});
|
|
1033
1074
|
|
|
1034
|
-
const proc = await
|
|
1075
|
+
const proc = await toolSafeSandboxSession.spawn({
|
|
1035
1076
|
command: `node ${shellQuote(`${bootstrapDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)}`,
|
|
1036
1077
|
env,
|
|
1037
1078
|
abortSignal: startOpts.abortSignal,
|
|
@@ -1047,7 +1088,7 @@ export function createClaudeCode(
|
|
|
1047
1088
|
void bridgeStartupStderrDone;
|
|
1048
1089
|
const { port: boundPort } = await waitForBridgeReady({
|
|
1049
1090
|
proc,
|
|
1050
|
-
sandbox:
|
|
1091
|
+
sandbox: toolSafeSandboxSession,
|
|
1051
1092
|
bridgeStateDir,
|
|
1052
1093
|
bridgeType: 'claude-code',
|
|
1053
1094
|
timeoutMs,
|
|
@@ -1071,9 +1112,10 @@ export function createClaudeCode(
|
|
|
1071
1112
|
});
|
|
1072
1113
|
void drainBridgeProcessStream(proc.stdout);
|
|
1073
1114
|
|
|
1074
|
-
const endpoint = await
|
|
1115
|
+
const endpoint = await resolveBridgeEndpoint({
|
|
1116
|
+
sandboxSession,
|
|
1117
|
+
override: settings.portEndpoint,
|
|
1075
1118
|
port: boundPort,
|
|
1076
|
-
protocol: 'ws',
|
|
1077
1119
|
});
|
|
1078
1120
|
const bridgeEndpoint = withBridgeToken({ endpoint, token });
|
|
1079
1121
|
|
|
@@ -1113,17 +1155,23 @@ export function createClaudeCode(
|
|
|
1113
1155
|
builtinToolFiltering: startOpts.builtinToolFiltering,
|
|
1114
1156
|
skills: startOpts.skills ?? [],
|
|
1115
1157
|
mcpServers: settings.mcpServers,
|
|
1158
|
+
supportsUserMessageResponses: () => supportsUserMessageResponses,
|
|
1116
1159
|
});
|
|
1117
1160
|
},
|
|
1118
1161
|
};
|
|
1119
1162
|
}
|
|
1120
1163
|
|
|
1121
|
-
function resolveBridgePort(
|
|
1122
|
-
sandboxSession
|
|
1123
|
-
override
|
|
1124
|
-
|
|
1164
|
+
function resolveBridgePort({
|
|
1165
|
+
sandboxSession,
|
|
1166
|
+
override,
|
|
1167
|
+
}: {
|
|
1168
|
+
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
1169
|
+
override: number | undefined;
|
|
1170
|
+
}): number {
|
|
1125
1171
|
if (override !== undefined) return override;
|
|
1126
|
-
if (sandboxSession.ports.length > 0)
|
|
1172
|
+
if ('ports' in sandboxSession && sandboxSession.ports.length > 0) {
|
|
1173
|
+
return sandboxSession.ports[0];
|
|
1174
|
+
}
|
|
1127
1175
|
throw new HarnessCapabilityUnsupportedError({
|
|
1128
1176
|
harnessId: 'claude-code',
|
|
1129
1177
|
message:
|
|
@@ -1132,6 +1180,52 @@ function resolveBridgePort(
|
|
|
1132
1180
|
});
|
|
1133
1181
|
}
|
|
1134
1182
|
|
|
1183
|
+
function validateBasicSandboxSettings({
|
|
1184
|
+
sandboxSession,
|
|
1185
|
+
port,
|
|
1186
|
+
portEndpoint,
|
|
1187
|
+
}: {
|
|
1188
|
+
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
1189
|
+
port: number | undefined;
|
|
1190
|
+
portEndpoint: HarnessV1PortEndpoint | undefined;
|
|
1191
|
+
}): void {
|
|
1192
|
+
if ('getPortEndpoint' in sandboxSession) return;
|
|
1193
|
+
if (port == null) {
|
|
1194
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
1195
|
+
harnessId: 'claude-code',
|
|
1196
|
+
message:
|
|
1197
|
+
'The Claude Code harness requires an explicit `port` when using a basic sandbox session.',
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
if (portEndpoint == null) {
|
|
1201
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
1202
|
+
harnessId: 'claude-code',
|
|
1203
|
+
message:
|
|
1204
|
+
'The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session.',
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
async function resolveBridgeEndpoint({
|
|
1210
|
+
sandboxSession,
|
|
1211
|
+
override,
|
|
1212
|
+
port,
|
|
1213
|
+
}: {
|
|
1214
|
+
sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
|
|
1215
|
+
override: HarnessV1PortEndpoint | undefined;
|
|
1216
|
+
port: number;
|
|
1217
|
+
}): Promise<HarnessV1PortEndpoint> {
|
|
1218
|
+
if (override != null) return override;
|
|
1219
|
+
if ('getPortEndpoint' in sandboxSession) {
|
|
1220
|
+
return sandboxSession.getPortEndpoint({ port, protocol: 'ws' });
|
|
1221
|
+
}
|
|
1222
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
1223
|
+
harnessId: 'claude-code',
|
|
1224
|
+
message:
|
|
1225
|
+
'The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session.',
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1135
1229
|
/**
|
|
1136
1230
|
* Materialise skill files into
|
|
1137
1231
|
* `$HOME/.claude/skills/<name>/SKILL.md`. The `claude` CLI
|
|
@@ -1145,7 +1239,7 @@ async function writeClaudeCodeSkills({
|
|
|
1145
1239
|
skills,
|
|
1146
1240
|
abortSignal,
|
|
1147
1241
|
}: {
|
|
1148
|
-
sandbox:
|
|
1242
|
+
sandbox: SandboxSession;
|
|
1149
1243
|
homeDir: string;
|
|
1150
1244
|
skills: ReadonlyArray<HarnessV1Skill>;
|
|
1151
1245
|
abortSignal?: AbortSignal;
|
|
@@ -1178,10 +1272,12 @@ function openWebSocketAndWaitForBridgeHello({
|
|
|
1178
1272
|
endpoint,
|
|
1179
1273
|
openTimeoutMs,
|
|
1180
1274
|
getHelloTimeoutMs,
|
|
1275
|
+
onHello,
|
|
1181
1276
|
}: {
|
|
1182
1277
|
endpoint: HarnessV1PortEndpoint;
|
|
1183
1278
|
openTimeoutMs: number;
|
|
1184
1279
|
getHelloTimeoutMs: () => number;
|
|
1280
|
+
onHello: (supportsUserMessageResponses: boolean) => void;
|
|
1185
1281
|
}): Promise<WebSocket> {
|
|
1186
1282
|
return new Promise<WebSocket>((resolve, reject) => {
|
|
1187
1283
|
const ws = new WebSocket(endpoint.url, {
|
|
@@ -1253,6 +1349,12 @@ function openWebSocketAndWaitForBridgeHello({
|
|
|
1253
1349
|
!Array.isArray(value) &&
|
|
1254
1350
|
(value as { type?: unknown }).type === 'bridge-hello'
|
|
1255
1351
|
) {
|
|
1352
|
+
const capabilities = (
|
|
1353
|
+
value as {
|
|
1354
|
+
capabilities?: { experimental_userMessageResponses?: unknown };
|
|
1355
|
+
}
|
|
1356
|
+
).capabilities;
|
|
1357
|
+
onHello(capabilities?.experimental_userMessageResponses === true);
|
|
1256
1358
|
sawBridgeHello = true;
|
|
1257
1359
|
tryResolve();
|
|
1258
1360
|
}
|
|
@@ -1280,9 +1382,11 @@ function openWebSocketAndWaitForBridgeHello({
|
|
|
1280
1382
|
async function openBridgeWebSocket({
|
|
1281
1383
|
endpoint,
|
|
1282
1384
|
timeoutMs,
|
|
1385
|
+
onHello,
|
|
1283
1386
|
}: {
|
|
1284
1387
|
endpoint: HarnessV1PortEndpoint;
|
|
1285
1388
|
timeoutMs: number;
|
|
1389
|
+
onHello: (supportsUserMessageResponses: boolean) => void;
|
|
1286
1390
|
}): Promise<WebSocket> {
|
|
1287
1391
|
const deadline = Date.now() + timeoutMs;
|
|
1288
1392
|
let attempt = 0;
|
|
@@ -1297,6 +1401,7 @@ async function openBridgeWebSocket({
|
|
|
1297
1401
|
openTimeoutMs: Math.min(10_000, remaining),
|
|
1298
1402
|
getHelloTimeoutMs: () =>
|
|
1299
1403
|
Math.min(5_000, Math.max(1, deadline - Date.now())),
|
|
1404
|
+
onHello,
|
|
1300
1405
|
});
|
|
1301
1406
|
} catch (err) {
|
|
1302
1407
|
lastError = err;
|
|
@@ -1368,6 +1473,7 @@ function createSession({
|
|
|
1368
1473
|
builtinToolFiltering,
|
|
1369
1474
|
skills,
|
|
1370
1475
|
mcpServers,
|
|
1476
|
+
supportsUserMessageResponses,
|
|
1371
1477
|
}: {
|
|
1372
1478
|
sessionId: string;
|
|
1373
1479
|
channel: ClaudeCodeChannel;
|
|
@@ -1383,12 +1489,13 @@ function createSession({
|
|
|
1383
1489
|
rerunContinue: boolean;
|
|
1384
1490
|
bridgePort: number;
|
|
1385
1491
|
bridgeToken: string;
|
|
1386
|
-
sandboxId: string;
|
|
1492
|
+
sandboxId: string | undefined;
|
|
1387
1493
|
debug: HarnessV1DebugConfig | undefined;
|
|
1388
1494
|
permissionMode: HarnessV1PermissionMode | undefined;
|
|
1389
1495
|
builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
|
|
1390
1496
|
skills: ReadonlyArray<HarnessV1Skill>;
|
|
1391
1497
|
mcpServers: Record<string, unknown> | undefined;
|
|
1498
|
+
supportsUserMessageResponses: () => boolean;
|
|
1392
1499
|
}): HarnessV1Session {
|
|
1393
1500
|
let stopped = false;
|
|
1394
1501
|
let stopPromise: Promise<void> | undefined;
|
|
@@ -1417,7 +1524,13 @@ function createSession({
|
|
|
1417
1524
|
pendingResolve = resolve;
|
|
1418
1525
|
pendingReject = reject;
|
|
1419
1526
|
});
|
|
1420
|
-
|
|
1527
|
+
const userMessageSubmitter = supportsUserMessageResponses()
|
|
1528
|
+
? experimental_createBridgeUserMessageSubmitter({
|
|
1529
|
+
send: message => channel.send(message),
|
|
1530
|
+
onResponse: listener => channel.on('user-message-response', listener),
|
|
1531
|
+
onReconnect: listener => channel.onReconnect(listener),
|
|
1532
|
+
})
|
|
1533
|
+
: undefined;
|
|
1421
1534
|
const unsubs: Array<() => void> = [];
|
|
1422
1535
|
const forward = (event: HarnessV1StreamPart) => {
|
|
1423
1536
|
try {
|
|
@@ -1429,12 +1542,14 @@ function createSession({
|
|
|
1429
1542
|
const settleSuccess = () => {
|
|
1430
1543
|
if (isSettled) return;
|
|
1431
1544
|
isSettled = true;
|
|
1545
|
+
userMessageSubmitter?.close();
|
|
1432
1546
|
for (const u of unsubs) u();
|
|
1433
1547
|
pendingResolve!();
|
|
1434
1548
|
};
|
|
1435
1549
|
const settleError = (err: unknown) => {
|
|
1436
1550
|
if (isSettled) return;
|
|
1437
1551
|
isSettled = true;
|
|
1552
|
+
userMessageSubmitter?.close(err);
|
|
1438
1553
|
for (const u of unsubs) u();
|
|
1439
1554
|
pendingReject!(err);
|
|
1440
1555
|
};
|
|
@@ -1528,9 +1643,13 @@ function createSession({
|
|
|
1528
1643
|
reason: input.reason,
|
|
1529
1644
|
});
|
|
1530
1645
|
},
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1646
|
+
...(userMessageSubmitter == null
|
|
1647
|
+
? {}
|
|
1648
|
+
: {
|
|
1649
|
+
submitUserMessage: async (text: string) => {
|
|
1650
|
+
await userMessageSubmitter.submit(text);
|
|
1651
|
+
},
|
|
1652
|
+
}),
|
|
1534
1653
|
done,
|
|
1535
1654
|
};
|
|
1536
1655
|
};
|
|
@@ -1689,7 +1808,7 @@ function createSession({
|
|
|
1689
1808
|
port: bridgePort,
|
|
1690
1809
|
token: bridgeToken,
|
|
1691
1810
|
lastSeenEventId,
|
|
1692
|
-
sandboxId,
|
|
1811
|
+
...(sandboxId == null ? {} : { sandboxId }),
|
|
1693
1812
|
},
|
|
1694
1813
|
},
|
|
1695
1814
|
};
|
|
@@ -1827,7 +1946,7 @@ function createSession({
|
|
|
1827
1946
|
port: bridgePort,
|
|
1828
1947
|
token: bridgeToken,
|
|
1829
1948
|
lastSeenEventId,
|
|
1830
|
-
sandboxId,
|
|
1949
|
+
...(sandboxId == null ? {} : { sandboxId }),
|
|
1831
1950
|
},
|
|
1832
1951
|
},
|
|
1833
1952
|
};
|