@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.
- package/CHANGELOG.md +22 -0
- package/dist/bridge/index.mjs +43 -28
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +160 -65
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/create-emit-stream-event.ts +10 -0
- package/src/bridge/index.ts +23 -0
- package/src/bridge/json-schema-to-zod.ts +8 -21
- package/src/claude-code-auth.ts +55 -2
- package/src/claude-code-bootstrap.ts +65 -0
- package/src/claude-code-harness.ts +110 -87
package/src/bridge/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
}
|
package/src/claude-code-auth.ts
CHANGED
|
@@ -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 {
|
|
5
|
+
import type { HarnessV1RequestTransformation } from '@ai-sdk/harness';
|
|
6
|
+
import {
|
|
7
|
+
createCredentialRequestTransformation,
|
|
8
|
+
getAiGatewayAuthFromEnv,
|
|
9
|
+
} from '@ai-sdk/harness/utils';
|
|
6
10
|
|
|
7
|
-
export
|
|
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:
|
|
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 =
|
|
890
|
-
|
|
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
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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(
|
|
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:
|
|
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
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
1178
|
+
endpoint,
|
|
1196
1179
|
openTimeoutMs,
|
|
1197
1180
|
getHelloTimeoutMs,
|
|
1198
1181
|
}: {
|
|
1199
|
-
|
|
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
|
-
|
|
1281
|
+
endpoint,
|
|
1297
1282
|
timeoutMs,
|
|
1298
1283
|
}: {
|
|
1299
|
-
|
|
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
|
-
|
|
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
|
: {}),
|