@ai-sdk/harness-claude-code 1.0.175 → 1.0.176
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 +12 -0
- package/dist/bridge/index.mjs +106 -6
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge/index.ts +88 -8
- package/src/bridge/permission-paths.ts +108 -0
- package/src/claude-code-bridge-protocol.ts +1 -0
- package/src/claude-code-harness.ts +12 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-claude-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.176",
|
|
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.
|
|
29
|
+
"@ai-sdk/harness": "1.0.172",
|
|
30
30
|
"@ai-sdk/provider": "4.0.35",
|
|
31
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
31
|
+
"@ai-sdk/provider-utils": "5.0.73",
|
|
32
32
|
"ws": "^8.21.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
package/src/bridge/index.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
type ClaudeMessage,
|
|
56
56
|
} from './create-emit-stream-event';
|
|
57
57
|
import { jsonSchemaToZodObject } from './json-schema-to-zod';
|
|
58
|
+
import { areFileToolPathsTrusted } from './permission-paths';
|
|
58
59
|
import {
|
|
59
60
|
resolveInactiveNativeTools,
|
|
60
61
|
resolveNativeTools,
|
|
@@ -173,6 +174,7 @@ function createPermissionOptions(input: {
|
|
|
173
174
|
approvalRequestedToolUseIds: Set<string>;
|
|
174
175
|
emittedToolCallIds: Set<string>;
|
|
175
176
|
approvalDecisions: Map<string, { approved: boolean; reason?: string }>;
|
|
177
|
+
toolCwds: Map<string, string>;
|
|
176
178
|
}): Record<string, unknown> {
|
|
177
179
|
const permissionMode = input.start.permissionMode ?? 'allow-all';
|
|
178
180
|
const inactiveNativeTools = new Set(input.inactiveNativeTools);
|
|
@@ -210,10 +212,18 @@ function createPermissionOptions(input: {
|
|
|
210
212
|
canUseTool: async (
|
|
211
213
|
toolName: string,
|
|
212
214
|
toolInput: Record<string, unknown>,
|
|
213
|
-
options: { toolUseID: string },
|
|
215
|
+
options: { toolUseID: string; blockedPath?: string },
|
|
214
216
|
) => {
|
|
215
217
|
if (
|
|
216
|
-
!needsHostApproval({
|
|
218
|
+
!(await needsHostApproval({
|
|
219
|
+
toolName,
|
|
220
|
+
toolInput,
|
|
221
|
+
permissionMode,
|
|
222
|
+
inactiveNativeTools,
|
|
223
|
+
cwd: input.toolCwds.get(options.toolUseID) ?? '',
|
|
224
|
+
readableSkillRoots: input.start.readableSkillRoots ?? [],
|
|
225
|
+
blockedPath: options.blockedPath,
|
|
226
|
+
}))
|
|
217
227
|
) {
|
|
218
228
|
return { behavior: 'allow', updatedInput: toolInput };
|
|
219
229
|
}
|
|
@@ -390,19 +400,72 @@ function createGatewayModelOverrides(
|
|
|
390
400
|
return id.includes('.') ? { [id.replace(/\./g, '-')]: model } : undefined;
|
|
391
401
|
}
|
|
392
402
|
|
|
393
|
-
function needsHostApproval(input: {
|
|
403
|
+
async function needsHostApproval(input: {
|
|
394
404
|
toolName: string;
|
|
405
|
+
toolInput: unknown;
|
|
406
|
+
cwd: string;
|
|
407
|
+
readableSkillRoots: readonly string[];
|
|
408
|
+
blockedPath?: string;
|
|
395
409
|
permissionMode: 'allow-reads' | 'allow-edits' | 'allow-all';
|
|
396
410
|
inactiveNativeTools: ReadonlySet<string>;
|
|
397
|
-
}): boolean {
|
|
411
|
+
}): Promise<boolean> {
|
|
398
412
|
if (input.toolName.startsWith('mcp__harness-tools__')) return false;
|
|
399
|
-
|
|
413
|
+
if (
|
|
400
414
|
input.inactiveNativeTools.has(input.toolName) ||
|
|
401
415
|
nativeToolRequiresApproval({
|
|
402
416
|
nativeName: input.toolName,
|
|
403
417
|
permissionMode: input.permissionMode,
|
|
404
418
|
})
|
|
405
|
-
)
|
|
419
|
+
)
|
|
420
|
+
return true;
|
|
421
|
+
const args =
|
|
422
|
+
input.toolInput != null && typeof input.toolInput === 'object'
|
|
423
|
+
? (input.toolInput as Record<string, unknown>)
|
|
424
|
+
: {};
|
|
425
|
+
let paths: unknown[];
|
|
426
|
+
let access: 'read' | 'write';
|
|
427
|
+
if (
|
|
428
|
+
input.toolName === 'Glob' &&
|
|
429
|
+
(typeof args.pattern !== 'string' ||
|
|
430
|
+
/\.\.|[{}()[\]]/.test(args.pattern) ||
|
|
431
|
+
args.pattern.includes('\\') ||
|
|
432
|
+
args.pattern.startsWith('/'))
|
|
433
|
+
)
|
|
434
|
+
return true;
|
|
435
|
+
switch (input.toolName) {
|
|
436
|
+
case 'Read':
|
|
437
|
+
paths = [args.file_path];
|
|
438
|
+
access = 'read';
|
|
439
|
+
break;
|
|
440
|
+
case 'Write':
|
|
441
|
+
case 'Edit':
|
|
442
|
+
paths = [args.file_path];
|
|
443
|
+
access = 'write';
|
|
444
|
+
break;
|
|
445
|
+
case 'NotebookEdit':
|
|
446
|
+
paths = [args.notebook_path];
|
|
447
|
+
access = 'write';
|
|
448
|
+
break;
|
|
449
|
+
case 'Glob':
|
|
450
|
+
case 'Grep':
|
|
451
|
+
paths = [args.path ?? input.cwd];
|
|
452
|
+
access = 'read';
|
|
453
|
+
break;
|
|
454
|
+
default:
|
|
455
|
+
if (input.blockedPath == null) return false;
|
|
456
|
+
paths = [];
|
|
457
|
+
access = 'read';
|
|
458
|
+
}
|
|
459
|
+
if (input.blockedPath != null) paths.push(input.blockedPath);
|
|
460
|
+
if (!paths.every((value): value is string => typeof value === 'string'))
|
|
461
|
+
return true;
|
|
462
|
+
return !(await areFileToolPathsTrusted({
|
|
463
|
+
workDir: workdir!,
|
|
464
|
+
cwd: input.cwd,
|
|
465
|
+
paths,
|
|
466
|
+
access,
|
|
467
|
+
readableSkillRoots: input.readableSkillRoots,
|
|
468
|
+
}));
|
|
406
469
|
}
|
|
407
470
|
|
|
408
471
|
/*
|
|
@@ -418,8 +481,11 @@ function createApprovalPreToolUseHook(input: {
|
|
|
418
481
|
decisions: Map<string, { approved: boolean; reason?: string }>;
|
|
419
482
|
deferredToolUseIds: Set<string>;
|
|
420
483
|
nativeToolCallNames: Map<string, string>;
|
|
484
|
+
readableSkillRoots: readonly string[];
|
|
485
|
+
toolCwds: Map<string, string>;
|
|
421
486
|
}): HookCallback {
|
|
422
487
|
return async (hookInput, toolUseID): Promise<HookJSONOutput> => {
|
|
488
|
+
if (hookInput.hook_event_name !== 'PreToolUse') return {};
|
|
423
489
|
const toolName = (hookInput as { tool_name?: unknown }).tool_name;
|
|
424
490
|
if (
|
|
425
491
|
toolUseID == null ||
|
|
@@ -428,6 +494,7 @@ function createApprovalPreToolUseHook(input: {
|
|
|
428
494
|
) {
|
|
429
495
|
return {};
|
|
430
496
|
}
|
|
497
|
+
input.toolCwds.set(toolUseID, hookInput.cwd);
|
|
431
498
|
const decision = input.decisions.get(toolUseID);
|
|
432
499
|
if (decision != null) {
|
|
433
500
|
// A bridge that resumes another bridge's deferred call never saw its
|
|
@@ -447,11 +514,14 @@ function createApprovalPreToolUseHook(input: {
|
|
|
447
514
|
};
|
|
448
515
|
}
|
|
449
516
|
if (
|
|
450
|
-
!needsHostApproval({
|
|
517
|
+
!(await needsHostApproval({
|
|
451
518
|
toolName,
|
|
519
|
+
toolInput: hookInput.tool_input,
|
|
520
|
+
cwd: hookInput.cwd,
|
|
521
|
+
readableSkillRoots: input.readableSkillRoots,
|
|
452
522
|
permissionMode: input.permissionMode,
|
|
453
523
|
inactiveNativeTools: input.inactiveNativeTools,
|
|
454
|
-
})
|
|
524
|
+
}))
|
|
455
525
|
) {
|
|
456
526
|
return {};
|
|
457
527
|
}
|
|
@@ -470,6 +540,12 @@ function nativeToolRequiresApproval(input: {
|
|
|
470
540
|
permissionMode: 'allow-reads' | 'allow-edits' | 'allow-all';
|
|
471
541
|
}): boolean {
|
|
472
542
|
if (input.permissionMode === 'allow-all') return false;
|
|
543
|
+
if (
|
|
544
|
+
input.nativeName === 'EnterWorktree' ||
|
|
545
|
+
input.nativeName === 'ExitWorktree'
|
|
546
|
+
)
|
|
547
|
+
return true;
|
|
548
|
+
if (!(input.nativeName in NATIVE_TOOL_KINDS)) return true;
|
|
473
549
|
const kind = NATIVE_TOOL_KINDS[input.nativeName] ?? 'edit';
|
|
474
550
|
if (input.permissionMode === 'allow-edits') return kind === 'bash';
|
|
475
551
|
return kind === 'edit' || kind === 'bash';
|
|
@@ -653,7 +729,9 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
653
729
|
string,
|
|
654
730
|
{ approved: boolean; reason?: string }
|
|
655
731
|
>();
|
|
732
|
+
const toolCwds = new Map<string, string>();
|
|
656
733
|
const permissionOptions = createPermissionOptions({
|
|
734
|
+
toolCwds,
|
|
657
735
|
approvalDecisions,
|
|
658
736
|
start,
|
|
659
737
|
inactiveNativeTools,
|
|
@@ -670,6 +748,8 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
|
|
|
670
748
|
const modelOverrides = createGatewayModelOverrides(start.model);
|
|
671
749
|
const deferredToolUseIds = new Set<string>();
|
|
672
750
|
const approvalPreToolUseHook = createApprovalPreToolUseHook({
|
|
751
|
+
toolCwds,
|
|
752
|
+
readableSkillRoots: start.readableSkillRoots ?? [],
|
|
673
753
|
permissionMode,
|
|
674
754
|
inactiveNativeTools: new Set(inactiveNativeTools),
|
|
675
755
|
decisions: approvalDecisions,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function areFileToolPathsTrusted(options: {
|
|
5
|
+
workDir: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
paths: readonly string[];
|
|
8
|
+
access: 'read' | 'write';
|
|
9
|
+
readableSkillRoots: readonly string[];
|
|
10
|
+
}): Promise<boolean> {
|
|
11
|
+
if (options.paths.length === 0) return false;
|
|
12
|
+
try {
|
|
13
|
+
const roots = [await realpath(options.workDir)];
|
|
14
|
+
if (options.access === 'read') {
|
|
15
|
+
for (const root of options.readableSkillRoots) {
|
|
16
|
+
try {
|
|
17
|
+
roots.push(await realpath(root));
|
|
18
|
+
} catch {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const filePath of options.paths) {
|
|
24
|
+
/*
|
|
25
|
+
* Grep and Glob trim paths and expand home-directory shorthand after
|
|
26
|
+
* the approval hook. Require approval for these forms so a literal
|
|
27
|
+
* workdir-relative path cannot authorize a different resolved path.
|
|
28
|
+
*/
|
|
29
|
+
if (
|
|
30
|
+
typeof filePath !== 'string' ||
|
|
31
|
+
filePath.length === 0 ||
|
|
32
|
+
filePath !== filePath.trim() ||
|
|
33
|
+
filePath === '~' ||
|
|
34
|
+
filePath.startsWith('~/')
|
|
35
|
+
) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
!path.posix.isAbsolute(filePath) &&
|
|
40
|
+
!path.posix.isAbsolute(options.cwd)
|
|
41
|
+
) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const absolutePath = path.posix.isAbsolute(filePath)
|
|
45
|
+
? filePath
|
|
46
|
+
: `${options.cwd}/${filePath}`;
|
|
47
|
+
/*
|
|
48
|
+
* Native runtimes differ in whether they normalize parent components
|
|
49
|
+
* before filesystem access. Both interpretations must stay within a
|
|
50
|
+
* trusted root, including when a symlink precedes a parent component.
|
|
51
|
+
*/
|
|
52
|
+
for (const candidatePath of [
|
|
53
|
+
absolutePath,
|
|
54
|
+
path.posix.normalize(absolutePath),
|
|
55
|
+
]) {
|
|
56
|
+
const candidate = await resolvePermissionPath({
|
|
57
|
+
filePath: candidatePath,
|
|
58
|
+
});
|
|
59
|
+
if (
|
|
60
|
+
!roots.some(root => {
|
|
61
|
+
const relative = path.posix.relative(root, candidate);
|
|
62
|
+
return (
|
|
63
|
+
relative === '' ||
|
|
64
|
+
(relative !== '..' &&
|
|
65
|
+
!relative.startsWith('../') &&
|
|
66
|
+
!path.posix.isAbsolute(relative))
|
|
67
|
+
);
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function resolvePermissionPath(options: {
|
|
80
|
+
filePath: string;
|
|
81
|
+
}): Promise<string> {
|
|
82
|
+
let existingPath = options.filePath;
|
|
83
|
+
const missing: string[] = [];
|
|
84
|
+
for (;;) {
|
|
85
|
+
let exists = true;
|
|
86
|
+
try {
|
|
87
|
+
await lstat(existingPath);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (
|
|
90
|
+
error == null ||
|
|
91
|
+
typeof error !== 'object' ||
|
|
92
|
+
!('code' in error) ||
|
|
93
|
+
error.code !== 'ENOENT'
|
|
94
|
+
)
|
|
95
|
+
throw error;
|
|
96
|
+
exists = false;
|
|
97
|
+
}
|
|
98
|
+
if (exists)
|
|
99
|
+
return path.posix.join(await realpath(existingPath), ...missing);
|
|
100
|
+
const parent = path.posix.dirname(existingPath);
|
|
101
|
+
const segment = path.posix.basename(existingPath);
|
|
102
|
+
if (parent === existingPath || segment === '.' || segment === '..') {
|
|
103
|
+
throw new Error('Unable to resolve permission path.');
|
|
104
|
+
}
|
|
105
|
+
missing.unshift(segment);
|
|
106
|
+
existingPath = parent;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -38,6 +38,7 @@ const skillOverridesSchema = z.record(
|
|
|
38
38
|
|
|
39
39
|
export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
|
|
40
40
|
prompt: harnessPromptContentSchema,
|
|
41
|
+
readableSkillRoots: z.array(z.string()).optional(),
|
|
41
42
|
instructions: z.string().optional(),
|
|
42
43
|
thinking: thinkingSchema,
|
|
43
44
|
effort: z.enum(['low', 'medium', 'high', 'xhigh', 'max']).optional(),
|
|
@@ -1670,7 +1670,12 @@ function createSession({
|
|
|
1670
1670
|
thinking,
|
|
1671
1671
|
...(effort !== undefined ? { effort } : {}),
|
|
1672
1672
|
...(promptOpts.skills.length > 0
|
|
1673
|
-
? {
|
|
1673
|
+
? {
|
|
1674
|
+
skills: promptOpts.skills.map(skill => skill.name),
|
|
1675
|
+
readableSkillRoots: promptOpts.skills.map(
|
|
1676
|
+
skill => `${sandboxHomeDir}/.claude/skills/${skill.name}`,
|
|
1677
|
+
),
|
|
1678
|
+
}
|
|
1674
1679
|
: {}),
|
|
1675
1680
|
...(mcpServers == null ? {} : { mcpServers }),
|
|
1676
1681
|
...(toolHints == null ? {} : { toolHints }),
|
|
@@ -1749,7 +1754,12 @@ function createSession({
|
|
|
1749
1754
|
thinking,
|
|
1750
1755
|
...(effort !== undefined ? { effort } : {}),
|
|
1751
1756
|
...(continueOpts.skills.length > 0
|
|
1752
|
-
? {
|
|
1757
|
+
? {
|
|
1758
|
+
skills: continueOpts.skills.map(skill => skill.name),
|
|
1759
|
+
readableSkillRoots: continueOpts.skills.map(
|
|
1760
|
+
skill => `${sandboxHomeDir}/.claude/skills/${skill.name}`,
|
|
1761
|
+
),
|
|
1762
|
+
}
|
|
1753
1763
|
: {}),
|
|
1754
1764
|
...(mcpServers == null ? {} : { mcpServers }),
|
|
1755
1765
|
...(toolHints == null ? {} : { toolHints }),
|