@ai-sdk/harness-claude-code 1.0.11 → 1.0.13
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 +19 -0
- package/dist/bridge/index.mjs +70 -9
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +29 -77
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge/index.ts +99 -13
- package/src/claude-code-harness.ts +32 -85
package/src/bridge/index.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
type BridgeEvent,
|
|
10
10
|
type BridgeTurn,
|
|
11
11
|
} from '@ai-sdk/harness/bridge';
|
|
12
|
-
import type { HarnessV1BuiltinToolName } from '@ai-sdk/harness';
|
|
13
12
|
import { createCompactionLatch } from './compaction-latch';
|
|
14
13
|
import type { StartMessage } from '../claude-code-bridge-protocol';
|
|
15
14
|
import { randomUUID } from 'node:crypto';
|
|
@@ -42,7 +41,16 @@ import { toClaudeSkillsOption } from './claude-skills-option';
|
|
|
42
41
|
* map (e.g. `WebFetch`, `NotebookEdit`) have no common equivalent; their
|
|
43
42
|
* native name is forwarded as-is on `tool-call` events.
|
|
44
43
|
*/
|
|
45
|
-
|
|
44
|
+
type CommonBuiltinToolName =
|
|
45
|
+
| 'read'
|
|
46
|
+
| 'write'
|
|
47
|
+
| 'edit'
|
|
48
|
+
| 'bash'
|
|
49
|
+
| 'glob'
|
|
50
|
+
| 'grep'
|
|
51
|
+
| 'webSearch';
|
|
52
|
+
|
|
53
|
+
const NATIVE_TO_COMMON: Readonly<Record<string, CommonBuiltinToolName>> = {
|
|
46
54
|
Read: 'read',
|
|
47
55
|
Write: 'write',
|
|
48
56
|
Edit: 'edit',
|
|
@@ -52,6 +60,36 @@ const NATIVE_TO_COMMON: Readonly<Record<string, HarnessV1BuiltinToolName>> = {
|
|
|
52
60
|
WebSearch: 'webSearch',
|
|
53
61
|
};
|
|
54
62
|
|
|
63
|
+
const PUBLIC_TO_NATIVE: Readonly<Record<string, string>> = {
|
|
64
|
+
read: 'Read',
|
|
65
|
+
write: 'Write',
|
|
66
|
+
edit: 'Edit',
|
|
67
|
+
bash: 'Bash',
|
|
68
|
+
glob: 'Glob',
|
|
69
|
+
grep: 'Grep',
|
|
70
|
+
webSearch: 'WebSearch',
|
|
71
|
+
WebFetch: 'WebFetch',
|
|
72
|
+
NotebookEdit: 'NotebookEdit',
|
|
73
|
+
TodoWrite: 'TodoWrite',
|
|
74
|
+
Agent: 'Agent',
|
|
75
|
+
TaskCreate: 'TaskCreate',
|
|
76
|
+
TaskGet: 'TaskGet',
|
|
77
|
+
TaskUpdate: 'TaskUpdate',
|
|
78
|
+
TaskList: 'TaskList',
|
|
79
|
+
TaskStop: 'TaskStop',
|
|
80
|
+
TaskOutput: 'TaskOutput',
|
|
81
|
+
Monitor: 'Monitor',
|
|
82
|
+
ListMcpResources: 'ListMcpResources',
|
|
83
|
+
ReadMcpResource: 'ReadMcpResource',
|
|
84
|
+
ExitPlanMode: 'ExitPlanMode',
|
|
85
|
+
EnterWorktree: 'EnterWorktree',
|
|
86
|
+
ExitWorktree: 'ExitWorktree',
|
|
87
|
+
AskUserQuestion: 'AskUserQuestion',
|
|
88
|
+
Skill: 'Skill',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const PUBLIC_TOOL_NAMES = Object.keys(PUBLIC_TO_NATIVE);
|
|
92
|
+
|
|
55
93
|
const NATIVE_TOOL_KINDS: Readonly<
|
|
56
94
|
Record<string, 'readonly' | 'edit' | 'bash'>
|
|
57
95
|
> = {
|
|
@@ -78,12 +116,41 @@ const NATIVE_TOOL_KINDS: Readonly<
|
|
|
78
116
|
Skill: 'edit',
|
|
79
117
|
AskUserQuestion: 'readonly',
|
|
80
118
|
Bash: 'bash',
|
|
119
|
+
Monitor: 'bash',
|
|
81
120
|
};
|
|
82
121
|
|
|
83
|
-
function toCommonName(nativeName: string):
|
|
122
|
+
function toCommonName(nativeName: string): CommonBuiltinToolName | string {
|
|
84
123
|
return NATIVE_TO_COMMON[nativeName] ?? nativeName;
|
|
85
124
|
}
|
|
86
125
|
|
|
126
|
+
function toNativeName(toolName: string): string {
|
|
127
|
+
return PUBLIC_TO_NATIVE[toolName] ?? toolName;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveNativeTools(start: StartMessage): string[] | undefined {
|
|
131
|
+
const toolFiltering = start.builtinToolFiltering;
|
|
132
|
+
if (toolFiltering == null) return undefined;
|
|
133
|
+
const activeToolNames =
|
|
134
|
+
toolFiltering.mode === 'allow'
|
|
135
|
+
? toolFiltering.toolNames
|
|
136
|
+
: PUBLIC_TOOL_NAMES.filter(
|
|
137
|
+
name => !toolFiltering.toolNames.includes(name),
|
|
138
|
+
);
|
|
139
|
+
return activeToolNames.map(name => toNativeName(name));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveInactiveNativeTools(start: StartMessage): string[] {
|
|
143
|
+
const toolFiltering = start.builtinToolFiltering;
|
|
144
|
+
if (toolFiltering == null) return [];
|
|
145
|
+
const inactiveToolNames =
|
|
146
|
+
toolFiltering.mode === 'allow'
|
|
147
|
+
? PUBLIC_TOOL_NAMES.filter(
|
|
148
|
+
name => !toolFiltering.toolNames.includes(name),
|
|
149
|
+
)
|
|
150
|
+
: toolFiltering.toolNames;
|
|
151
|
+
return inactiveToolNames.map(name => toNativeName(name));
|
|
152
|
+
}
|
|
153
|
+
|
|
87
154
|
/*
|
|
88
155
|
* The harness exposes a coarse `'off' | 'on' | 'adaptive'` thinking setting,
|
|
89
156
|
* but the Claude Agent SDK's `thinking` option takes a structured
|
|
@@ -146,13 +213,19 @@ type Emit = (msg: Record<string, unknown>) => void;
|
|
|
146
213
|
|
|
147
214
|
function createPermissionOptions(input: {
|
|
148
215
|
start: StartMessage;
|
|
216
|
+
inactiveNativeTools: readonly string[];
|
|
149
217
|
turn: BridgeTurn;
|
|
150
218
|
emit: Emit;
|
|
151
219
|
nativeToolCallNames: Map<string, string>;
|
|
152
220
|
approvalRequestedToolUseIds: Set<string>;
|
|
153
221
|
}): Record<string, unknown> {
|
|
154
222
|
const permissionMode = input.start.permissionMode ?? 'allow-all';
|
|
155
|
-
|
|
223
|
+
const inactiveNativeTools = new Set(input.inactiveNativeTools);
|
|
224
|
+
const permissionSettings = createPermissionSettings({
|
|
225
|
+
permissionMode,
|
|
226
|
+
inactiveNativeTools,
|
|
227
|
+
});
|
|
228
|
+
if (permissionMode === 'allow-all' && inactiveNativeTools.size === 0) {
|
|
156
229
|
return {
|
|
157
230
|
permissionMode: 'bypassPermissions',
|
|
158
231
|
allowDangerouslySkipPermissions: true,
|
|
@@ -163,7 +236,7 @@ function createPermissionOptions(input: {
|
|
|
163
236
|
permissionMode:
|
|
164
237
|
permissionMode === 'allow-edits' ? 'acceptEdits' : 'default',
|
|
165
238
|
allowDangerouslySkipPermissions: false,
|
|
166
|
-
settings:
|
|
239
|
+
...(permissionSettings ? { settings: permissionSettings } : {}),
|
|
167
240
|
canUseTool: async (
|
|
168
241
|
toolName: string,
|
|
169
242
|
toolInput: Record<string, unknown>,
|
|
@@ -173,6 +246,7 @@ function createPermissionOptions(input: {
|
|
|
173
246
|
return { behavior: 'allow', updatedInput: toolInput };
|
|
174
247
|
}
|
|
175
248
|
if (
|
|
249
|
+
!inactiveNativeTools.has(toolName) &&
|
|
176
250
|
!nativeToolRequiresApproval({
|
|
177
251
|
nativeName: toolName,
|
|
178
252
|
permissionMode,
|
|
@@ -212,21 +286,26 @@ function createPermissionOptions(input: {
|
|
|
212
286
|
|
|
213
287
|
function createPermissionSettings(input: {
|
|
214
288
|
permissionMode: 'allow-reads' | 'allow-edits' | 'allow-all';
|
|
289
|
+
inactiveNativeTools: ReadonlySet<string>;
|
|
215
290
|
}): Record<string, unknown> | undefined {
|
|
216
|
-
const askRules =
|
|
217
|
-
|
|
218
|
-
|
|
291
|
+
const askRules = new Set<string>();
|
|
292
|
+
for (const [nativeName, kind] of Object.entries(NATIVE_TOOL_KINDS)) {
|
|
293
|
+
if (
|
|
294
|
+
input.inactiveNativeTools.has(nativeName) ||
|
|
295
|
+
(input.permissionMode === 'allow-reads'
|
|
219
296
|
? kind === 'edit' || kind === 'bash'
|
|
220
297
|
: input.permissionMode === 'allow-edits'
|
|
221
298
|
? kind === 'bash'
|
|
222
|
-
: false
|
|
223
|
-
)
|
|
224
|
-
|
|
299
|
+
: false)
|
|
300
|
+
) {
|
|
301
|
+
askRules.add(`${nativeName}(*)`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
225
304
|
|
|
226
|
-
if (askRules.
|
|
305
|
+
if (askRules.size === 0) return undefined;
|
|
227
306
|
|
|
228
307
|
return {
|
|
229
|
-
permissions: { ask: askRules },
|
|
308
|
+
permissions: { ask: [...askRules] },
|
|
230
309
|
sandbox: { autoAllowBashIfSandboxed: false },
|
|
231
310
|
};
|
|
232
311
|
}
|
|
@@ -330,8 +409,11 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
330
409
|
abortSignal: abortCtl.signal,
|
|
331
410
|
});
|
|
332
411
|
const skillsOption = toClaudeSkillsOption(start.skills);
|
|
412
|
+
const nativeTools = resolveNativeTools(start);
|
|
413
|
+
const inactiveNativeTools = resolveInactiveNativeTools(start);
|
|
333
414
|
const permissionOptions = createPermissionOptions({
|
|
334
415
|
start,
|
|
416
|
+
inactiveNativeTools,
|
|
335
417
|
turn,
|
|
336
418
|
emit,
|
|
337
419
|
nativeToolCallNames,
|
|
@@ -344,6 +426,10 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
344
426
|
...(start.model ? { model: start.model } : {}),
|
|
345
427
|
...(start.maxTurns !== undefined ? { maxTurns: start.maxTurns } : {}),
|
|
346
428
|
...(skillsOption ? { skills: skillsOption } : {}),
|
|
429
|
+
...(nativeTools !== undefined ? { tools: nativeTools } : {}),
|
|
430
|
+
...(inactiveNativeTools.length > 0
|
|
431
|
+
? { disallowedTools: inactiveNativeTools }
|
|
432
|
+
: {}),
|
|
347
433
|
...(toThinkingConfig(start.thinking)
|
|
348
434
|
? { thinking: toThinkingConfig(start.thinking) }
|
|
349
435
|
: {}),
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
5
4
|
import {
|
|
6
5
|
commonTool,
|
|
@@ -8,9 +7,10 @@ import {
|
|
|
8
7
|
HarnessCapabilityUnsupportedError,
|
|
9
8
|
type HarnessV1,
|
|
10
9
|
type HarnessV1Bootstrap,
|
|
11
|
-
type
|
|
10
|
+
type HarnessV1BuiltinToolFiltering,
|
|
12
11
|
type HarnessV1BuiltinTool,
|
|
13
12
|
type HarnessV1ContinueTurnState,
|
|
13
|
+
type HarnessV1DebugConfig,
|
|
14
14
|
type HarnessV1PermissionMode,
|
|
15
15
|
type HarnessV1Prompt,
|
|
16
16
|
type HarnessV1PromptControl,
|
|
@@ -23,8 +23,11 @@ import {
|
|
|
23
23
|
import {
|
|
24
24
|
classifyDiskLog,
|
|
25
25
|
markBridgeStarting,
|
|
26
|
+
resolveSandboxHomeDir,
|
|
26
27
|
SandboxChannel,
|
|
28
|
+
shellQuote,
|
|
27
29
|
waitForBridgeReady,
|
|
30
|
+
writeSkills as writeHarnessSkills,
|
|
28
31
|
} from '@ai-sdk/harness/utils';
|
|
29
32
|
import {
|
|
30
33
|
tool,
|
|
@@ -276,6 +279,15 @@ const CLAUDE_CODE_BUILTIN_TOOLS = {
|
|
|
276
279
|
timeout: z.number(),
|
|
277
280
|
}),
|
|
278
281
|
}),
|
|
282
|
+
Monitor: tool({
|
|
283
|
+
description: 'Run and monitor a shell command',
|
|
284
|
+
inputSchema: z.object({
|
|
285
|
+
command: z.string(),
|
|
286
|
+
description: z.string().optional(),
|
|
287
|
+
timeout_ms: z.number().optional(),
|
|
288
|
+
persistent: z.boolean().optional(),
|
|
289
|
+
}),
|
|
290
|
+
}),
|
|
279
291
|
ListMcpResources: tool({
|
|
280
292
|
description: 'List resources available from MCP servers',
|
|
281
293
|
inputSchema: z.object({ server: z.string().optional() }),
|
|
@@ -404,6 +416,7 @@ export function createClaudeCode(
|
|
|
404
416
|
harnessId: 'claude-code',
|
|
405
417
|
builtinTools: CLAUDE_CODE_BUILTIN_TOOLS,
|
|
406
418
|
supportsBuiltinToolApprovals: true,
|
|
419
|
+
supportsBuiltinToolFiltering: true,
|
|
407
420
|
lifecycleStateSchema: claudeCodeResumeStateSchema,
|
|
408
421
|
getBootstrap: async () => {
|
|
409
422
|
if (cachedBootstrap != null) return cachedBootstrap;
|
|
@@ -509,6 +522,7 @@ export function createClaudeCode(
|
|
|
509
522
|
sandboxId,
|
|
510
523
|
debug: startOpts.observability?.debug,
|
|
511
524
|
permissionMode: startOpts.permissionMode,
|
|
525
|
+
builtinToolFiltering: startOpts.builtinToolFiltering,
|
|
512
526
|
skills: startOpts.skills ?? [],
|
|
513
527
|
});
|
|
514
528
|
} catch {
|
|
@@ -575,7 +589,7 @@ export function createClaudeCode(
|
|
|
575
589
|
if (!sandboxHomeDir) {
|
|
576
590
|
throw new Error('Unable to resolve sandbox HOME directory.');
|
|
577
591
|
}
|
|
578
|
-
await
|
|
592
|
+
await writeClaudeCodeSkills({
|
|
579
593
|
sandbox: session,
|
|
580
594
|
homeDir: sandboxHomeDir,
|
|
581
595
|
skills: startOpts.skills,
|
|
@@ -671,6 +685,7 @@ export function createClaudeCode(
|
|
|
671
685
|
sandboxId,
|
|
672
686
|
debug: startOpts.observability?.debug,
|
|
673
687
|
permissionMode: startOpts.permissionMode,
|
|
688
|
+
builtinToolFiltering: startOpts.builtinToolFiltering,
|
|
674
689
|
skills: startOpts.skills ?? [],
|
|
675
690
|
});
|
|
676
691
|
},
|
|
@@ -698,7 +713,7 @@ function resolveBridgePort(
|
|
|
698
713
|
* be in place before the bridge is spawned without mutating the session
|
|
699
714
|
* workdir. Each file uses the YAML-frontmatter shape the CLI expects.
|
|
700
715
|
*/
|
|
701
|
-
async function
|
|
716
|
+
async function writeClaudeCodeSkills({
|
|
702
717
|
sandbox,
|
|
703
718
|
homeDir,
|
|
704
719
|
skills,
|
|
@@ -709,89 +724,17 @@ async function writeSkills({
|
|
|
709
724
|
skills: ReadonlyArray<HarnessV1Skill>;
|
|
710
725
|
abortSignal?: AbortSignal;
|
|
711
726
|
}): Promise<void> {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
skillName: skill.name,
|
|
717
|
-
filePath: file.path,
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
await sandbox.run({
|
|
723
|
-
command: `mkdir -p ${shellQuote(homeDir)}/.claude/skills`,
|
|
724
|
-
abortSignal,
|
|
725
|
-
});
|
|
726
|
-
for (const skill of skills) {
|
|
727
|
-
const name = safeClaudeSkillName(skill.name);
|
|
728
|
-
const skillDir = `${homeDir}/.claude/skills/${name}`;
|
|
729
|
-
const path = `${skillDir}/SKILL.md`;
|
|
730
|
-
const content = `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}\n`;
|
|
731
|
-
await sandbox.writeTextFile({ path, content, abortSignal });
|
|
732
|
-
for (const file of skill.files ?? []) {
|
|
733
|
-
const filePath = safeClaudeSkillFilePath({
|
|
734
|
-
skillName: skill.name,
|
|
735
|
-
filePath: file.path,
|
|
736
|
-
});
|
|
737
|
-
await sandbox.writeTextFile({
|
|
738
|
-
path: `${skillDir}/${filePath}`,
|
|
739
|
-
content: file.content,
|
|
740
|
-
abortSignal,
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
async function resolveSandboxHomeDir({
|
|
747
|
-
sandbox,
|
|
748
|
-
abortSignal,
|
|
749
|
-
}: {
|
|
750
|
-
sandbox: Experimental_SandboxSession;
|
|
751
|
-
abortSignal?: AbortSignal;
|
|
752
|
-
}): Promise<string> {
|
|
753
|
-
const result = await sandbox.run({
|
|
754
|
-
command: 'printf "%s" "$HOME"',
|
|
727
|
+
await writeHarnessSkills({
|
|
728
|
+
sandbox,
|
|
729
|
+
rootDir: `${homeDir}/.claude/skills`,
|
|
730
|
+
skills,
|
|
755
731
|
abortSignal,
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
throw new Error(
|
|
760
|
-
`Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`,
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
return homeDir;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function safeClaudeSkillName(name: string): string {
|
|
767
|
-
if (!/^[A-Za-z0-9._-]+$/.test(name) || name === '.' || name === '..') {
|
|
768
|
-
throw new Error(`Invalid Claude Code skill name: ${name}`);
|
|
769
|
-
}
|
|
770
|
-
return name;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
function safeClaudeSkillFilePath({
|
|
774
|
-
skillName,
|
|
775
|
-
filePath,
|
|
776
|
-
}: {
|
|
777
|
-
skillName: string;
|
|
778
|
-
filePath: string;
|
|
779
|
-
}): string {
|
|
780
|
-
const normalized = path.posix.normalize(filePath);
|
|
781
|
-
if (
|
|
782
|
-
normalized === '.' ||
|
|
783
|
-
normalized.startsWith('../') ||
|
|
784
|
-
path.posix.isAbsolute(normalized)
|
|
785
|
-
) {
|
|
786
|
-
throw new Error(
|
|
732
|
+
invalidSkillNameMessage: ({ name }) =>
|
|
733
|
+
`Invalid Claude Code skill name: ${name}`,
|
|
734
|
+
invalidSkillFilePathMessage: ({ skillName, filePath }) =>
|
|
787
735
|
`Invalid Claude Code skill file path for ${skillName}: ${filePath}`,
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
return normalized;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function shellQuote(value: string): string {
|
|
794
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
736
|
+
trailingNewline: true,
|
|
737
|
+
});
|
|
795
738
|
}
|
|
796
739
|
|
|
797
740
|
async function readBridgeAsset(name: string): Promise<string> {
|
|
@@ -1104,6 +1047,7 @@ function createSession({
|
|
|
1104
1047
|
sandboxId,
|
|
1105
1048
|
debug,
|
|
1106
1049
|
permissionMode,
|
|
1050
|
+
builtinToolFiltering,
|
|
1107
1051
|
skills,
|
|
1108
1052
|
}: {
|
|
1109
1053
|
sessionId: string;
|
|
@@ -1121,6 +1065,7 @@ function createSession({
|
|
|
1121
1065
|
sandboxId: string;
|
|
1122
1066
|
debug: HarnessV1DebugConfig | undefined;
|
|
1123
1067
|
permissionMode: HarnessV1PermissionMode | undefined;
|
|
1068
|
+
builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
|
|
1124
1069
|
skills: ReadonlyArray<HarnessV1Skill>;
|
|
1125
1070
|
}): HarnessV1Session {
|
|
1126
1071
|
let stopped = false;
|
|
@@ -1306,6 +1251,7 @@ function createSession({
|
|
|
1306
1251
|
? { skills: skills.map(skill => skill.name) }
|
|
1307
1252
|
: {}),
|
|
1308
1253
|
...(permissionMode ? { permissionMode } : {}),
|
|
1254
|
+
...(builtinToolFiltering ? { builtinToolFiltering } : {}),
|
|
1309
1255
|
...(debug ? { debug } : {}),
|
|
1310
1256
|
...(pendingResumeFlag ? { continue: true } : {}),
|
|
1311
1257
|
};
|
|
@@ -1357,6 +1303,7 @@ function createSession({
|
|
|
1357
1303
|
? { skills: skills.map(skill => skill.name) }
|
|
1358
1304
|
: {}),
|
|
1359
1305
|
...(permissionMode ? { permissionMode } : {}),
|
|
1306
|
+
...(builtinToolFiltering ? { builtinToolFiltering } : {}),
|
|
1360
1307
|
...(debug ? { debug } : {}),
|
|
1361
1308
|
continue: true,
|
|
1362
1309
|
});
|