@ai-sdk/harness 1.0.0-canary.9 → 1.0.1
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 +150 -0
- package/README.md +27 -15
- package/dist/agent/index.d.ts +45 -12
- package/dist/agent/index.js +258 -31
- package/dist/agent/index.js.map +1 -1
- package/dist/bridge/index.js +3 -2
- package/dist/bridge/index.js.map +1 -1
- package/dist/utils/index.d.ts +8 -1
- package/dist/utils/index.js +13 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +6 -6
- package/src/agent/harness-agent-settings.ts +47 -12
- package/src/agent/harness-agent.ts +70 -35
- package/src/agent/internal/bootstrap-recipe.ts +1 -1
- package/src/agent/internal/harness-stream-text-result.ts +12 -1
- package/src/agent/internal/run-prompt.ts +13 -1
- package/src/agent/internal/sandbox-bootstrap.ts +266 -0
- package/src/agent/prewarm.ts +29 -11
- package/src/bridge/index.ts +4 -2
- package/src/utils/ai-gateway-auth.ts +15 -0
- package/src/utils/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,155 @@
|
|
|
1
1
|
# @ai-sdk/harness
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ai@7.0.1
|
|
8
|
+
|
|
9
|
+
## 1.0.0
|
|
10
|
+
|
|
11
|
+
### Major Changes
|
|
12
|
+
|
|
13
|
+
- 9d6dbe0: feat(harness): add sandbox specific expansion for harness abstraction, add `sandbox-just-bash` and `sandbox-vercel`
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- e5d4a24: chore(harness): update ws package
|
|
18
|
+
- aae0138: fix(harness): make listening for sandbox bridge readiness compatible with Bun
|
|
19
|
+
- be83911: fix(harness): reject bridge startup when the WebSocket port cannot be bound
|
|
20
|
+
- 3d87086: fix(harness): guard against invalid resuming a session vs continuing a turn
|
|
21
|
+
- d77bed4: chore(harness): separate harness spec types (v1) from consumer-facing types
|
|
22
|
+
- 21d3d60: feat(harness): implement harness specification
|
|
23
|
+
- 3d9a50c: feat(harness): implement harness adapters for Claude Code, Codex, Pi
|
|
24
|
+
- 57e0a59: fix(harness): ensure finish chunk's total usage is actually coming from total usage
|
|
25
|
+
- 6c7a3e5: Start the `1.0.0` canary release line for the experimental harness and sandbox packages. They were unintentionally published as `0.0.0-canary.*` because they were scaffolded with a `0.0.0-canary.0` premajor version, which semver could not advance past on a major bump.
|
|
26
|
+
- 1ea15a3: fix(harness): fix various bugs with harness skills not being correctly processed by the harness adapters
|
|
27
|
+
- a83a367: feat(harness): allow pre-snapshot `sandboxConfig.onBootstrap` callback in `HarnessAgent`
|
|
28
|
+
- b8396f0: trigger initial beta release
|
|
29
|
+
- 534dac6: fix(harness): fix incomplete OIDC token support for AI Gateway auth in harness adapters
|
|
30
|
+
|
|
31
|
+
## 1.0.0-beta.27
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- ai@7.0.0-beta.187
|
|
36
|
+
|
|
37
|
+
## 1.0.0-beta.26
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- a83a367: feat(harness): allow pre-snapshot `sandboxConfig.onBootstrap` callback in `HarnessAgent`
|
|
42
|
+
|
|
43
|
+
## 1.0.0-beta.25
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- ai@7.0.0-beta.186
|
|
48
|
+
|
|
49
|
+
## 1.0.0-beta.24
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- Updated dependencies [75763b0]
|
|
54
|
+
- ai@7.0.0-beta.185
|
|
55
|
+
|
|
56
|
+
## 1.0.0-beta.23
|
|
57
|
+
|
|
58
|
+
### Patch Changes
|
|
59
|
+
|
|
60
|
+
- 57e0a59: fix(harness): ensure finish chunk's total usage is actually coming from total usage
|
|
61
|
+
|
|
62
|
+
## 1.0.0-beta.22
|
|
63
|
+
|
|
64
|
+
### Patch Changes
|
|
65
|
+
|
|
66
|
+
- Updated dependencies [0416e3e]
|
|
67
|
+
- @ai-sdk/provider@4.0.0-beta.20
|
|
68
|
+
- ai@7.0.0-beta.184
|
|
69
|
+
- @ai-sdk/provider-utils@5.0.0-beta.50
|
|
70
|
+
|
|
71
|
+
## 1.0.0-beta.21
|
|
72
|
+
|
|
73
|
+
### Patch Changes
|
|
74
|
+
|
|
75
|
+
- ai@7.0.0-beta.183
|
|
76
|
+
|
|
77
|
+
## 1.0.0-beta.20
|
|
78
|
+
|
|
79
|
+
### Patch Changes
|
|
80
|
+
|
|
81
|
+
- e5d4a24: chore(harness): update ws package
|
|
82
|
+
- Updated dependencies [cc6ab90]
|
|
83
|
+
- ai@7.0.0-beta.182
|
|
84
|
+
|
|
85
|
+
## 1.0.0-beta.19
|
|
86
|
+
|
|
87
|
+
### Patch Changes
|
|
88
|
+
|
|
89
|
+
- Updated dependencies [6a2caf9]
|
|
90
|
+
- ai@7.0.0-beta.181
|
|
91
|
+
|
|
92
|
+
## 1.0.0-beta.18
|
|
93
|
+
|
|
94
|
+
### Patch Changes
|
|
95
|
+
|
|
96
|
+
- Updated dependencies [81a284b]
|
|
97
|
+
- ai@7.0.0-beta.180
|
|
98
|
+
|
|
99
|
+
## 1.0.0-beta.17
|
|
100
|
+
|
|
101
|
+
### Patch Changes
|
|
102
|
+
|
|
103
|
+
- 534dac6: fix(harness): fix incomplete OIDC token support for AI Gateway auth in harness adapters
|
|
104
|
+
|
|
105
|
+
## 1.0.0-beta.16
|
|
106
|
+
|
|
107
|
+
### Patch Changes
|
|
108
|
+
|
|
109
|
+
- ai@7.0.0-beta.179
|
|
110
|
+
|
|
111
|
+
## 1.0.0-beta.15
|
|
112
|
+
|
|
113
|
+
### Patch Changes
|
|
114
|
+
|
|
115
|
+
- Updated dependencies [b097c52]
|
|
116
|
+
- ai@7.0.0-beta.178
|
|
117
|
+
|
|
118
|
+
## 1.0.0-beta.14
|
|
119
|
+
|
|
120
|
+
### Patch Changes
|
|
121
|
+
|
|
122
|
+
- b8396f0: trigger initial beta release
|
|
123
|
+
- Updated dependencies [b8396f0]
|
|
124
|
+
- @ai-sdk/provider-utils@5.0.0-beta.49
|
|
125
|
+
- @ai-sdk/provider@4.0.0-beta.19
|
|
126
|
+
- ai@7.0.0-beta.177
|
|
127
|
+
|
|
128
|
+
## 1.0.0-canary.13
|
|
129
|
+
|
|
130
|
+
### Patch Changes
|
|
131
|
+
|
|
132
|
+
- ai@7.0.0-canary.176
|
|
133
|
+
|
|
134
|
+
## 1.0.0-canary.12
|
|
135
|
+
|
|
136
|
+
### Patch Changes
|
|
137
|
+
|
|
138
|
+
- Updated dependencies [6ec57f5]
|
|
139
|
+
- ai@7.0.0-canary.175
|
|
140
|
+
|
|
141
|
+
## 1.0.0-canary.11
|
|
142
|
+
|
|
143
|
+
### Patch Changes
|
|
144
|
+
|
|
145
|
+
- be83911: fix(harness): reject bridge startup when the WebSocket port cannot be bound
|
|
146
|
+
|
|
147
|
+
## 1.0.0-canary.10
|
|
148
|
+
|
|
149
|
+
### Patch Changes
|
|
150
|
+
|
|
151
|
+
- ai@7.0.0-canary.174
|
|
152
|
+
|
|
3
153
|
## 1.0.0-canary.9
|
|
4
154
|
|
|
5
155
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -27,12 +27,25 @@ const agent = new HarnessAgent({
|
|
|
27
27
|
runtime: 'node24',
|
|
28
28
|
ports: [4000],
|
|
29
29
|
}),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
sandboxConfig: {
|
|
31
|
+
bootstrapHash: 'ripgrep-v1',
|
|
32
|
+
onBootstrap: async ({ session, abortSignal }) => {
|
|
33
|
+
const result = await session.run({
|
|
34
|
+
command:
|
|
35
|
+
'command -v rg >/dev/null || (apt-get update && apt-get install -y ripgrep)',
|
|
36
|
+
abortSignal,
|
|
37
|
+
});
|
|
38
|
+
if (result.exitCode !== 0) {
|
|
39
|
+
throw new Error(`Failed to install ripgrep: ${result.stderr}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
onSession: async ({ session, sessionWorkDir, abortSignal }) => {
|
|
43
|
+
await session.writeTextFile({
|
|
44
|
+
path: `${sessionWorkDir}/README.md`,
|
|
45
|
+
content: 'Workspace notes for this session.',
|
|
46
|
+
abortSignal,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
36
49
|
},
|
|
37
50
|
tools: {
|
|
38
51
|
deploy: tool({
|
|
@@ -70,18 +83,17 @@ try {
|
|
|
70
83
|
}
|
|
71
84
|
```
|
|
72
85
|
|
|
73
|
-
|
|
86
|
+
Use `session.detach()` to park a bridge-backed session for later attach, `session.stop()` to save state and stop the sandbox, or `session.destroy()` to clean up without keeping resume state. Bridge-backed adapters (claude-code, codex) require a sandbox provider that exposes ports — `@ai-sdk/sandbox-vercel` is the supported choice today. `@ai-sdk/sandbox-just-bash` is suitable only for non-bridge flows.
|
|
87
|
+
|
|
88
|
+
`sandbox` is a required `HarnessV1SandboxProvider` — the agent calls `provider.createSession()` when a session starts. Use `sandboxConfig` for agent specific sandbox configuration that works independently from the sandbox provider that is used:
|
|
89
|
+
|
|
90
|
+
- Use `sandboxConfig.onSession` to prepare the acquired sandbox before the harness adapter starts. The hook runs for fresh and resumed sessions, so keep it idempotent.
|
|
91
|
+
- Use `sandboxConfig.onBootstrap` for expensive sandbox setup that should be baked into a reusable snapshot, such as installing tools or cloning a large repository. Provide `sandboxConfig.bootstrapHash` with it and change that value whenever the bootstrap output should invalidate the cached snapshot.
|
|
92
|
+
- Use `sandboxConfig.workDir` to set a stable working directory for the agent, relative to the sandbox's default working directory; otherwise regular sessions use the existing `<harnessId>-<sessionId>` directory. In that case, the `onBootstrap` callback receives the sandbox's default working directory.
|
|
74
93
|
|
|
75
94
|
### Available harnesses
|
|
76
95
|
|
|
77
|
-
|
|
78
|
-
- `@ai-sdk/harness-codex`
|
|
79
|
-
- `@ai-sdk/harness-opencode` (WIP)
|
|
80
|
-
- `@ai-sdk/harness-goose` (WIP)
|
|
81
|
-
- `@ai-sdk/harness-mastra` (WIP)
|
|
82
|
-
- `@ai-sdk/harness-deepagents` (WIP)
|
|
83
|
-
- `@ai-sdk/harness-openai-agents` (WIP)
|
|
84
|
-
- `@ai-sdk/harness-pi` (WIP)
|
|
96
|
+
See the [harness adapters documentation](https://ai-sdk.dev/v7/docs/ai-sdk-harnesses/harness-adapters).
|
|
85
97
|
|
|
86
98
|
## Implementing a harness
|
|
87
99
|
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -1039,6 +1039,44 @@ type HarnessAgentSkill = HarnessV1Skill;
|
|
|
1039
1039
|
type HarnessAgentPermissionMode = HarnessV1PermissionMode;
|
|
1040
1040
|
|
|
1041
1041
|
type HarnessAgentToolApprovalConfiguration = Readonly<Record<string, ToolApprovalStatus>>;
|
|
1042
|
+
type HarnessAgentSandboxConfig = {
|
|
1043
|
+
/**
|
|
1044
|
+
* Optional fixed working directory for all sessions, relative to the
|
|
1045
|
+
* sandbox's default working directory. When omitted, sessions keep the
|
|
1046
|
+
* existing `<harnessId>-<sessionId>` work directory.
|
|
1047
|
+
*/
|
|
1048
|
+
readonly workDir?: string;
|
|
1049
|
+
/**
|
|
1050
|
+
* Caller-controlled identity for `onBootstrap`. Change this whenever the
|
|
1051
|
+
* bootstrap side effects should invalidate the reusable sandbox snapshot.
|
|
1052
|
+
*/
|
|
1053
|
+
readonly bootstrapHash?: string;
|
|
1054
|
+
/**
|
|
1055
|
+
* Called during sandbox template creation after the harness adapter's own
|
|
1056
|
+
* bootstrap has run and before snapshot-capable providers publish a snapshot.
|
|
1057
|
+
*
|
|
1058
|
+
* `bootstrapHash` must be provided with this callback.
|
|
1059
|
+
*/
|
|
1060
|
+
readonly onBootstrap?: (opts: {
|
|
1061
|
+
readonly session: Experimental_SandboxSession;
|
|
1062
|
+
readonly workDir: string;
|
|
1063
|
+
readonly abortSignal?: AbortSignal;
|
|
1064
|
+
}) => Promise<void>;
|
|
1065
|
+
/**
|
|
1066
|
+
* Called after each sandbox session is acquired and the session work
|
|
1067
|
+
* directory exists, before the harness adapter starts. Runs for fresh and
|
|
1068
|
+
* resumed sessions.
|
|
1069
|
+
*
|
|
1070
|
+
* Use this to write per-session config, install lightweight tools, activate
|
|
1071
|
+
* licenses, or prepare files in `sessionWorkDir`. Keep it idempotent if the
|
|
1072
|
+
* agent may resume sessions.
|
|
1073
|
+
*/
|
|
1074
|
+
readonly onSession?: (opts: {
|
|
1075
|
+
readonly session: Experimental_SandboxSession;
|
|
1076
|
+
readonly sessionWorkDir: string;
|
|
1077
|
+
readonly abortSignal?: AbortSignal;
|
|
1078
|
+
}) => Promise<void>;
|
|
1079
|
+
};
|
|
1042
1080
|
/**
|
|
1043
1081
|
* Construction-time settings for a `HarnessAgent`.
|
|
1044
1082
|
*
|
|
@@ -1102,19 +1140,11 @@ type HarnessAgentSettings<THarness extends HarnessAgentAdapter<any> = HarnessAge
|
|
|
1102
1140
|
*/
|
|
1103
1141
|
readonly sandbox: HarnessV1SandboxProvider;
|
|
1104
1142
|
/**
|
|
1105
|
-
*
|
|
1106
|
-
* directory exists, before the harness adapter starts. Runs for fresh and
|
|
1107
|
-
* resumed sessions.
|
|
1108
|
-
*
|
|
1109
|
-
* Use this to write per-session config, install lightweight tools, activate
|
|
1110
|
-
* licenses, or prepare files in `sessionWorkDir`. Keep it idempotent if the
|
|
1111
|
-
* agent may resume sessions.
|
|
1143
|
+
* Sandbox working-directory and lifecycle hook configuration.
|
|
1112
1144
|
*/
|
|
1113
|
-
readonly
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
readonly abortSignal?: AbortSignal;
|
|
1117
|
-
}) => Promise<void>;
|
|
1145
|
+
readonly sandboxConfig?: HarnessAgentSandboxConfig;
|
|
1146
|
+
/** @deprecated Use `sandboxConfig.onSession` instead. */
|
|
1147
|
+
readonly onSandboxSession?: HarnessAgentSandboxConfig['onSession'];
|
|
1118
1148
|
/**
|
|
1119
1149
|
* Telemetry configuration. The harness drives AI SDK's pluggable
|
|
1120
1150
|
* `Telemetry` integration contract from the turn lifecycle, so a harness turn
|
|
@@ -1356,6 +1386,7 @@ declare class HarnessAgent<THarness extends HarnessAgentAdapter<any> = HarnessAg
|
|
|
1356
1386
|
*/
|
|
1357
1387
|
readonly tools: HarnessAllTools<THarness, TUserTools>;
|
|
1358
1388
|
private readonly settings;
|
|
1389
|
+
private readonly sandboxConfig;
|
|
1359
1390
|
private readonly userTools;
|
|
1360
1391
|
private readonly permissionMode;
|
|
1361
1392
|
constructor(settings: HarnessAgentSettings<THarness, TUserTools>);
|
|
@@ -1423,6 +1454,7 @@ declare class HarnessAgent<THarness extends HarnessAgentAdapter<any> = HarnessAg
|
|
|
1423
1454
|
private _toGenerateResult;
|
|
1424
1455
|
}
|
|
1425
1456
|
|
|
1457
|
+
type SandboxBootstrapSettings = Omit<HarnessAgentSandboxConfig, 'onSession'>;
|
|
1426
1458
|
/**
|
|
1427
1459
|
* Pre-build a harness's sandbox template without running an agent. Idempotent:
|
|
1428
1460
|
* if the template already exists (snapshot present, or marker on a non-snapshot
|
|
@@ -1440,6 +1472,7 @@ declare class HarnessAgent<THarness extends HarnessAgentAdapter<any> = HarnessAg
|
|
|
1440
1472
|
declare function prewarmHarness(options: {
|
|
1441
1473
|
readonly harness: HarnessAgentAdapter;
|
|
1442
1474
|
readonly sandboxProvider: HarnessV1SandboxProvider;
|
|
1475
|
+
readonly sandboxConfig?: SandboxBootstrapSettings;
|
|
1443
1476
|
readonly abortSignal?: AbortSignal;
|
|
1444
1477
|
}): Promise<void>;
|
|
1445
1478
|
|
package/dist/agent/index.js
CHANGED
|
@@ -317,9 +317,15 @@ var HarnessStreamTextResult = class {
|
|
|
317
317
|
/**
|
|
318
318
|
* Resolve every delayed promise and close `fullStream`. Idempotent.
|
|
319
319
|
*/
|
|
320
|
-
async finish() {
|
|
320
|
+
async finish(input) {
|
|
321
321
|
if (this.settled) return;
|
|
322
322
|
this.settled = true;
|
|
323
|
+
if (input != null) {
|
|
324
|
+
this.finalFinishReason = input.finishReason.unified;
|
|
325
|
+
this.finalRawFinishReason = input.finishReason.raw;
|
|
326
|
+
this.finalProviderMetadata = input.providerMetadata;
|
|
327
|
+
this.accumulatedUsage = asLanguageModelUsage(input.totalUsage);
|
|
328
|
+
}
|
|
323
329
|
if (this.currentStepContent.length > 0) {
|
|
324
330
|
const trailingStep = new DefaultStepResult({
|
|
325
331
|
callId: generateId(),
|
|
@@ -1224,6 +1230,7 @@ function runPrompt(input) {
|
|
|
1224
1230
|
])
|
|
1225
1231
|
);
|
|
1226
1232
|
const settledApprovalToolCallIds = /* @__PURE__ */ new Set();
|
|
1233
|
+
let finalFinish;
|
|
1227
1234
|
let stepText = "";
|
|
1228
1235
|
let stepReasoning = "";
|
|
1229
1236
|
let stepToolCalls = [];
|
|
@@ -1485,6 +1492,7 @@ function runPrompt(input) {
|
|
|
1485
1492
|
});
|
|
1486
1493
|
}
|
|
1487
1494
|
if (value.type === "finish") {
|
|
1495
|
+
finalFinish = value;
|
|
1488
1496
|
telemetry.end({
|
|
1489
1497
|
finishReason: value.finishReason,
|
|
1490
1498
|
usage: value.totalUsage
|
|
@@ -1606,7 +1614,13 @@ function runPrompt(input) {
|
|
|
1606
1614
|
}
|
|
1607
1615
|
}
|
|
1608
1616
|
(_g = input.onTurnFinished) == null ? void 0 : _g.call(input);
|
|
1609
|
-
await result.finish(
|
|
1617
|
+
await result.finish(
|
|
1618
|
+
finalFinish ? {
|
|
1619
|
+
finishReason: finalFinish.finishReason,
|
|
1620
|
+
totalUsage: finalFinish.totalUsage,
|
|
1621
|
+
providerMetadata: finalFinish.harnessMetadata
|
|
1622
|
+
} : void 0
|
|
1623
|
+
);
|
|
1610
1624
|
} catch (err) {
|
|
1611
1625
|
telemetry.error(err);
|
|
1612
1626
|
result.fail(err);
|
|
@@ -2118,7 +2132,7 @@ function collectHarnessAgentToolApprovalContinuations(input) {
|
|
|
2118
2132
|
|
|
2119
2133
|
// src/agent/internal/bootstrap-recipe.ts
|
|
2120
2134
|
var BOOTSTRAP_SCHEMA_VERSION = 1;
|
|
2121
|
-
async function
|
|
2135
|
+
async function hashHarnessBootstrap(recipe) {
|
|
2122
2136
|
const encoder = new TextEncoder();
|
|
2123
2137
|
const chunks = [];
|
|
2124
2138
|
const pushString = (value) => {
|
|
@@ -2190,6 +2204,188 @@ ${result.stderr || result.stdout}`
|
|
|
2190
2204
|
});
|
|
2191
2205
|
}
|
|
2192
2206
|
|
|
2207
|
+
// src/agent/internal/sandbox-bootstrap.ts
|
|
2208
|
+
import { posix } from "path";
|
|
2209
|
+
var SANDBOX_BOOTSTRAP_IDENTITY_VERSION = 1;
|
|
2210
|
+
function validateSandboxBootstrapSettings(settings) {
|
|
2211
|
+
if (settings.onBootstrap == null !== (settings.bootstrapHash == null)) {
|
|
2212
|
+
throw new Error(
|
|
2213
|
+
"HarnessAgent: `sandboxConfig.onBootstrap` and `sandboxConfig.bootstrapHash` must be provided together."
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
if (settings.workDir != null) {
|
|
2217
|
+
normalizeSandboxWorkDir(settings.workDir);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
function normalizeSandboxWorkDir(workDir) {
|
|
2221
|
+
if (workDir.length === 0) {
|
|
2222
|
+
throw new Error("HarnessAgent: `sandboxConfig.workDir` must not be empty.");
|
|
2223
|
+
}
|
|
2224
|
+
if (workDir.includes("\0")) {
|
|
2225
|
+
throw new Error(
|
|
2226
|
+
"HarnessAgent: `sandboxConfig.workDir` must not contain NUL."
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
if (workDir.includes("\\")) {
|
|
2230
|
+
throw new Error(
|
|
2231
|
+
"HarnessAgent: `sandboxConfig.workDir` must use POSIX path separators."
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
if (posix.isAbsolute(workDir)) {
|
|
2235
|
+
throw new Error("HarnessAgent: `sandboxConfig.workDir` must be relative.");
|
|
2236
|
+
}
|
|
2237
|
+
const normalized = posix.normalize(workDir);
|
|
2238
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
2239
|
+
throw new Error(
|
|
2240
|
+
"HarnessAgent: `sandboxConfig.workDir` must stay inside the sandbox default working directory."
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
return normalized;
|
|
2244
|
+
}
|
|
2245
|
+
function resolveSessionWorkDir({
|
|
2246
|
+
defaultWorkingDirectory,
|
|
2247
|
+
harnessId,
|
|
2248
|
+
sessionId,
|
|
2249
|
+
workDir
|
|
2250
|
+
}) {
|
|
2251
|
+
return joinSandboxPath({
|
|
2252
|
+
base: defaultWorkingDirectory,
|
|
2253
|
+
path: workDir != null ? workDir : `${harnessId}-${sessionId}`
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
async function createSandboxBootstrapPlan({
|
|
2257
|
+
recipe,
|
|
2258
|
+
settings
|
|
2259
|
+
}) {
|
|
2260
|
+
const workDir = settings.workDir == null ? void 0 : normalizeSandboxWorkDir(settings.workDir);
|
|
2261
|
+
const recipeIdentity = recipe == null ? void 0 : await hashHarnessBootstrap(recipe);
|
|
2262
|
+
const hasCallerBootstrap = settings.onBootstrap != null;
|
|
2263
|
+
const needsCombinedIdentity = hasCallerBootstrap || recipeIdentity != null && workDir != null;
|
|
2264
|
+
const identity = needsCombinedIdentity && (recipeIdentity != null || hasCallerBootstrap) ? await hashSandboxBootstrapIdentity({
|
|
2265
|
+
recipeIdentity,
|
|
2266
|
+
bootstrapHash: settings.bootstrapHash,
|
|
2267
|
+
workDir
|
|
2268
|
+
}) : recipeIdentity;
|
|
2269
|
+
return {
|
|
2270
|
+
...recipe != null ? { recipe } : {},
|
|
2271
|
+
...recipeIdentity != null ? { recipeIdentity } : {},
|
|
2272
|
+
...identity != null ? { identity } : {},
|
|
2273
|
+
...workDir != null ? { workDir } : {},
|
|
2274
|
+
...recipe != null || settings.onBootstrap != null ? {
|
|
2275
|
+
onFirstCreate: (session, opts) => runSandboxBootstrap({
|
|
2276
|
+
session,
|
|
2277
|
+
recipe,
|
|
2278
|
+
recipeIdentity,
|
|
2279
|
+
workDir,
|
|
2280
|
+
onBootstrap: settings.onBootstrap,
|
|
2281
|
+
abortSignal: opts.abortSignal
|
|
2282
|
+
})
|
|
2283
|
+
} : {}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
async function runSandboxBootstrap({
|
|
2287
|
+
session,
|
|
2288
|
+
recipe,
|
|
2289
|
+
recipeIdentity,
|
|
2290
|
+
workDir,
|
|
2291
|
+
onBootstrap,
|
|
2292
|
+
abortSignal
|
|
2293
|
+
}) {
|
|
2294
|
+
if (recipe != null && recipeIdentity != null) {
|
|
2295
|
+
await applyBootstrapRecipe(session, recipe, recipeIdentity, {
|
|
2296
|
+
abortSignal
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
if (onBootstrap == null) return;
|
|
2300
|
+
const defaultWorkingDirectory = await resolveDefaultWorkingDirectory({
|
|
2301
|
+
session,
|
|
2302
|
+
abortSignal
|
|
2303
|
+
});
|
|
2304
|
+
const bootstrapWorkDir = workDir == null ? defaultWorkingDirectory : joinSandboxPath({
|
|
2305
|
+
base: defaultWorkingDirectory,
|
|
2306
|
+
path: workDir
|
|
2307
|
+
});
|
|
2308
|
+
await ensureSandboxDirectory({
|
|
2309
|
+
session,
|
|
2310
|
+
workDir: bootstrapWorkDir,
|
|
2311
|
+
abortSignal
|
|
2312
|
+
});
|
|
2313
|
+
await onBootstrap({ session, workDir: bootstrapWorkDir, abortSignal });
|
|
2314
|
+
}
|
|
2315
|
+
async function resolveDefaultWorkingDirectory({
|
|
2316
|
+
session,
|
|
2317
|
+
abortSignal
|
|
2318
|
+
}) {
|
|
2319
|
+
const result = await session.run({
|
|
2320
|
+
command: "pwd",
|
|
2321
|
+
abortSignal
|
|
2322
|
+
});
|
|
2323
|
+
if (result.exitCode !== 0) {
|
|
2324
|
+
throw new Error(
|
|
2325
|
+
`Failed to resolve sandbox default working directory (exit ${result.exitCode}): ${result.stderr || result.stdout}`
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
const cwd = result.stdout.trim();
|
|
2329
|
+
if (!posix.isAbsolute(cwd)) {
|
|
2330
|
+
throw new Error(
|
|
2331
|
+
`Failed to resolve sandbox default working directory: expected an absolute path, got ${JSON.stringify(cwd)}.`
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
return cwd === "/" ? cwd : cwd.replace(/\/+$/, "");
|
|
2335
|
+
}
|
|
2336
|
+
async function ensureSandboxDirectory({
|
|
2337
|
+
session,
|
|
2338
|
+
workDir,
|
|
2339
|
+
abortSignal
|
|
2340
|
+
}) {
|
|
2341
|
+
const result = await session.run({
|
|
2342
|
+
command: 'mkdir -p "$WORK_DIR"',
|
|
2343
|
+
env: { WORK_DIR: workDir },
|
|
2344
|
+
abortSignal
|
|
2345
|
+
});
|
|
2346
|
+
if (result.exitCode !== 0) {
|
|
2347
|
+
throw new Error(
|
|
2348
|
+
`Failed to create sandbox work directory ${workDir} (exit ${result.exitCode}): ${result.stderr || result.stdout}`
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
async function hashSandboxBootstrapIdentity({
|
|
2353
|
+
recipeIdentity,
|
|
2354
|
+
bootstrapHash,
|
|
2355
|
+
workDir
|
|
2356
|
+
}) {
|
|
2357
|
+
const encoder = new TextEncoder();
|
|
2358
|
+
const chunks = [];
|
|
2359
|
+
const pushString = (value) => {
|
|
2360
|
+
chunks.push(encoder.encode(value));
|
|
2361
|
+
chunks.push(encoder.encode("\0"));
|
|
2362
|
+
};
|
|
2363
|
+
pushString(String(SANDBOX_BOOTSTRAP_IDENTITY_VERSION));
|
|
2364
|
+
pushString(recipeIdentity != null ? recipeIdentity : "");
|
|
2365
|
+
pushString(bootstrapHash != null ? bootstrapHash : "");
|
|
2366
|
+
pushString(workDir != null ? workDir : "");
|
|
2367
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
2368
|
+
const buffer = new Uint8Array(totalLength);
|
|
2369
|
+
let offset = 0;
|
|
2370
|
+
for (const chunk of chunks) {
|
|
2371
|
+
buffer.set(chunk, offset);
|
|
2372
|
+
offset += chunk.length;
|
|
2373
|
+
}
|
|
2374
|
+
const digest = await crypto.subtle.digest("SHA-256", buffer);
|
|
2375
|
+
const bytes = new Uint8Array(digest);
|
|
2376
|
+
let hex = "";
|
|
2377
|
+
for (let i = 0; i < 8; i++) {
|
|
2378
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
2379
|
+
}
|
|
2380
|
+
return hex;
|
|
2381
|
+
}
|
|
2382
|
+
function joinSandboxPath({
|
|
2383
|
+
base,
|
|
2384
|
+
path
|
|
2385
|
+
}) {
|
|
2386
|
+
return posix.join(base, path);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2193
2389
|
// src/agent/internal/resolve-observability.ts
|
|
2194
2390
|
import { asArray } from "@ai-sdk/provider-utils";
|
|
2195
2391
|
var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
@@ -2267,7 +2463,10 @@ var HarnessAgent = class {
|
|
|
2267
2463
|
constructor(settings) {
|
|
2268
2464
|
this.version = "agent-v1";
|
|
2269
2465
|
var _a3;
|
|
2466
|
+
const sandboxConfig = resolveSandboxConfig(settings);
|
|
2467
|
+
validateSandboxBootstrapSettings(sandboxConfig);
|
|
2270
2468
|
this.settings = settings;
|
|
2469
|
+
this.sandboxConfig = sandboxConfig;
|
|
2271
2470
|
this.id = settings.id;
|
|
2272
2471
|
this.userTools = (_a3 = settings.tools) != null ? _a3 : {};
|
|
2273
2472
|
this.permissionMode = resolvePermissionMode({
|
|
@@ -2329,17 +2528,18 @@ var HarnessAgent = class {
|
|
|
2329
2528
|
const effectiveContinueFrom = validatedContinueFrom != null ? validatedContinueFrom : validatedResumeFrom == null ? void 0 : validatedResumeFrom.continueFrom;
|
|
2330
2529
|
const isResumedSession = validatedResumeFrom != null || effectiveContinueFrom != null;
|
|
2331
2530
|
let recipe;
|
|
2332
|
-
let identity;
|
|
2333
2531
|
if (harness.getBootstrap != null) {
|
|
2334
2532
|
recipe = await harness.getBootstrap({ abortSignal });
|
|
2335
|
-
identity = await hashBootstrap(recipe);
|
|
2336
2533
|
}
|
|
2534
|
+
const sandboxBootstrapPlan = await createSandboxBootstrapPlan({
|
|
2535
|
+
recipe,
|
|
2536
|
+
settings: this.sandboxConfig
|
|
2537
|
+
});
|
|
2337
2538
|
const acquiredSandboxSession = await this._acquireSandbox({
|
|
2338
2539
|
sandboxProvider,
|
|
2339
2540
|
sessionId,
|
|
2340
2541
|
isResume: isResumedSession,
|
|
2341
|
-
|
|
2342
|
-
identity,
|
|
2542
|
+
bootstrapPlan: sandboxBootstrapPlan,
|
|
2343
2543
|
abortSignal
|
|
2344
2544
|
});
|
|
2345
2545
|
const leased = applyPortLease({
|
|
@@ -2349,22 +2549,28 @@ var HarnessAgent = class {
|
|
|
2349
2549
|
});
|
|
2350
2550
|
const sandboxSession = leased.sandboxSession;
|
|
2351
2551
|
const leasedBridgePort = leased.port;
|
|
2352
|
-
const sessionWorkDir =
|
|
2552
|
+
const sessionWorkDir = resolveSessionWorkDir({
|
|
2553
|
+
defaultWorkingDirectory: sandboxSession.defaultWorkingDirectory,
|
|
2554
|
+
harnessId: harness.harnessId,
|
|
2555
|
+
sessionId,
|
|
2556
|
+
workDir: sandboxBootstrapPlan.workDir
|
|
2557
|
+
});
|
|
2353
2558
|
try {
|
|
2354
|
-
if (!isResumedSession && recipe != null &&
|
|
2559
|
+
if (!isResumedSession && sandboxBootstrapPlan.recipe != null && sandboxBootstrapPlan.recipeIdentity != null) {
|
|
2355
2560
|
await applyBootstrapRecipe(
|
|
2356
2561
|
sandboxSession.restricted(),
|
|
2357
|
-
recipe,
|
|
2358
|
-
|
|
2562
|
+
sandboxBootstrapPlan.recipe,
|
|
2563
|
+
sandboxBootstrapPlan.recipeIdentity,
|
|
2359
2564
|
{ abortSignal }
|
|
2360
2565
|
);
|
|
2361
2566
|
}
|
|
2362
|
-
await
|
|
2363
|
-
|
|
2567
|
+
await ensureSandboxDirectory({
|
|
2568
|
+
session: sandboxSession,
|
|
2569
|
+
workDir: sessionWorkDir,
|
|
2364
2570
|
abortSignal
|
|
2365
2571
|
});
|
|
2366
|
-
if (this.
|
|
2367
|
-
await this.
|
|
2572
|
+
if (this.sandboxConfig.onSession != null) {
|
|
2573
|
+
await this.sandboxConfig.onSession({
|
|
2368
2574
|
session: sandboxSession.restricted(),
|
|
2369
2575
|
sessionWorkDir,
|
|
2370
2576
|
abortSignal
|
|
@@ -2521,13 +2727,8 @@ var HarnessAgent = class {
|
|
|
2521
2727
|
return sandboxProvider.createSession({
|
|
2522
2728
|
sessionId: input.sessionId,
|
|
2523
2729
|
abortSignal: input.abortSignal,
|
|
2524
|
-
identity: input.identity,
|
|
2525
|
-
onFirstCreate: input.
|
|
2526
|
-
session,
|
|
2527
|
-
input.recipe,
|
|
2528
|
-
input.identity,
|
|
2529
|
-
opts
|
|
2530
|
-
) : void 0
|
|
2730
|
+
identity: input.bootstrapPlan.identity,
|
|
2731
|
+
onFirstCreate: input.bootstrapPlan.onFirstCreate
|
|
2531
2732
|
});
|
|
2532
2733
|
}
|
|
2533
2734
|
/*
|
|
@@ -2668,6 +2869,18 @@ var HarnessGenerateTextResult = class {
|
|
|
2668
2869
|
return this.finalStep.providerMetadata;
|
|
2669
2870
|
}
|
|
2670
2871
|
};
|
|
2872
|
+
function resolveSandboxConfig(settings) {
|
|
2873
|
+
var _a3;
|
|
2874
|
+
if (settings.onSandboxSession != null) {
|
|
2875
|
+
console.warn(
|
|
2876
|
+
"HarnessAgent: `onSandboxSession` is deprecated. Use `sandboxConfig.onSession` instead."
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
return {
|
|
2880
|
+
...settings.sandboxConfig,
|
|
2881
|
+
...((_a3 = settings.sandboxConfig) == null ? void 0 : _a3.onSession) == null && settings.onSandboxSession != null ? { onSession: settings.onSandboxSession } : {}
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2671
2884
|
function applyPortLease(input) {
|
|
2672
2885
|
const pool = input.provider.bridgePorts;
|
|
2673
2886
|
if (pool == null || pool.length === 0) {
|
|
@@ -2704,21 +2917,35 @@ async function cleanupAfterStartFailure(input) {
|
|
|
2704
2917
|
|
|
2705
2918
|
// src/agent/prewarm.ts
|
|
2706
2919
|
async function prewarmHarness(options) {
|
|
2707
|
-
var _a3, _b3;
|
|
2708
|
-
const
|
|
2920
|
+
var _a3, _b3, _c;
|
|
2921
|
+
const sandboxConfig = (_a3 = options.sandboxConfig) != null ? _a3 : {};
|
|
2922
|
+
validateSandboxBootstrapSettings(sandboxConfig);
|
|
2923
|
+
const recipe = await ((_c = (_b3 = options.harness).getBootstrap) == null ? void 0 : _c.call(_b3, {
|
|
2709
2924
|
abortSignal: options.abortSignal
|
|
2710
2925
|
}));
|
|
2711
|
-
|
|
2712
|
-
|
|
2926
|
+
const bootstrapPlan = await createSandboxBootstrapPlan({
|
|
2927
|
+
recipe,
|
|
2928
|
+
settings: sandboxConfig
|
|
2929
|
+
});
|
|
2930
|
+
if (bootstrapPlan.identity == null || bootstrapPlan.onFirstCreate == null) {
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2713
2933
|
const sandboxSession = await options.sandboxProvider.createSession({
|
|
2714
2934
|
abortSignal: options.abortSignal,
|
|
2715
|
-
identity,
|
|
2716
|
-
onFirstCreate:
|
|
2935
|
+
identity: bootstrapPlan.identity,
|
|
2936
|
+
onFirstCreate: bootstrapPlan.onFirstCreate
|
|
2717
2937
|
});
|
|
2718
2938
|
try {
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2939
|
+
if (bootstrapPlan.recipe != null && bootstrapPlan.recipeIdentity != null) {
|
|
2940
|
+
await applyBootstrapRecipe(
|
|
2941
|
+
sandboxSession.restricted(),
|
|
2942
|
+
bootstrapPlan.recipe,
|
|
2943
|
+
bootstrapPlan.recipeIdentity,
|
|
2944
|
+
{
|
|
2945
|
+
abortSignal: options.abortSignal
|
|
2946
|
+
}
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2722
2949
|
} finally {
|
|
2723
2950
|
await Promise.resolve(sandboxSession.stop()).catch(() => {
|
|
2724
2951
|
});
|