@ai-sdk/harness 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 +15 -0
- package/agent/index.ts +1 -0
- package/dist/agent/index.d.ts +57 -11
- package/dist/agent/index.js +151 -15
- package/dist/agent/index.js.map +1 -1
- package/dist/index.d.ts +46 -1
- package/dist/index.js +28 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +60 -2
- package/dist/utils/index.js +144 -2
- package/dist/utils/index.js.map +1 -1
- package/package.json +3 -3
- package/src/agent/harness-agent-session.ts +9 -0
- package/src/agent/harness-agent-settings.ts +23 -2
- package/src/agent/harness-agent-tool-types.ts +15 -0
- package/src/agent/harness-agent.ts +29 -18
- package/src/agent/internal/run-prompt.ts +67 -10
- package/src/agent/internal/tool-filtering.ts +114 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/sandbox-home-dir.ts +22 -0
- package/src/utils/shell-quote.ts +3 -0
- package/src/utils/write-skills.ts +141 -0
- package/src/v1/harness-v1-bridge-protocol.ts +15 -0
- package/src/v1/harness-v1-session.ts +8 -0
- package/src/v1/harness-v1-tool-filtering.ts +25 -0
- package/src/v1/harness-v1.ts +11 -0
- package/src/v1/index.ts +6 -0
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import {
|
|
2
|
+
getHarnessV1BuiltinToolFilteringDenialReason,
|
|
3
|
+
isHarnessV1BuiltinToolIncluded,
|
|
4
|
+
type HarnessV1,
|
|
5
|
+
type HarnessV1BuiltinToolFiltering,
|
|
6
|
+
type HarnessV1PendingToolApproval,
|
|
7
|
+
type HarnessV1Prompt,
|
|
8
|
+
type HarnessV1PromptControl,
|
|
9
|
+
type HarnessV1Session,
|
|
10
|
+
type HarnessV1StreamPart,
|
|
11
|
+
type HarnessV1ToolSpec,
|
|
9
12
|
} from '../../v1';
|
|
10
13
|
import { toHarnessStream } from './to-harness-stream';
|
|
11
14
|
import {
|
|
@@ -60,7 +63,9 @@ export function runPrompt<
|
|
|
60
63
|
prompt?: HarnessV1Prompt;
|
|
61
64
|
instructions: string | undefined;
|
|
62
65
|
tools: TOOLS;
|
|
66
|
+
activeTools?: ToolSet;
|
|
63
67
|
toolSpecs: HarnessV1ToolSpec[];
|
|
68
|
+
builtinToolFiltering?: HarnessV1BuiltinToolFiltering | undefined;
|
|
64
69
|
sandboxSession: SandboxSession;
|
|
65
70
|
sessionWorkDir: string;
|
|
66
71
|
runtimeContext: RUNTIME_CONTEXT;
|
|
@@ -89,6 +94,7 @@ export function runPrompt<
|
|
|
89
94
|
const pendingToolApprovals = input.pendingToolApprovals ?? [];
|
|
90
95
|
const onPendingToolApproval = input.onPendingToolApproval ?? (() => {});
|
|
91
96
|
const onToolApprovalSettled = input.onToolApprovalSettled ?? (() => {});
|
|
97
|
+
const activeTools = input.activeTools ?? input.tools;
|
|
92
98
|
|
|
93
99
|
const telemetry = createTurnTelemetry({
|
|
94
100
|
telemetry: input.telemetry,
|
|
@@ -304,7 +310,7 @@ export function runPrompt<
|
|
|
304
310
|
|
|
305
311
|
const outcome = await maybeExecuteHostTool({
|
|
306
312
|
event: rawToolCall,
|
|
307
|
-
tools:
|
|
313
|
+
tools: activeTools,
|
|
308
314
|
sandboxSession: input.sandboxSession,
|
|
309
315
|
abortSignal: input.abortSignal,
|
|
310
316
|
control,
|
|
@@ -379,6 +385,39 @@ export function runPrompt<
|
|
|
379
385
|
continue;
|
|
380
386
|
}
|
|
381
387
|
|
|
388
|
+
if (displayValue.type === 'tool-approval-request') {
|
|
389
|
+
const toolCall = toolCallsByToolCallId.get(displayValue.toolCallId);
|
|
390
|
+
if (toolCall == null) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Harness '${input.harness.harnessId}' emitted approval request '${displayValue.approvalId}' for unknown tool call '${displayValue.toolCallId}'.`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const rawToolCall = rawToolCallsByToolCallId.get(
|
|
396
|
+
displayValue.toolCallId,
|
|
397
|
+
);
|
|
398
|
+
const toolName = rawToolCall?.toolName ?? toolCall.toolName;
|
|
399
|
+
if (
|
|
400
|
+
!isHarnessV1BuiltinToolIncluded({
|
|
401
|
+
toolName,
|
|
402
|
+
toolFiltering: input.builtinToolFiltering,
|
|
403
|
+
})
|
|
404
|
+
) {
|
|
405
|
+
if (control.submitToolApproval == null) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`Harness '${input.harness.harnessId}' emitted a built-in tool approval request but does not support approval responses.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
await control.submitToolApproval({
|
|
411
|
+
approvalId: displayValue.approvalId,
|
|
412
|
+
approved: false,
|
|
413
|
+
reason: getHarnessV1BuiltinToolFilteringDenialReason({
|
|
414
|
+
toolName,
|
|
415
|
+
}),
|
|
416
|
+
});
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
382
421
|
// Forward to consumer as soon as possible.
|
|
383
422
|
for (const part of translateStreamPart<TOOLS>(displayValue)) {
|
|
384
423
|
result.enqueue(part);
|
|
@@ -514,6 +553,20 @@ export function runPrompt<
|
|
|
514
553
|
`Harness '${input.harness.harnessId}' could not find parsed tool call '${toolCall.toolCallId}' for custom tool approval.`,
|
|
515
554
|
);
|
|
516
555
|
}
|
|
556
|
+
if (!hasTool({ tools: activeTools, toolName: toolCall.toolName })) {
|
|
557
|
+
const output = {
|
|
558
|
+
type: 'execution-denied',
|
|
559
|
+
reason: getHarnessV1BuiltinToolFilteringDenialReason({
|
|
560
|
+
toolName: toolCall.toolName,
|
|
561
|
+
}),
|
|
562
|
+
};
|
|
563
|
+
await control.submitToolResult({
|
|
564
|
+
toolCallId: toolCall.toolCallId,
|
|
565
|
+
output,
|
|
566
|
+
});
|
|
567
|
+
telemetry.toolEnd(toolCall.toolCallId, { ok: true, output });
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
517
570
|
const customToolApprovalDecision = resolveCustomToolApproval({
|
|
518
571
|
toolName: toolCall.toolName,
|
|
519
572
|
toolApproval: input.toolApproval,
|
|
@@ -595,7 +648,7 @@ export function runPrompt<
|
|
|
595
648
|
}
|
|
596
649
|
const outcome = await maybeExecuteHostTool({
|
|
597
650
|
event: toolCall,
|
|
598
|
-
tools:
|
|
651
|
+
tools: activeTools,
|
|
599
652
|
sandboxSession: input.sandboxSession,
|
|
600
653
|
abortSignal: input.abortSignal,
|
|
601
654
|
control,
|
|
@@ -694,6 +747,10 @@ type ToolCallTextStreamPart = {
|
|
|
694
747
|
readonly title?: string;
|
|
695
748
|
};
|
|
696
749
|
|
|
750
|
+
function hasTool(input: { tools: ToolSet; toolName: string }): boolean {
|
|
751
|
+
return Object.prototype.hasOwnProperty.call(input.tools, input.toolName);
|
|
752
|
+
}
|
|
753
|
+
|
|
697
754
|
async function maybeExecuteHostTool<TOOLS extends ToolSet>(input: {
|
|
698
755
|
event: { toolCallId: string; toolName: string; input: string };
|
|
699
756
|
tools: TOOLS;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { HarnessCapabilityUnsupportedError } from '../../errors/harness-capability-unsupported-error';
|
|
2
|
+
import type { HarnessV1, HarnessV1BuiltinToolFiltering } from '../../v1';
|
|
3
|
+
import { NoSuchToolError, type ActiveTools } from 'ai';
|
|
4
|
+
import type { ToolSet } from '@ai-sdk/provider-utils';
|
|
5
|
+
|
|
6
|
+
export type ResolvedHarnessAgentToolFiltering<TUserTools extends ToolSet> = {
|
|
7
|
+
readonly activeUserTools: TUserTools;
|
|
8
|
+
readonly builtinToolFiltering?: HarnessV1BuiltinToolFiltering;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function resolveHarnessAgentToolFiltering<
|
|
12
|
+
TAllTools extends ToolSet,
|
|
13
|
+
TUserTools extends ToolSet,
|
|
14
|
+
>(input: {
|
|
15
|
+
harness: HarnessV1;
|
|
16
|
+
userTools: TUserTools;
|
|
17
|
+
allTools: TAllTools;
|
|
18
|
+
activeTools: ActiveTools<TAllTools>;
|
|
19
|
+
inactiveTools: ActiveTools<TAllTools>;
|
|
20
|
+
}): ResolvedHarnessAgentToolFiltering<TUserTools> {
|
|
21
|
+
if (input.activeTools !== undefined && input.inactiveTools !== undefined) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'HarnessAgent: pass either `activeTools` or `inactiveTools`, not both.',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const allToolNames = Object.keys(input.allTools);
|
|
28
|
+
const activeTools = dedupeToolNames({ toolNames: input.activeTools });
|
|
29
|
+
const inactiveTools = dedupeToolNames({ toolNames: input.inactiveTools });
|
|
30
|
+
validateToolNames({
|
|
31
|
+
requestedToolNames: activeTools ?? inactiveTools,
|
|
32
|
+
availableToolNames: allToolNames,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const userToolNames = Object.keys(input.userTools);
|
|
36
|
+
const activeUserToolNames =
|
|
37
|
+
activeTools != null
|
|
38
|
+
? userToolNames.filter(name => activeTools.includes(name))
|
|
39
|
+
: inactiveTools != null
|
|
40
|
+
? userToolNames.filter(name => !inactiveTools.includes(name))
|
|
41
|
+
: userToolNames;
|
|
42
|
+
|
|
43
|
+
const builtinToolNames = Object.keys(input.harness.builtinTools);
|
|
44
|
+
const disabledBuiltinToolNames =
|
|
45
|
+
activeTools != null
|
|
46
|
+
? builtinToolNames.filter(name => !activeTools.includes(name))
|
|
47
|
+
: inactiveTools != null
|
|
48
|
+
? builtinToolNames.filter(name => inactiveTools.includes(name))
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
const builtinToolFiltering =
|
|
52
|
+
disabledBuiltinToolNames.length > 0
|
|
53
|
+
? activeTools != null
|
|
54
|
+
? {
|
|
55
|
+
mode: 'allow' as const,
|
|
56
|
+
toolNames: builtinToolNames.filter(name =>
|
|
57
|
+
activeTools.includes(name),
|
|
58
|
+
),
|
|
59
|
+
}
|
|
60
|
+
: { mode: 'deny' as const, toolNames: disabledBuiltinToolNames }
|
|
61
|
+
: undefined;
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
builtinToolFiltering != null &&
|
|
65
|
+
input.harness.supportsBuiltinToolFiltering !== true &&
|
|
66
|
+
input.harness.supportsBuiltinToolApprovals !== true
|
|
67
|
+
) {
|
|
68
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
69
|
+
message: `Harness '${input.harness.harnessId}' does not support built-in tool filtering controls.`,
|
|
70
|
+
harnessId: input.harness.harnessId,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
activeUserTools: filterToolSet({
|
|
76
|
+
tools: input.userTools,
|
|
77
|
+
toolNames: activeUserToolNames,
|
|
78
|
+
}),
|
|
79
|
+
...(builtinToolFiltering != null ? { builtinToolFiltering } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function dedupeToolNames(input: {
|
|
84
|
+
toolNames: ReadonlyArray<string> | undefined;
|
|
85
|
+
}): ReadonlyArray<string> | undefined {
|
|
86
|
+
return input.toolNames == null
|
|
87
|
+
? undefined
|
|
88
|
+
: Array.from(new Set(input.toolNames));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateToolNames(input: {
|
|
92
|
+
requestedToolNames: ReadonlyArray<string> | undefined;
|
|
93
|
+
availableToolNames: ReadonlyArray<string>;
|
|
94
|
+
}): void {
|
|
95
|
+
if (input.requestedToolNames == null) return;
|
|
96
|
+
for (const toolName of input.requestedToolNames) {
|
|
97
|
+
if (!input.availableToolNames.includes(toolName)) {
|
|
98
|
+
throw new NoSuchToolError({
|
|
99
|
+
toolName,
|
|
100
|
+
availableTools: [...input.availableToolNames],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function filterToolSet<TUserTools extends ToolSet>(input: {
|
|
107
|
+
tools: TUserTools;
|
|
108
|
+
toolNames: ReadonlyArray<string>;
|
|
109
|
+
}): TUserTools {
|
|
110
|
+
const allowed = new Set(input.toolNames);
|
|
111
|
+
return Object.fromEntries(
|
|
112
|
+
Object.entries(input.tools).filter(([name]) => allowed.has(name)),
|
|
113
|
+
) as TUserTools;
|
|
114
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -6,6 +6,13 @@ export {
|
|
|
6
6
|
} from './sandbox-channel';
|
|
7
7
|
export { classifyDiskLog, type DiskLogRecoveryMode } from './classify-disk-log';
|
|
8
8
|
export { getAiGatewayAuthFromEnv } from './ai-gateway-auth';
|
|
9
|
+
export { resolveSandboxHomeDir } from './sandbox-home-dir';
|
|
10
|
+
export { shellQuote } from './shell-quote';
|
|
11
|
+
export {
|
|
12
|
+
writeSkills,
|
|
13
|
+
type SkillFilePathMode,
|
|
14
|
+
type WriteSkillsOptions,
|
|
15
|
+
} from './write-skills';
|
|
9
16
|
export {
|
|
10
17
|
markBridgeStarting,
|
|
11
18
|
waitForBridgeReady,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
|
|
3
|
+
|
|
4
|
+
export async function resolveSandboxHomeDir({
|
|
5
|
+
sandbox,
|
|
6
|
+
abortSignal,
|
|
7
|
+
}: {
|
|
8
|
+
sandbox: Experimental_SandboxSession;
|
|
9
|
+
abortSignal?: AbortSignal;
|
|
10
|
+
}): Promise<string> {
|
|
11
|
+
const result = await sandbox.run({
|
|
12
|
+
command: 'printf "%s" "$HOME"',
|
|
13
|
+
abortSignal,
|
|
14
|
+
});
|
|
15
|
+
const homeDir = result.stdout.trim();
|
|
16
|
+
if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return homeDir;
|
|
22
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
|
|
3
|
+
import type { HarnessV1Skill } from '../v1';
|
|
4
|
+
import { shellQuote } from './shell-quote';
|
|
5
|
+
|
|
6
|
+
export type SkillFilePathMode = 'relative' | 'strip-leading-slashes';
|
|
7
|
+
|
|
8
|
+
export type WriteSkillsOptions = {
|
|
9
|
+
sandbox: Experimental_SandboxSession;
|
|
10
|
+
rootDir: string;
|
|
11
|
+
skills: ReadonlyArray<HarnessV1Skill>;
|
|
12
|
+
abortSignal?: AbortSignal;
|
|
13
|
+
skillNamePattern?: RegExp;
|
|
14
|
+
invalidSkillNameMessage?: (input: { name: string }) => string;
|
|
15
|
+
filePathMode?: SkillFilePathMode;
|
|
16
|
+
invalidSkillFilePathMessage?: (input: {
|
|
17
|
+
skillName: string;
|
|
18
|
+
filePath: string;
|
|
19
|
+
}) => string;
|
|
20
|
+
trailingNewline?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function writeSkills({
|
|
24
|
+
sandbox,
|
|
25
|
+
rootDir,
|
|
26
|
+
skills,
|
|
27
|
+
abortSignal,
|
|
28
|
+
skillNamePattern = /^[A-Za-z0-9._-]+$/,
|
|
29
|
+
invalidSkillNameMessage = ({ name }) => `Invalid skill name: ${name}`,
|
|
30
|
+
filePathMode = 'relative',
|
|
31
|
+
invalidSkillFilePathMessage = ({ skillName, filePath }) =>
|
|
32
|
+
`Invalid skill file path for ${skillName}: ${filePath}`,
|
|
33
|
+
trailingNewline = false,
|
|
34
|
+
}: WriteSkillsOptions): Promise<void> {
|
|
35
|
+
for (const skill of skills) {
|
|
36
|
+
validateSkillName({
|
|
37
|
+
name: skill.name,
|
|
38
|
+
pattern: skillNamePattern,
|
|
39
|
+
message: invalidSkillNameMessage,
|
|
40
|
+
});
|
|
41
|
+
for (const file of skill.files ?? []) {
|
|
42
|
+
normalizeSkillFilePath({
|
|
43
|
+
skillName: skill.name,
|
|
44
|
+
filePath: file.path,
|
|
45
|
+
mode: filePathMode,
|
|
46
|
+
message: invalidSkillFilePathMessage,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await sandbox.run({
|
|
52
|
+
command: `mkdir -p ${shellQuote(rootDir)}`,
|
|
53
|
+
abortSignal,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for (const skill of skills) {
|
|
57
|
+
const name = validateSkillName({
|
|
58
|
+
name: skill.name,
|
|
59
|
+
pattern: skillNamePattern,
|
|
60
|
+
message: invalidSkillNameMessage,
|
|
61
|
+
});
|
|
62
|
+
const skillDir = path.posix.join(rootDir, name);
|
|
63
|
+
await sandbox.writeTextFile({
|
|
64
|
+
path: path.posix.join(skillDir, 'SKILL.md'),
|
|
65
|
+
content: renderSkillFile({ skill, trailingNewline }),
|
|
66
|
+
abortSignal,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
for (const file of skill.files ?? []) {
|
|
70
|
+
const filePath = normalizeSkillFilePath({
|
|
71
|
+
skillName: skill.name,
|
|
72
|
+
filePath: file.path,
|
|
73
|
+
mode: filePathMode,
|
|
74
|
+
message: invalidSkillFilePathMessage,
|
|
75
|
+
});
|
|
76
|
+
await sandbox.writeTextFile({
|
|
77
|
+
path: path.posix.join(skillDir, filePath),
|
|
78
|
+
content: file.content,
|
|
79
|
+
abortSignal,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateSkillName({
|
|
86
|
+
name,
|
|
87
|
+
pattern,
|
|
88
|
+
message,
|
|
89
|
+
}: {
|
|
90
|
+
name: string;
|
|
91
|
+
pattern: RegExp;
|
|
92
|
+
message?: (input: { name: string }) => string;
|
|
93
|
+
}): string {
|
|
94
|
+
if (!pattern.test(name) || name === '.' || name === '..') {
|
|
95
|
+
throw new Error(message?.({ name }) ?? `Invalid skill name: ${name}`);
|
|
96
|
+
}
|
|
97
|
+
return name;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeSkillFilePath({
|
|
101
|
+
skillName,
|
|
102
|
+
filePath,
|
|
103
|
+
mode,
|
|
104
|
+
message,
|
|
105
|
+
}: {
|
|
106
|
+
skillName?: string;
|
|
107
|
+
filePath: string;
|
|
108
|
+
mode: SkillFilePathMode;
|
|
109
|
+
message?: (input: { skillName: string; filePath: string }) => string;
|
|
110
|
+
}): string {
|
|
111
|
+
const normalized =
|
|
112
|
+
mode === 'strip-leading-slashes'
|
|
113
|
+
? filePath.replace(/^\/+/, '')
|
|
114
|
+
: path.posix.normalize(filePath);
|
|
115
|
+
const invalid =
|
|
116
|
+
normalized === '' ||
|
|
117
|
+
(mode === 'relative' && normalized === '.') ||
|
|
118
|
+
normalized.startsWith('../') ||
|
|
119
|
+
normalized.includes('/../') ||
|
|
120
|
+
normalized.endsWith('/..') ||
|
|
121
|
+
(mode === 'relative' && path.posix.isAbsolute(normalized));
|
|
122
|
+
|
|
123
|
+
if (invalid) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
message?.({ skillName: skillName ?? '', filePath }) ??
|
|
126
|
+
`Invalid skill file path for ${skillName}: ${filePath}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return normalized;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function renderSkillFile({
|
|
133
|
+
skill,
|
|
134
|
+
trailingNewline,
|
|
135
|
+
}: {
|
|
136
|
+
skill: HarnessV1Skill;
|
|
137
|
+
trailingNewline: boolean;
|
|
138
|
+
}): string {
|
|
139
|
+
const content = `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}`;
|
|
140
|
+
return trailingNewline ? `${content}\n` : content;
|
|
141
|
+
}
|
|
@@ -73,6 +73,20 @@ export const harnessV1BridgePermissionModeSchema = z.enum([
|
|
|
73
73
|
'allow-all',
|
|
74
74
|
]);
|
|
75
75
|
|
|
76
|
+
export const harnessV1BridgeBuiltinToolFilteringSchema = z.discriminatedUnion(
|
|
77
|
+
'mode',
|
|
78
|
+
[
|
|
79
|
+
z.object({
|
|
80
|
+
mode: z.literal('allow'),
|
|
81
|
+
toolNames: z.array(z.string()),
|
|
82
|
+
}),
|
|
83
|
+
z.object({
|
|
84
|
+
mode: z.literal('deny'),
|
|
85
|
+
toolNames: z.array(z.string()),
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
);
|
|
89
|
+
|
|
76
90
|
/**
|
|
77
91
|
* Common fields of the inbound `start` message. Each adapter extends this with
|
|
78
92
|
* its runtime-specific configuration (e.g. `thinking`/`continue` for Claude
|
|
@@ -90,6 +104,7 @@ export const harnessV1BridgeStartBaseSchema = z.object({
|
|
|
90
104
|
model: z.string().optional(),
|
|
91
105
|
debug: harnessV1DebugConfigSchema.optional(),
|
|
92
106
|
permissionMode: harnessV1BridgePermissionModeSchema.optional(),
|
|
107
|
+
builtinToolFiltering: harnessV1BridgeBuiltinToolFilteringSchema.optional(),
|
|
93
108
|
});
|
|
94
109
|
|
|
95
110
|
// --- Transport / control frames (outbound, not consumer events) ---
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
import type { HarnessV1Skill } from './harness-v1-skill';
|
|
11
11
|
import type { HarnessV1StreamPart } from './harness-v1-stream-part';
|
|
12
12
|
import type { HarnessV1ToolSpec } from './harness-v1-tool-spec';
|
|
13
|
+
import type { HarnessV1BuiltinToolFiltering } from './harness-v1-tool-filtering';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Options passed to `HarnessV1.doStart`.
|
|
@@ -53,6 +54,13 @@ export type HarnessV1StartOptions = {
|
|
|
53
54
|
*/
|
|
54
55
|
readonly permissionMode?: HarnessV1PermissionMode;
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Adapter-native built-in tools that should be available for this session.
|
|
59
|
+
* Custom host-executed tools are filtered by the framework before they reach
|
|
60
|
+
* the adapter.
|
|
61
|
+
*/
|
|
62
|
+
readonly builtinToolFiltering?: HarnessV1BuiltinToolFiltering;
|
|
63
|
+
|
|
56
64
|
/**
|
|
57
65
|
* Signal that aborts startup. The adapter must propagate cancellation to
|
|
58
66
|
* any spawned processes or network calls.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type HarnessV1BuiltinToolFiltering =
|
|
2
|
+
| {
|
|
3
|
+
mode: 'allow';
|
|
4
|
+
toolNames: string[];
|
|
5
|
+
}
|
|
6
|
+
| {
|
|
7
|
+
mode: 'deny';
|
|
8
|
+
toolNames: string[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function isHarnessV1BuiltinToolIncluded(input: {
|
|
12
|
+
toolName: string;
|
|
13
|
+
toolFiltering: HarnessV1BuiltinToolFiltering | undefined;
|
|
14
|
+
}): boolean {
|
|
15
|
+
if (input.toolFiltering == null) return true;
|
|
16
|
+
return input.toolFiltering.mode === 'allow'
|
|
17
|
+
? input.toolFiltering.toolNames.includes(input.toolName)
|
|
18
|
+
: !input.toolFiltering.toolNames.includes(input.toolName);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getHarnessV1BuiltinToolFilteringDenialReason(input: {
|
|
22
|
+
toolName: string;
|
|
23
|
+
}): string {
|
|
24
|
+
return `Tool '${input.toolName}' is inactive due to the HarnessAgent tool filtering policy.`;
|
|
25
|
+
}
|
package/src/v1/harness-v1.ts
CHANGED
|
@@ -51,6 +51,17 @@ export type HarnessV1<TBuiltinTools extends ToolSet = ToolSet> = {
|
|
|
51
51
|
*/
|
|
52
52
|
readonly supportsBuiltinToolApprovals?: boolean;
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Whether the adapter can prevent its underlying runtime from seeing or
|
|
56
|
+
* calling inactive built-in tools for every tool in `builtinTools`.
|
|
57
|
+
*
|
|
58
|
+
* Adapters without native filtering can still support `activeTools` and
|
|
59
|
+
* `inactiveTools` for built-ins when `supportsBuiltinToolApprovals` is
|
|
60
|
+
* `true`: the framework routes inactive built-in tool calls through the
|
|
61
|
+
* approval path and auto-denies them before they execute.
|
|
62
|
+
*/
|
|
63
|
+
readonly supportsBuiltinToolFiltering?: boolean;
|
|
64
|
+
|
|
54
65
|
/**
|
|
55
66
|
* Optional schema for the adapter-defined `data` payload returned by session
|
|
56
67
|
* lifecycle methods. When present, the adapter promises that exported state
|
package/src/v1/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ export {
|
|
|
58
58
|
} from './harness-v1-stream-part';
|
|
59
59
|
export {
|
|
60
60
|
harnessV1BridgeAbortInboundSchema,
|
|
61
|
+
harnessV1BridgeBuiltinToolFilteringSchema,
|
|
61
62
|
harnessV1BridgeDebugEventSchema,
|
|
62
63
|
harnessV1BridgeDetachInboundSchema,
|
|
63
64
|
harnessV1BridgeDetachSchema,
|
|
@@ -91,3 +92,8 @@ export {
|
|
|
91
92
|
} from './harness-v1-diagnostic';
|
|
92
93
|
export type { HarnessV1ToolSpec } from './harness-v1-tool-spec';
|
|
93
94
|
export type { HarnessV1PermissionMode } from './harness-v1-permission-mode';
|
|
95
|
+
export type { HarnessV1BuiltinToolFiltering } from './harness-v1-tool-filtering';
|
|
96
|
+
export {
|
|
97
|
+
getHarnessV1BuiltinToolFilteringDenialReason,
|
|
98
|
+
isHarnessV1BuiltinToolIncluded,
|
|
99
|
+
} from './harness-v1-tool-filtering';
|