@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.
@@ -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
- const NATIVE_TO_COMMON: Readonly<Record<string, HarnessV1BuiltinToolName>> = {
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): HarnessV1BuiltinToolName | 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
- if (permissionMode === 'allow-all') {
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: createPermissionSettings({ permissionMode }),
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 = Object.entries(NATIVE_TOOL_KINDS)
217
- .filter(([, kind]) =>
218
- input.permissionMode === 'allow-reads'
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
- .map(([nativeName]) => `${nativeName}(*)`);
299
+ : false)
300
+ ) {
301
+ askRules.add(`${nativeName}(*)`);
302
+ }
303
+ }
225
304
 
226
- if (askRules.length === 0) return undefined;
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 HarnessV1DebugConfig,
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 writeSkills({
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 writeSkills({
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
- for (const skill of skills) {
713
- safeClaudeSkillName(skill.name);
714
- for (const file of skill.files ?? []) {
715
- safeClaudeSkillFilePath({
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
- const homeDir = result.stdout.trim();
758
- if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
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
  });