@ai-sdk/harness 1.0.70 → 1.0.72
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 +21 -0
- package/dist/agent/index.d.ts +90 -9
- package/dist/agent/index.js +69 -44
- package/dist/agent/index.js.map +1 -1
- package/dist/index.d.ts +88 -10
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +59 -7
- package/dist/utils/index.js +36 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +3 -3
- package/src/agent/internal/run-prompt.ts +78 -40
- package/src/agent/prepare-sandbox-for-harness.ts +8 -18
- package/src/errors/harness-capability-unsupported-error.ts +2 -2
- package/src/utils/index.ts +5 -0
- package/src/utils/sandbox-credential-brokering.ts +41 -0
- package/src/v1/harness-v1-network-sandbox-session.ts +87 -5
- package/src/v1/harness-v1-session.ts +4 -2
- package/src/v1/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @ai-sdk/harness
|
|
2
2
|
|
|
3
|
+
## 1.0.72
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 69bb613: feat(harness): support request transformations in network sandbox abstraction and use it to apply credential brokering when available
|
|
8
|
+
- 52bc889: feat(harness): add `getPortEndpoint()` as more comprehensive replacement to `getPortUrl()` (now deprecated) in `HarnessV1NetworkSandboxSession`
|
|
9
|
+
- 4cd4989: chore(harness): decouple bridge based harness bootstrap recipe logic from dynamic params and enforce unique harnesses in `prepareSandboxForHarness()` helper
|
|
10
|
+
- Updated dependencies [0782259]
|
|
11
|
+
- Updated dependencies [2fd1214]
|
|
12
|
+
- ai@7.0.66
|
|
13
|
+
|
|
14
|
+
## 1.0.71
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 8d717b3: Execute independent host tool calls concurrently within a harness step.
|
|
19
|
+
- Updated dependencies [dc8caae]
|
|
20
|
+
- Updated dependencies [72ec74f]
|
|
21
|
+
- Updated dependencies [c5b0515]
|
|
22
|
+
- ai@7.0.65
|
|
23
|
+
|
|
3
24
|
## 1.0.70
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -117,6 +117,14 @@ interface HarnessV1Bootstrap {
|
|
|
117
117
|
readonly commands: ReadonlyArray<HarnessV1BootstrapCommand>;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Connection details for a sandbox-exposed port. Headers are scoped to the
|
|
122
|
+
* returned URL and must be included when opening the connection.
|
|
123
|
+
*/
|
|
124
|
+
type HarnessV1PortEndpoint = {
|
|
125
|
+
readonly url: string;
|
|
126
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
127
|
+
};
|
|
120
128
|
/**
|
|
121
129
|
* Network sandbox session returned by `HarnessV1SandboxProvider.createSession()`. The
|
|
122
130
|
* harness keeps this for the lifetime of a session. It is itself a
|
|
@@ -125,8 +133,8 @@ interface HarnessV1Bootstrap {
|
|
|
125
133
|
*
|
|
126
134
|
* Code that should only touch the filesystem and spawn processes receives the
|
|
127
135
|
* reduced view from {@link HarnessV1NetworkSandboxSession.restricted}, never the
|
|
128
|
-
* network sandbox session itself — so it cannot stop the sandbox
|
|
129
|
-
* network
|
|
136
|
+
* network sandbox session itself — so it cannot stop the sandbox, change
|
|
137
|
+
* network access, or transform requests.
|
|
130
138
|
*/
|
|
131
139
|
interface HarnessV1NetworkSandboxSession extends Experimental_SandboxSession {
|
|
132
140
|
/**
|
|
@@ -151,12 +159,21 @@ interface HarnessV1NetworkSandboxSession extends Experimental_SandboxSession {
|
|
|
151
159
|
* not bake a provider-specific base into their own paths.
|
|
152
160
|
*/
|
|
153
161
|
readonly defaultWorkingDirectory: string;
|
|
154
|
-
/** Ports the sandbox exposes; resolvable
|
|
162
|
+
/** Ports the sandbox exposes; resolvable via `getPortEndpoint`. */
|
|
155
163
|
readonly ports: ReadonlyArray<number>;
|
|
156
164
|
/**
|
|
157
|
-
* Resolve
|
|
165
|
+
* Resolve the connection details for a sandbox-exposed port. Bridge-backed
|
|
158
166
|
* adapters call this to open their WebSocket to the in-sandbox bridge.
|
|
159
167
|
*/
|
|
168
|
+
readonly getPortEndpoint: (options: {
|
|
169
|
+
port: number;
|
|
170
|
+
protocol?: 'http' | 'https' | 'ws';
|
|
171
|
+
}) => PromiseLike<HarnessV1PortEndpoint>;
|
|
172
|
+
/**
|
|
173
|
+
* Resolve a publicly-reachable URL for a sandbox-exposed port.
|
|
174
|
+
*
|
|
175
|
+
* @deprecated Use `getPortEndpoint` instead.
|
|
176
|
+
*/
|
|
160
177
|
readonly getPortUrl: (options: {
|
|
161
178
|
port: number;
|
|
162
179
|
protocol?: 'http' | 'https' | 'ws';
|
|
@@ -176,6 +193,23 @@ interface HarnessV1NetworkSandboxSession extends Experimental_SandboxSession {
|
|
|
176
193
|
* missing implementation is a no-op.
|
|
177
194
|
*/
|
|
178
195
|
readonly setNetworkPolicy?: (policy: HarnessV1NetworkPolicy) => PromiseLike<void>;
|
|
196
|
+
/**
|
|
197
|
+
* Replace the sandbox's outbound request-transformation rules. Optional —
|
|
198
|
+
* implementations expose this only when credentials can be injected outside
|
|
199
|
+
* the sandbox security boundary. Calling this method assumes authority over
|
|
200
|
+
* the complete transformation set; harness adapters should normally use
|
|
201
|
+
* `addRequestTransformations` instead. Adapters may preserve legacy
|
|
202
|
+
* credential-forwarding behavior when additive request transformations are
|
|
203
|
+
* unavailable.
|
|
204
|
+
*/
|
|
205
|
+
readonly setRequestTransformations?: (transformations: ReadonlyArray<HarnessV1RequestTransformation>) => PromiseLike<void>;
|
|
206
|
+
/**
|
|
207
|
+
* Add outbound request-transformation rules without replacing rules already
|
|
208
|
+
* managed by the sandbox session. Optional for the same reason as
|
|
209
|
+
* `setRequestTransformations`. Harness adapters should use this additive
|
|
210
|
+
* capability unless they explicitly own the complete transformation set.
|
|
211
|
+
*/
|
|
212
|
+
readonly addRequestTransformations?: (transformations: ReadonlyArray<HarnessV1RequestTransformation>) => PromiseLike<void>;
|
|
179
213
|
/**
|
|
180
214
|
* Replace the set of ports exposed by the sandbox. Full-replacement
|
|
181
215
|
* semantics: ports omitted from the array are deregistered. Optional —
|
|
@@ -192,7 +226,8 @@ interface HarnessV1NetworkSandboxSession extends Experimental_SandboxSession {
|
|
|
192
226
|
*
|
|
193
227
|
* The returned object points at exactly the same underlying sandbox
|
|
194
228
|
* resource as the network sandbox session it was produced from; it is only a
|
|
195
|
-
* narrower surface over the same resource, not a separate sandbox.
|
|
229
|
+
* narrower surface over the same resource, not a separate sandbox. In
|
|
230
|
+
* particular, it cannot mutate network access or request transformations.
|
|
196
231
|
*/
|
|
197
232
|
readonly restricted: () => Experimental_SandboxSession;
|
|
198
233
|
}
|
|
@@ -226,6 +261,47 @@ type HarnessV1NetworkPolicy = {
|
|
|
226
261
|
allowedCIDRs: ReadonlyArray<string>;
|
|
227
262
|
deniedCIDRs?: ReadonlyArray<string>;
|
|
228
263
|
};
|
|
264
|
+
type HarnessV1RequestTransformationPathMatcher = {
|
|
265
|
+
exact: string;
|
|
266
|
+
} | {
|
|
267
|
+
startsWith: string;
|
|
268
|
+
} | {
|
|
269
|
+
regex: string;
|
|
270
|
+
};
|
|
271
|
+
type HarnessV1RequestTransformationKeyValuePartMatcher = {
|
|
272
|
+
exact: string;
|
|
273
|
+
} | {
|
|
274
|
+
startsWith: string;
|
|
275
|
+
} | {
|
|
276
|
+
regex: string;
|
|
277
|
+
};
|
|
278
|
+
type HarnessV1RequestTransformationKeyValueMatcher = {
|
|
279
|
+
readonly key?: HarnessV1RequestTransformationKeyValuePartMatcher;
|
|
280
|
+
readonly value?: HarnessV1RequestTransformationKeyValuePartMatcher;
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* Outbound HTTPS request transformation applied outside the sandbox security
|
|
284
|
+
* boundary. The host is part of the match so each rule is self-contained and
|
|
285
|
+
* several rules, including several for the same host, can be installed at
|
|
286
|
+
* once.
|
|
287
|
+
*
|
|
288
|
+
* Credential values belong in `transform.headers`, while the sandbox process
|
|
289
|
+
* receives only a non-secret placeholder. Implementations must overwrite
|
|
290
|
+
* matching request headers after the request leaves the sandbox rather than
|
|
291
|
+
* making transformed values available inside it.
|
|
292
|
+
*/
|
|
293
|
+
type HarnessV1RequestTransformation = {
|
|
294
|
+
readonly match: {
|
|
295
|
+
readonly host: string;
|
|
296
|
+
readonly path?: HarnessV1RequestTransformationPathMatcher;
|
|
297
|
+
readonly method?: ReadonlyArray<string>;
|
|
298
|
+
readonly queryString?: ReadonlyArray<HarnessV1RequestTransformationKeyValueMatcher>;
|
|
299
|
+
readonly headers?: ReadonlyArray<HarnessV1RequestTransformationKeyValueMatcher>;
|
|
300
|
+
};
|
|
301
|
+
readonly transform: {
|
|
302
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
303
|
+
};
|
|
304
|
+
};
|
|
229
305
|
|
|
230
306
|
/** Severity of a diagnostic, ordered most → least severe. */
|
|
231
307
|
declare const harnessV1DebugLevelSchema: z.ZodEnum<{
|
|
@@ -664,8 +740,10 @@ type HarnessV1StartOptions = {
|
|
|
664
740
|
* Network sandbox session the adapter operates against. It is owned and
|
|
665
741
|
* lifecycled by `HarnessAgent`. Adapters call `restricted()` for the
|
|
666
742
|
* tool-safe filesystem/exec/spawn surface, and use the infra methods
|
|
667
|
-
* (`
|
|
668
|
-
*
|
|
743
|
+
* (`getPortEndpoint`, `ports`, `setNetworkPolicy`,
|
|
744
|
+
* `setRequestTransformations`, `addRequestTransformations`) for bridge
|
|
745
|
+
* wiring. Adapters must not call `stop()` themselves; the agent does that
|
|
746
|
+
* during cleanup.
|
|
669
747
|
*/
|
|
670
748
|
readonly sandboxSession: HarnessV1NetworkSandboxSession;
|
|
671
749
|
/**
|
|
@@ -1606,6 +1684,9 @@ type PrepareSandboxForHarnessResult = {
|
|
|
1606
1684
|
* When a later `HarnessAgent` session uses a sandbox created from the persisted
|
|
1607
1685
|
* artifact, the adapter recomputes the same recipe identity and the existing
|
|
1608
1686
|
* bootstrap marker makes the bootstrap logic a no-op.
|
|
1687
|
+
*
|
|
1688
|
+
* Repeated harness IDs are prepared once. When multiple adapters use the same
|
|
1689
|
+
* ID, the last adapter in `harnesses` is used.
|
|
1609
1690
|
*/
|
|
1610
1691
|
declare function prepareSandboxForHarness(options: {
|
|
1611
1692
|
readonly session: Experimental_SandboxSession;
|
|
@@ -1633,8 +1714,8 @@ declare const symbol$1: unique symbol;
|
|
|
1633
1714
|
/**
|
|
1634
1715
|
* Thrown when a caller asks the harness to do something the adapter (or the
|
|
1635
1716
|
* supplied sandbox) does not support, e.g. requesting manual compaction from
|
|
1636
|
-
* an adapter that only auto-compacts, or invoking `
|
|
1637
|
-
* that does not expose one.
|
|
1717
|
+
* an adapter that only auto-compacts, or invoking `getPortEndpoint` on a
|
|
1718
|
+
* sandbox that does not expose one.
|
|
1638
1719
|
*
|
|
1639
1720
|
* The caller supplies the full human-readable message. Optional `harnessId`
|
|
1640
1721
|
* is recorded as structured context for tooling.
|
package/dist/agent/index.js
CHANGED
|
@@ -1476,6 +1476,23 @@ function runPrompt(input) {
|
|
|
1476
1476
|
let pendingStopBoundary;
|
|
1477
1477
|
let finalFinish;
|
|
1478
1478
|
const completedSteps = [];
|
|
1479
|
+
const outstandingHostToolExecutions = [];
|
|
1480
|
+
const startHostToolExecution = (execution) => {
|
|
1481
|
+
outstandingHostToolExecutions.push(execution);
|
|
1482
|
+
void execution.catch(() => {
|
|
1483
|
+
});
|
|
1484
|
+
};
|
|
1485
|
+
const waitForOutstandingHostToolExecutions = async () => {
|
|
1486
|
+
if (outstandingHostToolExecutions.length === 0) return;
|
|
1487
|
+
const executions = outstandingHostToolExecutions.splice(0);
|
|
1488
|
+
const results = await Promise.allSettled(executions);
|
|
1489
|
+
const failedExecution = results.find(
|
|
1490
|
+
(result2) => result2.status === "rejected"
|
|
1491
|
+
);
|
|
1492
|
+
if (failedExecution != null) {
|
|
1493
|
+
throw failedExecution.reason;
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1479
1496
|
const releasePendingStopBoundary = () => {
|
|
1480
1497
|
var _a6;
|
|
1481
1498
|
(_a6 = pendingStopBoundary == null ? void 0 : pendingStopBoundary.releaseCheckpoint) == null ? void 0 : _a6.call(pendingStopBoundary);
|
|
@@ -1531,6 +1548,7 @@ function runPrompt(input) {
|
|
|
1531
1548
|
return step;
|
|
1532
1549
|
};
|
|
1533
1550
|
const finishForHostInputPause = async (options) => {
|
|
1551
|
+
await waitForOutstandingHostToolExecutions();
|
|
1534
1552
|
if (options.completeCurrentStep) {
|
|
1535
1553
|
await completeStep({
|
|
1536
1554
|
finishReason: toolCallsFinishReason,
|
|
@@ -1799,6 +1817,7 @@ function runPrompt(input) {
|
|
|
1799
1817
|
}
|
|
1800
1818
|
}
|
|
1801
1819
|
if (value.type === "error" && displayValue.type === "error") {
|
|
1820
|
+
await waitForOutstandingHostToolExecutions();
|
|
1802
1821
|
await telemetry.error(value.error);
|
|
1803
1822
|
logBridgeError({
|
|
1804
1823
|
harnessId: input.harness.harnessId,
|
|
@@ -1892,6 +1911,7 @@ function runPrompt(input) {
|
|
|
1892
1911
|
return;
|
|
1893
1912
|
}
|
|
1894
1913
|
if (value.type === "finish-step") {
|
|
1914
|
+
await waitForOutstandingHostToolExecutions();
|
|
1895
1915
|
await completeStep({
|
|
1896
1916
|
finishReason: value.finishReason,
|
|
1897
1917
|
usage: value.usage,
|
|
@@ -1906,6 +1926,7 @@ function runPrompt(input) {
|
|
|
1906
1926
|
}
|
|
1907
1927
|
}
|
|
1908
1928
|
if (value.type === "finish") {
|
|
1929
|
+
await waitForOutstandingHostToolExecutions();
|
|
1909
1930
|
finalFinish = value;
|
|
1910
1931
|
await telemetry.end({
|
|
1911
1932
|
finishReason: value.finishReason,
|
|
@@ -2009,41 +2030,51 @@ function runPrompt(input) {
|
|
|
2009
2030
|
await finishForHostInputPause({ completeCurrentStep: true });
|
|
2010
2031
|
return;
|
|
2011
2032
|
}
|
|
2012
|
-
|
|
2013
|
-
event: toolCall,
|
|
2014
|
-
tools: activeTools,
|
|
2015
|
-
wrappedExecuteTool: telemetry.executeTool,
|
|
2016
|
-
sandboxSession: input.sandboxSession,
|
|
2017
|
-
abortSignal: input.abortSignal,
|
|
2018
|
-
control,
|
|
2019
|
-
onPreliminaryResult: (preliminaryOutput) => {
|
|
2020
|
-
const stripped = stripWorkDir(
|
|
2021
|
-
{
|
|
2022
|
-
type: "tool-result",
|
|
2023
|
-
toolCallId: toolCall.toolCallId,
|
|
2024
|
-
toolName: toolCall.toolName,
|
|
2025
|
-
result: preliminaryOutput
|
|
2026
|
-
},
|
|
2027
|
-
input.sessionWorkDir
|
|
2028
|
-
);
|
|
2029
|
-
result.enqueue({
|
|
2030
|
-
type: "tool-result",
|
|
2031
|
-
toolCallId: toolCall.toolCallId,
|
|
2032
|
-
toolName: toolCall.toolName,
|
|
2033
|
-
input: void 0,
|
|
2034
|
-
output: stripped.result,
|
|
2035
|
-
preliminary: true
|
|
2036
|
-
});
|
|
2037
|
-
}
|
|
2038
|
-
});
|
|
2039
|
-
if (!execution.executed) {
|
|
2033
|
+
if (!isExecutableTool(activeTools[toolCall.toolName])) {
|
|
2040
2034
|
recordPendingToolResult({ toolCall });
|
|
2041
2035
|
await finishForHostInputPause({ completeCurrentStep: true });
|
|
2042
2036
|
return;
|
|
2043
2037
|
}
|
|
2044
|
-
|
|
2038
|
+
startHostToolExecution(
|
|
2039
|
+
(async () => {
|
|
2040
|
+
const execution = await maybeExecuteHostTool({
|
|
2041
|
+
event: toolCall,
|
|
2042
|
+
tools: activeTools,
|
|
2043
|
+
wrappedExecuteTool: telemetry.executeTool,
|
|
2044
|
+
sandboxSession: input.sandboxSession,
|
|
2045
|
+
abortSignal: input.abortSignal,
|
|
2046
|
+
control,
|
|
2047
|
+
onPreliminaryResult: (preliminaryOutput) => {
|
|
2048
|
+
const stripped = stripWorkDir(
|
|
2049
|
+
{
|
|
2050
|
+
type: "tool-result",
|
|
2051
|
+
toolCallId: toolCall.toolCallId,
|
|
2052
|
+
toolName: toolCall.toolName,
|
|
2053
|
+
result: preliminaryOutput
|
|
2054
|
+
},
|
|
2055
|
+
input.sessionWorkDir
|
|
2056
|
+
);
|
|
2057
|
+
result.enqueue({
|
|
2058
|
+
type: "tool-result",
|
|
2059
|
+
toolCallId: toolCall.toolCallId,
|
|
2060
|
+
toolName: toolCall.toolName,
|
|
2061
|
+
input: void 0,
|
|
2062
|
+
output: stripped.result,
|
|
2063
|
+
preliminary: true
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
if (!execution.executed) {
|
|
2068
|
+
throw new Error(
|
|
2069
|
+
`Harness '${input.harness.harnessId}' could not execute host tool '${toolCall.toolName}'.`
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
await telemetry.toolEnd(toolCall.toolCallId, execution.outcome);
|
|
2073
|
+
})()
|
|
2074
|
+
);
|
|
2045
2075
|
}
|
|
2046
2076
|
}
|
|
2077
|
+
await waitForOutstandingHostToolExecutions();
|
|
2047
2078
|
const isTurnSuspending = ((_j = input.isTurnSuspending) == null ? void 0 : _j.call(input)) === true;
|
|
2048
2079
|
if (isTurnSuspending) {
|
|
2049
2080
|
if (finalFinish == null) {
|
|
@@ -2062,6 +2093,10 @@ function runPrompt(input) {
|
|
|
2062
2093
|
} : void 0
|
|
2063
2094
|
);
|
|
2064
2095
|
} catch (err) {
|
|
2096
|
+
try {
|
|
2097
|
+
await waitForOutstandingHostToolExecutions();
|
|
2098
|
+
} catch (e) {
|
|
2099
|
+
}
|
|
2065
2100
|
await telemetry.error(err);
|
|
2066
2101
|
logBridgeError({
|
|
2067
2102
|
harnessId: input.harness.harnessId,
|
|
@@ -3656,10 +3691,11 @@ async function prepareSandboxForHarness(options) {
|
|
|
3656
3691
|
"prepareSandboxForHarness: at least one harness must be provided."
|
|
3657
3692
|
);
|
|
3658
3693
|
}
|
|
3659
|
-
const harnesses = [
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3694
|
+
const harnesses = [
|
|
3695
|
+
...new Map(
|
|
3696
|
+
options.harnesses.map((harness) => [harness.harnessId, harness])
|
|
3697
|
+
).values()
|
|
3698
|
+
].sort((a, b) => a.harnessId.localeCompare(b.harnessId));
|
|
3663
3699
|
const workDir = sandboxConfig.workDir == null ? void 0 : normalizeSandboxWorkDir(sandboxConfig.workDir);
|
|
3664
3700
|
const recipeIdentities = {};
|
|
3665
3701
|
const skippedHarnessIds = [];
|
|
@@ -3706,17 +3742,6 @@ async function prepareSandboxForHarness(options) {
|
|
|
3706
3742
|
skippedHarnessIds
|
|
3707
3743
|
};
|
|
3708
3744
|
}
|
|
3709
|
-
function assertUniqueHarnessIds(harnesses) {
|
|
3710
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3711
|
-
for (const harness of harnesses) {
|
|
3712
|
-
if (seen.has(harness.harnessId)) {
|
|
3713
|
-
throw new Error(
|
|
3714
|
-
`prepareSandboxForHarness: duplicate harness id "${harness.harnessId}".`
|
|
3715
|
-
);
|
|
3716
|
-
}
|
|
3717
|
-
seen.add(harness.harnessId);
|
|
3718
|
-
}
|
|
3719
|
-
}
|
|
3720
3745
|
async function resolvePreparedSandboxIdentity({
|
|
3721
3746
|
recipeIdentities,
|
|
3722
3747
|
bootstrapHash,
|