@ai-sdk/harness 1.0.122 → 1.0.124
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 +27 -0
- package/dist/agent/index.d.ts +5 -5
- package/dist/agent/index.js +273 -64
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.js +0 -1
- package/dist/bridge/index.js.map +1 -1
- package/dist/index.d.ts +28 -6
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +30 -5
- package/dist/utils/index.js +130 -16
- package/dist/utils/index.js.map +1 -1
- package/package.json +4 -4
- package/src/agent/harness-agent.ts +32 -12
- package/src/agent/internal/bootstrap-recipe.ts +16 -16
- package/src/agent/internal/harness-stream-text-result.ts +1 -3
- package/src/agent/internal/run-prompt.ts +134 -19
- package/src/agent/internal/sandbox-bootstrap.ts +24 -10
- package/src/agent/internal/strip-work-dir.ts +57 -0
- package/src/agent/prepare-harness-sandbox-template.ts +16 -3
- package/src/agent/prepare-sandbox-for-harness.ts +14 -5
- package/src/bridge/index.ts +0 -1
- package/src/utils/sandbox-channel.ts +172 -19
- package/src/utils/write-instructions.ts +1 -1
- package/src/utils/write-skills.ts +1 -1
- package/src/v1/harness-session-data-directory-path.ts +28 -0
- package/src/v1/harness-v1-bootstrap.ts +5 -5
- package/src/v1/harness-v1-network-sandbox-session.ts +24 -0
- package/src/v1/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @ai-sdk/harness
|
|
2
2
|
|
|
3
|
+
## 1.0.124
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 40231b6: fix(harness): enforce tool input schemas at execution and consistently reject invalid inputs
|
|
8
|
+
- Updated dependencies [b5679a7]
|
|
9
|
+
- Updated dependencies [ca31b89]
|
|
10
|
+
- ai@7.0.114
|
|
11
|
+
|
|
12
|
+
## 1.0.123
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 9c8c0c1: feat (harness): keep harness-generated state out of the sandbox workspace. The framework and every bridge adapter now resolve their generated state — bootstrap recipes and their dependencies (`.harness-bootstrap/…`) and per-session run state (`.agent-runs/…`) — through the new `harnessV1StateDirectory()` helper, which always resolves to a fixed directory (`~/.ai-sdk-harness`) under the sandbox's own HOME, never the sandbox's working directory. This is not configurable: every sandbox provider gets the same layout, so harness infrastructure never lands in a user-owned workspace. This changes the on-disk layout for every provider (a breaking change, acceptable given the experimental nature of this API) — sandboxes bootstrapped under the previous layout re-bootstrap once under the new path.
|
|
17
|
+
- 771e74b: chore: enable dead code lint rules
|
|
18
|
+
- 7115a3f: fix(harness): preserve buffered SandboxChannel event order without blocking selective listeners and forward Claude Code compaction events
|
|
19
|
+
- Updated dependencies [dcdb011]
|
|
20
|
+
- Updated dependencies [8f72832]
|
|
21
|
+
- Updated dependencies [fe07867]
|
|
22
|
+
- Updated dependencies [b74c0cb]
|
|
23
|
+
- Updated dependencies [a4b0940]
|
|
24
|
+
- Updated dependencies [2693319]
|
|
25
|
+
- Updated dependencies [c93ee90]
|
|
26
|
+
- Updated dependencies [771e74b]
|
|
27
|
+
- ai@7.0.113
|
|
28
|
+
- @ai-sdk/provider-utils@5.0.47
|
|
29
|
+
|
|
3
30
|
## 1.0.122
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { JSONSchema7, JSONValue, LanguageModelV4StreamPart, LanguageModelV4ToolC
|
|
|
8
8
|
/**
|
|
9
9
|
* One file to write into the sandbox as part of an adapter's bootstrap recipe.
|
|
10
10
|
* Absolute paths are used as-is. Relative paths are resolved against the
|
|
11
|
-
*
|
|
11
|
+
* harness state directory (`$HOME/.ai-sdk-harness`). Paths should live under
|
|
12
12
|
* {@link HarnessV1Bootstrap.bootstrapDir}.
|
|
13
13
|
*/
|
|
14
14
|
interface HarnessV1BootstrapFile {
|
|
@@ -37,10 +37,10 @@ interface HarnessV1Bootstrap {
|
|
|
37
37
|
readonly harnessId: string;
|
|
38
38
|
/**
|
|
39
39
|
* Path inside the sandbox where this recipe writes its state. Absolute paths
|
|
40
|
-
* are used as-is. Relative paths are resolved against the
|
|
41
|
-
*
|
|
42
|
-
* declared in {@link files} should also use this prefix so an
|
|
43
|
-
* can sweep stale state by clearing the directory.
|
|
40
|
+
* are used as-is. Relative paths are resolved against the harness state
|
|
41
|
+
* directory (`$HOME/.ai-sdk-harness`). The marker file lives directly under
|
|
42
|
+
* it. Files declared in {@link files} should also use this prefix so an
|
|
43
|
+
* adapter upgrade can sweep stale state by clearing the directory.
|
|
44
44
|
*/
|
|
45
45
|
readonly bootstrapDir: string;
|
|
46
46
|
/** Files to write into the sandbox before any command runs. */
|
package/dist/agent/index.js
CHANGED
|
@@ -38,6 +38,34 @@ var HarnessCapabilityUnsupportedError = class extends (_b2 = HarnessError, _a2 =
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
// src/v1/harness-v1-network-sandbox-session.ts
|
|
42
|
+
import { posix } from "path";
|
|
43
|
+
var HARNESS_V1_STATE_DIRECTORY_NAME = ".ai-sdk-harness";
|
|
44
|
+
function harnessStateDirectoryPath({
|
|
45
|
+
sandboxHomeDir
|
|
46
|
+
}) {
|
|
47
|
+
return posix.join(sandboxHomeDir, HARNESS_V1_STATE_DIRECTORY_NAME);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/v1/harness-session-data-directory-path.ts
|
|
51
|
+
import { posix as posix2 } from "path";
|
|
52
|
+
function encodeHarnessPathSegment(value) {
|
|
53
|
+
const encoded = encodeURIComponent(value);
|
|
54
|
+
if (encoded === "") return "%";
|
|
55
|
+
if (encoded === ".") return "%2E";
|
|
56
|
+
if (encoded === "..") return "%2E%2E";
|
|
57
|
+
return encoded;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/v1/harness-v1-tool-filtering.ts
|
|
61
|
+
function isHarnessV1BuiltinToolIncluded(input) {
|
|
62
|
+
if (input.toolFiltering == null) return true;
|
|
63
|
+
return input.toolFiltering.mode === "allow" ? input.toolFiltering.toolNames.includes(input.toolName) : !input.toolFiltering.toolNames.includes(input.toolName);
|
|
64
|
+
}
|
|
65
|
+
function getHarnessV1BuiltinToolFilteringDenialReason(input) {
|
|
66
|
+
return `Tool '${input.toolName}' is inactive due to the HarnessAgent tool filtering policy.`;
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
// src/agent/harness-agent.ts
|
|
42
70
|
import {
|
|
43
71
|
asArray as asArray2,
|
|
@@ -115,15 +143,6 @@ async function validateLifecycleStateData(input) {
|
|
|
115
143
|
};
|
|
116
144
|
}
|
|
117
145
|
|
|
118
|
-
// src/v1/harness-v1-tool-filtering.ts
|
|
119
|
-
function isHarnessV1BuiltinToolIncluded(input) {
|
|
120
|
-
if (input.toolFiltering == null) return true;
|
|
121
|
-
return input.toolFiltering.mode === "allow" ? input.toolFiltering.toolNames.includes(input.toolName) : !input.toolFiltering.toolNames.includes(input.toolName);
|
|
122
|
-
}
|
|
123
|
-
function getHarnessV1BuiltinToolFilteringDenialReason(input) {
|
|
124
|
-
return `Tool '${input.toolName}' is inactive due to the HarnessAgent tool filtering policy.`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
146
|
// src/agent/internal/to-harness-stream.ts
|
|
128
147
|
async function toHarnessStream(options) {
|
|
129
148
|
let controller;
|
|
@@ -764,9 +783,7 @@ var HarnessStreamTextResult = class {
|
|
|
764
783
|
this.currentStepContent.push({
|
|
765
784
|
...part
|
|
766
785
|
});
|
|
767
|
-
return;
|
|
768
786
|
default:
|
|
769
|
-
return;
|
|
770
787
|
}
|
|
771
788
|
}
|
|
772
789
|
};
|
|
@@ -1086,6 +1103,45 @@ function stripWorkDir(part, sessionWorkDir) {
|
|
|
1086
1103
|
return part;
|
|
1087
1104
|
}
|
|
1088
1105
|
}
|
|
1106
|
+
function stripParsedToolInputWorkDir({
|
|
1107
|
+
value,
|
|
1108
|
+
sessionWorkDir
|
|
1109
|
+
}) {
|
|
1110
|
+
if (sessionWorkDir.length === 0) return value;
|
|
1111
|
+
const projected = /* @__PURE__ */ new WeakMap();
|
|
1112
|
+
const pending = [];
|
|
1113
|
+
const project = (item) => {
|
|
1114
|
+
if (typeof item === "string") return stripString(item, sessionWorkDir);
|
|
1115
|
+
if (item === null || typeof item !== "object") return item;
|
|
1116
|
+
if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) {
|
|
1117
|
+
return item;
|
|
1118
|
+
}
|
|
1119
|
+
if (projected.has(item)) return projected.get(item);
|
|
1120
|
+
const target = Array.isArray(item) ? new Array(item.length) : {};
|
|
1121
|
+
projected.set(item, target);
|
|
1122
|
+
pending.push({ source: item, target });
|
|
1123
|
+
return target;
|
|
1124
|
+
};
|
|
1125
|
+
const result = project(value);
|
|
1126
|
+
while (pending.length > 0) {
|
|
1127
|
+
const { source, target } = pending.pop();
|
|
1128
|
+
if (Array.isArray(source) && Array.isArray(target)) {
|
|
1129
|
+
for (let index = 0; index < source.length; index++) {
|
|
1130
|
+
if (index in source) target[index] = project(source[index]);
|
|
1131
|
+
}
|
|
1132
|
+
} else {
|
|
1133
|
+
for (const [key, item] of Object.entries(source)) {
|
|
1134
|
+
Object.defineProperty(target, key, {
|
|
1135
|
+
value: project(item),
|
|
1136
|
+
enumerable: true,
|
|
1137
|
+
writable: true,
|
|
1138
|
+
configurable: true
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return result;
|
|
1144
|
+
}
|
|
1089
1145
|
function stripString(value, workDir) {
|
|
1090
1146
|
return stripStreamingString({
|
|
1091
1147
|
value,
|
|
@@ -1514,6 +1570,7 @@ function pinSandboxChannelEventCheckpoint(event) {
|
|
|
1514
1570
|
}
|
|
1515
1571
|
|
|
1516
1572
|
// src/agent/internal/run-prompt.ts
|
|
1573
|
+
var invalidToolInputMessage = "Tool input validation failed.";
|
|
1517
1574
|
function unwrapToolResultOutput(toolResult) {
|
|
1518
1575
|
switch (toolResult.output.type) {
|
|
1519
1576
|
case "text":
|
|
@@ -1928,7 +1985,7 @@ function runPrompt(input) {
|
|
|
1928
1985
|
};
|
|
1929
1986
|
const processPendingApprovalContinuation = async (approval, continuation) => {
|
|
1930
1987
|
var _a6;
|
|
1931
|
-
const rawToolCall = (_a6 = rawToolCallsByToolCallId.get(approval.toolCallId)) != null ? _a6 : {
|
|
1988
|
+
const rawToolCall = (_a6 = approval.kind === "builtin" ? rawToolCallsByToolCallId.get(approval.toolCallId) : void 0) != null ? _a6 : {
|
|
1932
1989
|
type: "tool-call",
|
|
1933
1990
|
toolCallId: approval.toolCallId,
|
|
1934
1991
|
toolName: approval.toolName,
|
|
@@ -1936,14 +1993,29 @@ function runPrompt(input) {
|
|
|
1936
1993
|
providerExecuted: approval.providerExecuted,
|
|
1937
1994
|
nativeName: approval.nativeName
|
|
1938
1995
|
};
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1996
|
+
let validatedToolCall;
|
|
1997
|
+
let toolCall;
|
|
1998
|
+
if (approval.kind === "custom" && continuation.approved) {
|
|
1999
|
+
validatedToolCall = asToolCallTextStreamPart({
|
|
2000
|
+
part: await validateToolCall({
|
|
2001
|
+
event: rawToolCall,
|
|
2002
|
+
tools: input.tools
|
|
2003
|
+
})
|
|
2004
|
+
});
|
|
2005
|
+
toolCall = displayHostToolCall({
|
|
2006
|
+
toolCall: validatedToolCall,
|
|
2007
|
+
sessionWorkDir: input.sessionWorkDir
|
|
2008
|
+
});
|
|
2009
|
+
} else {
|
|
2010
|
+
const parsedInput = await safeParseJSON2({ text: rawToolCall.input });
|
|
2011
|
+
toolCall = {
|
|
2012
|
+
type: "tool-call",
|
|
2013
|
+
toolCallId: rawToolCall.toolCallId,
|
|
2014
|
+
toolName: rawToolCall.toolName,
|
|
2015
|
+
input: parsedInput.success ? parsedInput.value : rawToolCall.input,
|
|
2016
|
+
...rawToolCall.providerExecuted !== void 0 ? { providerExecuted: rawToolCall.providerExecuted } : {}
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
1947
2019
|
enqueueApprovalResponse(approval, continuation, toolCall);
|
|
1948
2020
|
onToolApprovalSettled(approval.approvalId);
|
|
1949
2021
|
pendingApprovalsByApprovalId.delete(approval.approvalId);
|
|
@@ -1973,6 +2045,27 @@ function runPrompt(input) {
|
|
|
1973
2045
|
});
|
|
1974
2046
|
return "continued";
|
|
1975
2047
|
}
|
|
2048
|
+
if (validatedToolCall == null) {
|
|
2049
|
+
throw new Error(
|
|
2050
|
+
`Harness '${input.harness.harnessId}' could not validate approved host tool '${approval.toolName}'.`
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
if (validatedToolCall.invalid) {
|
|
2054
|
+
const error = new Error(invalidToolInputMessage);
|
|
2055
|
+
await submitToolResult({
|
|
2056
|
+
toolCallId: approval.toolCallId,
|
|
2057
|
+
output: { error: invalidToolInputMessage },
|
|
2058
|
+
isError: true
|
|
2059
|
+
});
|
|
2060
|
+
bufferedToolOutcomes.push(() => {
|
|
2061
|
+
enqueueHostToolOutcome({
|
|
2062
|
+
toolCall,
|
|
2063
|
+
outcome: { ok: false, error }
|
|
2064
|
+
});
|
|
2065
|
+
});
|
|
2066
|
+
await publishToolExecutions();
|
|
2067
|
+
return "continued";
|
|
2068
|
+
}
|
|
1976
2069
|
await lifecycle.start(input.model);
|
|
1977
2070
|
toolExecutions.set(rawToolCall.toolCallId, {
|
|
1978
2071
|
toolCall
|
|
@@ -1980,6 +2073,7 @@ function runPrompt(input) {
|
|
|
1980
2073
|
const executionStartedAt = Date.now();
|
|
1981
2074
|
const execution = await maybeExecuteHostTool({
|
|
1982
2075
|
event: rawToolCall,
|
|
2076
|
+
parsedToolCall: validatedToolCall,
|
|
1983
2077
|
tools: activeTools,
|
|
1984
2078
|
toolsContext,
|
|
1985
2079
|
wrappedExecuteTool: lifecycle.executeTool,
|
|
@@ -2174,15 +2268,25 @@ function runPrompt(input) {
|
|
|
2174
2268
|
} else {
|
|
2175
2269
|
for (const part of translatedParts) result.enqueue(part);
|
|
2176
2270
|
}
|
|
2271
|
+
let validatedHostToolCall;
|
|
2177
2272
|
if (displayValue.type === "tool-call") {
|
|
2273
|
+
const isHostTool = value.type === "tool-call" && !value.providerExecuted && hasTool({ tools: activeTools, toolName: value.toolName }) && !(value.toolName === "askUserQuestions" && hasTool({
|
|
2274
|
+
tools: input.harness.builtinTools,
|
|
2275
|
+
toolName: value.toolName
|
|
2276
|
+
}));
|
|
2178
2277
|
const parsed = await validateToolCall({
|
|
2179
|
-
event: displayValue,
|
|
2278
|
+
event: isHostTool ? value : displayValue,
|
|
2180
2279
|
tools: input.tools
|
|
2181
2280
|
});
|
|
2182
2281
|
const parsedToolCall = asToolCallTextStreamPart({ part: parsed });
|
|
2282
|
+
validatedHostToolCall = isHostTool ? parsedToolCall : void 0;
|
|
2283
|
+
const displayToolCall = isHostTool ? displayHostToolCall({
|
|
2284
|
+
toolCall: parsedToolCall,
|
|
2285
|
+
sessionWorkDir: input.sessionWorkDir
|
|
2286
|
+
}) : parsedToolCall;
|
|
2183
2287
|
rawToolCallsByToolCallId.set(displayValue.toolCallId, displayValue);
|
|
2184
|
-
toolCallsByToolCallId.set(displayValue.toolCallId,
|
|
2185
|
-
result.enqueue(
|
|
2288
|
+
toolCallsByToolCallId.set(displayValue.toolCallId, displayToolCall);
|
|
2289
|
+
result.enqueue(displayToolCall);
|
|
2186
2290
|
}
|
|
2187
2291
|
if (value.type === "text-delta") {
|
|
2188
2292
|
stepText += value.delta;
|
|
@@ -2328,6 +2432,31 @@ function runPrompt(input) {
|
|
|
2328
2432
|
await finishForHostInputPause({ completeCurrentStep: true });
|
|
2329
2433
|
return;
|
|
2330
2434
|
}
|
|
2435
|
+
if (validatedHostToolCall == null) {
|
|
2436
|
+
throw new Error(
|
|
2437
|
+
`Harness '${input.harness.harnessId}' could not validate host tool '${toolCall.toolName}'.`
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
if (validatedHostToolCall.invalid) {
|
|
2441
|
+
settledHostToolCallIds.add(toolCall.toolCallId);
|
|
2442
|
+
toolExecutions.delete(toolCall.toolCallId);
|
|
2443
|
+
bufferedToolOutcomes.push(() => {
|
|
2444
|
+
result.enqueue({
|
|
2445
|
+
type: "tool-error",
|
|
2446
|
+
toolCallId: toolCall.toolCallId,
|
|
2447
|
+
toolName: toolCall.toolName,
|
|
2448
|
+
input: parsedToolCall.input,
|
|
2449
|
+
error: new Error(invalidToolInputMessage),
|
|
2450
|
+
dynamic: true
|
|
2451
|
+
});
|
|
2452
|
+
});
|
|
2453
|
+
await submitToolResult({
|
|
2454
|
+
toolCallId: toolCall.toolCallId,
|
|
2455
|
+
output: { error: invalidToolInputMessage },
|
|
2456
|
+
isError: true
|
|
2457
|
+
});
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2331
2460
|
const customToolApprovalDecision = resolveCustomToolApproval({
|
|
2332
2461
|
toolName: toolCall.toolName,
|
|
2333
2462
|
toolApproval: input.toolApproval
|
|
@@ -2427,6 +2556,7 @@ function runPrompt(input) {
|
|
|
2427
2556
|
const executionStartedAt = Date.now();
|
|
2428
2557
|
const execution = await maybeExecuteHostTool({
|
|
2429
2558
|
event: toolCall,
|
|
2559
|
+
parsedToolCall: validatedHostToolCall,
|
|
2430
2560
|
tools: activeTools,
|
|
2431
2561
|
toolsContext,
|
|
2432
2562
|
wrappedExecuteTool: lifecycle.executeTool,
|
|
@@ -2532,14 +2662,31 @@ function asToolCallTextStreamPart(input) {
|
|
|
2532
2662
|
}
|
|
2533
2663
|
return input.part;
|
|
2534
2664
|
}
|
|
2665
|
+
function displayHostToolCall(input) {
|
|
2666
|
+
return {
|
|
2667
|
+
...input.toolCall,
|
|
2668
|
+
input: stripParsedToolInputWorkDir({
|
|
2669
|
+
value: input.toolCall.input,
|
|
2670
|
+
sessionWorkDir: input.sessionWorkDir
|
|
2671
|
+
}),
|
|
2672
|
+
...input.toolCall.invalid ? { error: new Error(invalidToolInputMessage) } : {}
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2535
2675
|
function hasTool(input) {
|
|
2536
2676
|
return Object.prototype.hasOwnProperty.call(input.tools, input.toolName);
|
|
2537
2677
|
}
|
|
2538
2678
|
async function maybeExecuteHostTool(input) {
|
|
2539
2679
|
const tool = input.tools[input.event.toolName];
|
|
2540
2680
|
if (!isExecutableTool(tool)) return { executed: false };
|
|
2541
|
-
|
|
2542
|
-
|
|
2681
|
+
if (input.parsedToolCall.invalid) {
|
|
2682
|
+
const error = new Error(invalidToolInputMessage);
|
|
2683
|
+
await input.submitToolResult({
|
|
2684
|
+
toolCallId: input.event.toolCallId,
|
|
2685
|
+
output: { error: invalidToolInputMessage },
|
|
2686
|
+
isError: true
|
|
2687
|
+
});
|
|
2688
|
+
return { executed: true, outcome: { ok: false, error } };
|
|
2689
|
+
}
|
|
2543
2690
|
let context;
|
|
2544
2691
|
try {
|
|
2545
2692
|
context = await validateToolContext({
|
|
@@ -2562,7 +2709,7 @@ async function maybeExecuteHostTool(input) {
|
|
|
2562
2709
|
let output2;
|
|
2563
2710
|
const stream = executeTool({
|
|
2564
2711
|
tool,
|
|
2565
|
-
input:
|
|
2712
|
+
input: input.parsedToolCall.input,
|
|
2566
2713
|
options: {
|
|
2567
2714
|
toolCallId: input.event.toolCallId,
|
|
2568
2715
|
messages: [],
|
|
@@ -3251,7 +3398,7 @@ function collectHarnessAgentToolResultContinuations(input) {
|
|
|
3251
3398
|
}
|
|
3252
3399
|
|
|
3253
3400
|
// src/agent/internal/bootstrap-recipe.ts
|
|
3254
|
-
import { posix } from "path";
|
|
3401
|
+
import { posix as posix3 } from "path";
|
|
3255
3402
|
var BOOTSTRAP_SCHEMA_VERSION = 1;
|
|
3256
3403
|
async function hashHarnessBootstrap(recipe) {
|
|
3257
3404
|
const encoder = new TextEncoder();
|
|
@@ -3260,6 +3407,7 @@ async function hashHarnessBootstrap(recipe) {
|
|
|
3260
3407
|
chunks.push(encoder.encode(value));
|
|
3261
3408
|
chunks.push(encoder.encode("\0"));
|
|
3262
3409
|
};
|
|
3410
|
+
pushString(harnessStateDirectoryPath({ sandboxHomeDir: "$HOME" }));
|
|
3263
3411
|
pushString(recipe.harnessId);
|
|
3264
3412
|
pushString(recipe.bootstrapDir);
|
|
3265
3413
|
const sortedFiles = [...recipe.files].sort(
|
|
@@ -3289,12 +3437,12 @@ async function hashHarnessBootstrap(recipe) {
|
|
|
3289
3437
|
function bootstrapMarkerPath({
|
|
3290
3438
|
recipe,
|
|
3291
3439
|
identity,
|
|
3292
|
-
|
|
3440
|
+
stateDirectory
|
|
3293
3441
|
}) {
|
|
3294
|
-
return
|
|
3442
|
+
return posix3.join(
|
|
3295
3443
|
resolveBootstrapPath({
|
|
3296
3444
|
path: recipe.bootstrapDir,
|
|
3297
|
-
|
|
3445
|
+
stateDirectory
|
|
3298
3446
|
}),
|
|
3299
3447
|
`.bootstrap-${identity}.ok`
|
|
3300
3448
|
);
|
|
@@ -3303,13 +3451,13 @@ async function applyBootstrapRecipe({
|
|
|
3303
3451
|
session,
|
|
3304
3452
|
recipe,
|
|
3305
3453
|
identity,
|
|
3306
|
-
|
|
3454
|
+
stateDirectory,
|
|
3307
3455
|
abortSignal
|
|
3308
3456
|
}) {
|
|
3309
3457
|
const markerPath = bootstrapMarkerPath({
|
|
3310
3458
|
recipe,
|
|
3311
3459
|
identity,
|
|
3312
|
-
|
|
3460
|
+
stateDirectory
|
|
3313
3461
|
});
|
|
3314
3462
|
const existingMarker = await session.readTextFile({
|
|
3315
3463
|
path: markerPath,
|
|
@@ -3320,11 +3468,10 @@ async function applyBootstrapRecipe({
|
|
|
3320
3468
|
}
|
|
3321
3469
|
const bootstrapDir = resolveBootstrapPath({
|
|
3322
3470
|
path: recipe.bootstrapDir,
|
|
3323
|
-
|
|
3471
|
+
stateDirectory
|
|
3324
3472
|
});
|
|
3325
3473
|
const mkdirResult = await session.run({
|
|
3326
3474
|
command: 'mkdir -p "$BOOTSTRAP_DIR"',
|
|
3327
|
-
workingDirectory: defaultWorkingDirectory,
|
|
3328
3475
|
env: { BOOTSTRAP_DIR: bootstrapDir },
|
|
3329
3476
|
abortSignal
|
|
3330
3477
|
});
|
|
@@ -3338,7 +3485,7 @@ ${mkdirResult.stderr || mkdirResult.stdout}`
|
|
|
3338
3485
|
await session.writeTextFile({
|
|
3339
3486
|
path: resolveBootstrapPath({
|
|
3340
3487
|
path: file.path,
|
|
3341
|
-
|
|
3488
|
+
stateDirectory
|
|
3342
3489
|
}),
|
|
3343
3490
|
content: file.content,
|
|
3344
3491
|
abortSignal
|
|
@@ -3364,17 +3511,17 @@ ${result.stderr || result.stdout}`
|
|
|
3364
3511
|
});
|
|
3365
3512
|
}
|
|
3366
3513
|
function resolveBootstrapPath({
|
|
3367
|
-
path,
|
|
3368
|
-
|
|
3514
|
+
path: path2,
|
|
3515
|
+
stateDirectory
|
|
3369
3516
|
}) {
|
|
3370
|
-
return
|
|
3517
|
+
return posix3.isAbsolute(path2) ? path2 : posix3.resolve(stateDirectory, path2);
|
|
3371
3518
|
}
|
|
3372
3519
|
|
|
3373
3520
|
// src/agent/internal/sandbox-bootstrap.ts
|
|
3374
|
-
import { posix as
|
|
3521
|
+
import { posix as posix5 } from "path";
|
|
3375
3522
|
|
|
3376
3523
|
// src/utils/resolve-sandbox-default-working-directory.ts
|
|
3377
|
-
import { posix as
|
|
3524
|
+
import { posix as posix4 } from "path";
|
|
3378
3525
|
async function resolveSandboxDefaultWorkingDirectory({
|
|
3379
3526
|
sandboxSession,
|
|
3380
3527
|
abortSignal
|
|
@@ -3392,7 +3539,7 @@ async function resolveSandboxDefaultWorkingDirectory({
|
|
|
3392
3539
|
);
|
|
3393
3540
|
}
|
|
3394
3541
|
const cwd = result.stdout.trim();
|
|
3395
|
-
if (!
|
|
3542
|
+
if (!posix4.isAbsolute(cwd)) {
|
|
3396
3543
|
throw new Error(
|
|
3397
3544
|
`Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
|
|
3398
3545
|
);
|
|
@@ -3400,6 +3547,25 @@ async function resolveSandboxDefaultWorkingDirectory({
|
|
|
3400
3547
|
return cwd === "/" ? cwd : cwd.replace(/\/+$/, "");
|
|
3401
3548
|
}
|
|
3402
3549
|
|
|
3550
|
+
// src/utils/sandbox-home-dir.ts
|
|
3551
|
+
import path from "path";
|
|
3552
|
+
async function resolveSandboxHomeDir({
|
|
3553
|
+
sandbox,
|
|
3554
|
+
abortSignal
|
|
3555
|
+
}) {
|
|
3556
|
+
const result = await sandbox.run({
|
|
3557
|
+
command: 'printf "%s" "$HOME"',
|
|
3558
|
+
abortSignal
|
|
3559
|
+
});
|
|
3560
|
+
const homeDir = result.stdout.trim();
|
|
3561
|
+
if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
|
|
3562
|
+
throw new Error(
|
|
3563
|
+
`Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`
|
|
3564
|
+
);
|
|
3565
|
+
}
|
|
3566
|
+
return homeDir;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3403
3569
|
// src/agent/internal/sandbox-bootstrap.ts
|
|
3404
3570
|
var SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
|
|
3405
3571
|
function validateSandboxBootstrapSettings(settings) {
|
|
@@ -3426,10 +3592,10 @@ function normalizeSandboxWorkDir(workDir) {
|
|
|
3426
3592
|
"HarnessAgent: `sandboxConfig.workDir` must use POSIX path separators."
|
|
3427
3593
|
);
|
|
3428
3594
|
}
|
|
3429
|
-
if (
|
|
3595
|
+
if (posix5.isAbsolute(workDir)) {
|
|
3430
3596
|
throw new Error("HarnessAgent: `sandboxConfig.workDir` must be relative.");
|
|
3431
3597
|
}
|
|
3432
|
-
const normalized =
|
|
3598
|
+
const normalized = posix5.normalize(workDir);
|
|
3433
3599
|
if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
3434
3600
|
throw new Error(
|
|
3435
3601
|
"HarnessAgent: `sandboxConfig.workDir` must stay inside the sandbox default working directory."
|
|
@@ -3445,7 +3611,7 @@ function resolveSessionWorkDir({
|
|
|
3445
3611
|
}) {
|
|
3446
3612
|
return joinSandboxPath({
|
|
3447
3613
|
base: defaultWorkingDirectory,
|
|
3448
|
-
path: workDir != null ? workDir : `${harnessId}-${sessionId}`
|
|
3614
|
+
path: workDir != null ? workDir : `${encodeHarnessPathSegment(harnessId)}-${encodeHarnessPathSegment(sessionId)}`
|
|
3449
3615
|
});
|
|
3450
3616
|
}
|
|
3451
3617
|
async function createSandboxBootstrapPlan({
|
|
@@ -3488,20 +3654,30 @@ async function runSandboxBootstrap({
|
|
|
3488
3654
|
abortSignal
|
|
3489
3655
|
}) {
|
|
3490
3656
|
if (recipe == null && onBootstrap == null) return;
|
|
3491
|
-
const resolvedDefaultWorkingDirectory = defaultWorkingDirectory != null ? defaultWorkingDirectory : await resolveSandboxDefaultWorkingDirectory({
|
|
3492
|
-
sandboxSession: session,
|
|
3493
|
-
abortSignal
|
|
3494
|
-
});
|
|
3495
3657
|
if (recipe != null && recipeIdentity != null) {
|
|
3496
3658
|
await applyBootstrapRecipe({
|
|
3497
3659
|
session,
|
|
3498
3660
|
recipe,
|
|
3499
3661
|
identity: recipeIdentity,
|
|
3500
|
-
|
|
3662
|
+
// Harness infrastructure always lives under the sandbox's own HOME,
|
|
3663
|
+
// never the working directory resolved below for the caller's own
|
|
3664
|
+
// `onBootstrap` hook. Resolved directly from `session` — this runs
|
|
3665
|
+
// from a provider's `onFirstCreate`, before a
|
|
3666
|
+
// `HarnessV1NetworkSandboxSession` even exists.
|
|
3667
|
+
stateDirectory: harnessStateDirectoryPath({
|
|
3668
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
3669
|
+
sandbox: session,
|
|
3670
|
+
abortSignal
|
|
3671
|
+
})
|
|
3672
|
+
}),
|
|
3501
3673
|
abortSignal
|
|
3502
3674
|
});
|
|
3503
3675
|
}
|
|
3504
3676
|
if (onBootstrap == null) return;
|
|
3677
|
+
const resolvedDefaultWorkingDirectory = defaultWorkingDirectory != null ? defaultWorkingDirectory : await resolveSandboxDefaultWorkingDirectory({
|
|
3678
|
+
sandboxSession: session,
|
|
3679
|
+
abortSignal
|
|
3680
|
+
});
|
|
3505
3681
|
const bootstrapWorkDir = workDir == null ? resolvedDefaultWorkingDirectory : joinSandboxPath({
|
|
3506
3682
|
base: resolvedDefaultWorkingDirectory,
|
|
3507
3683
|
path: workDir
|
|
@@ -3561,9 +3737,9 @@ async function hashSandboxBootstrapIdentity({
|
|
|
3561
3737
|
}
|
|
3562
3738
|
function joinSandboxPath({
|
|
3563
3739
|
base,
|
|
3564
|
-
path
|
|
3740
|
+
path: path2
|
|
3565
3741
|
}) {
|
|
3566
|
-
return
|
|
3742
|
+
return posix5.join(base, path2);
|
|
3567
3743
|
}
|
|
3568
3744
|
|
|
3569
3745
|
// src/agent/internal/resolve-observability.ts
|
|
@@ -3829,7 +4005,14 @@ var HarnessAgent = class {
|
|
|
3829
4005
|
session: toolSafeSandboxSession,
|
|
3830
4006
|
recipe,
|
|
3831
4007
|
identity: recipeIdentity,
|
|
3832
|
-
|
|
4008
|
+
// Harness infrastructure always lives under the sandbox's own
|
|
4009
|
+
// HOME, never the session's working directory.
|
|
4010
|
+
stateDirectory: harnessStateDirectoryPath({
|
|
4011
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
4012
|
+
sandbox: toolSafeSandboxSession,
|
|
4013
|
+
abortSignal
|
|
4014
|
+
})
|
|
4015
|
+
}),
|
|
3833
4016
|
abortSignal
|
|
3834
4017
|
});
|
|
3835
4018
|
} catch (err) {
|
|
@@ -3872,7 +4055,14 @@ var HarnessAgent = class {
|
|
|
3872
4055
|
session: resumedSandboxSession.restricted(),
|
|
3873
4056
|
recipe,
|
|
3874
4057
|
identity: recipeIdentity,
|
|
3875
|
-
|
|
4058
|
+
// Harness infrastructure always lives under the sandbox's own
|
|
4059
|
+
// HOME, never the working directory.
|
|
4060
|
+
stateDirectory: harnessStateDirectoryPath({
|
|
4061
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
4062
|
+
sandbox: resumedSandboxSession,
|
|
4063
|
+
abortSignal
|
|
4064
|
+
})
|
|
4065
|
+
}),
|
|
3876
4066
|
abortSignal
|
|
3877
4067
|
});
|
|
3878
4068
|
} catch (err) {
|
|
@@ -3907,7 +4097,14 @@ var HarnessAgent = class {
|
|
|
3907
4097
|
session: createdSandboxSession.restricted(),
|
|
3908
4098
|
recipe: sandboxBootstrapPlan.recipe,
|
|
3909
4099
|
identity: sandboxBootstrapPlan.recipeIdentity,
|
|
3910
|
-
|
|
4100
|
+
// Harness infrastructure always lives under the sandbox's own
|
|
4101
|
+
// HOME, never the working directory.
|
|
4102
|
+
stateDirectory: harnessStateDirectoryPath({
|
|
4103
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
4104
|
+
sandbox: createdSandboxSession,
|
|
4105
|
+
abortSignal
|
|
4106
|
+
})
|
|
4107
|
+
}),
|
|
3911
4108
|
abortSignal
|
|
3912
4109
|
});
|
|
3913
4110
|
} catch (err) {
|
|
@@ -4147,7 +4344,6 @@ var HarnessAgent = class {
|
|
|
4147
4344
|
};
|
|
4148
4345
|
}
|
|
4149
4346
|
}
|
|
4150
|
-
return void 0;
|
|
4151
4347
|
}
|
|
4152
4348
|
/*
|
|
4153
4349
|
* Reduce AI SDK input to the single user message the harness should run
|
|
@@ -4451,11 +4647,19 @@ async function prepareHarnessSandboxTemplate(options) {
|
|
|
4451
4647
|
});
|
|
4452
4648
|
try {
|
|
4453
4649
|
if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
|
|
4650
|
+
const restrictedSession = sandboxSession.restricted();
|
|
4454
4651
|
await applyBootstrapRecipe({
|
|
4455
|
-
session:
|
|
4652
|
+
session: restrictedSession,
|
|
4456
4653
|
recipe: bootstrapPlan.recipe,
|
|
4457
4654
|
identity: bootstrapPlan.recipeIdentity,
|
|
4458
|
-
|
|
4655
|
+
// Harness infrastructure always lives under the sandbox's own HOME,
|
|
4656
|
+
// never the working directory.
|
|
4657
|
+
stateDirectory: harnessStateDirectoryPath({
|
|
4658
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
4659
|
+
sandbox: restrictedSession,
|
|
4660
|
+
abortSignal
|
|
4661
|
+
})
|
|
4662
|
+
}),
|
|
4459
4663
|
abortSignal
|
|
4460
4664
|
});
|
|
4461
4665
|
}
|
|
@@ -4485,7 +4689,7 @@ async function prepareSandboxForHarness(options) {
|
|
|
4485
4689
|
const workDir = sandboxConfig.workDir == null ? void 0 : normalizeSandboxWorkDir(sandboxConfig.workDir);
|
|
4486
4690
|
const recipeIdentities = {};
|
|
4487
4691
|
const skippedHarnessIds = [];
|
|
4488
|
-
let
|
|
4692
|
+
let stateDirectory;
|
|
4489
4693
|
for (const harness of harnesses) {
|
|
4490
4694
|
const recipe = await ((_b4 = harness.getBootstrap) == null ? void 0 : _b4.call(harness, {
|
|
4491
4695
|
abortSignal: options.abortSignal
|
|
@@ -4496,19 +4700,24 @@ async function prepareSandboxForHarness(options) {
|
|
|
4496
4700
|
}
|
|
4497
4701
|
const recipeIdentity = await hashHarnessBootstrap(recipe);
|
|
4498
4702
|
recipeIdentities[harness.harnessId] = recipeIdentity;
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4703
|
+
stateDirectory != null ? stateDirectory : stateDirectory = harnessStateDirectoryPath({
|
|
4704
|
+
sandboxHomeDir: await resolveSandboxHomeDir({
|
|
4705
|
+
sandbox: options.session,
|
|
4706
|
+
abortSignal: options.abortSignal
|
|
4707
|
+
})
|
|
4502
4708
|
});
|
|
4503
4709
|
await applyBootstrapRecipe({
|
|
4504
4710
|
session: options.session,
|
|
4505
4711
|
recipe,
|
|
4506
4712
|
identity: recipeIdentity,
|
|
4507
|
-
|
|
4713
|
+
stateDirectory,
|
|
4508
4714
|
abortSignal: options.abortSignal
|
|
4509
4715
|
});
|
|
4510
4716
|
}
|
|
4511
4717
|
if (sandboxConfig.onBootstrap != null) {
|
|
4718
|
+
const defaultWorkingDirectory = await resolveSandboxDefaultWorkingDirectory(
|
|
4719
|
+
{ sandboxSession: options.session, abortSignal: options.abortSignal }
|
|
4720
|
+
);
|
|
4512
4721
|
await runSandboxBootstrap({
|
|
4513
4722
|
session: options.session,
|
|
4514
4723
|
workDir,
|
|
@@ -4604,7 +4813,7 @@ import { appendFileSync, mkdirSync } from "fs";
|
|
|
4604
4813
|
function createFileReporter(options) {
|
|
4605
4814
|
var _a4, _b4;
|
|
4606
4815
|
const fileName = (_a4 = options.fileName) != null ? _a4 : "events.jsonl";
|
|
4607
|
-
const
|
|
4816
|
+
const path2 = `${options.dir}/${fileName}`;
|
|
4608
4817
|
const failOnly = (_b4 = options.failOnly) != null ? _b4 : false;
|
|
4609
4818
|
const turns = /* @__PURE__ */ new Map();
|
|
4610
4819
|
let lastOpenCallId;
|
|
@@ -4632,7 +4841,7 @@ function createFileReporter(options) {
|
|
|
4632
4841
|
mkdirSync(options.dir, { recursive: true });
|
|
4633
4842
|
dirReady = true;
|
|
4634
4843
|
}
|
|
4635
|
-
appendFileSync(
|
|
4844
|
+
appendFileSync(path2, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
|
|
4636
4845
|
} catch (e) {
|
|
4637
4846
|
}
|
|
4638
4847
|
};
|