@ai-sdk/harness 0.0.0 → 1.0.0-canary.2
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 +13 -0
- package/LICENSE +13 -0
- package/README.md +15 -0
- package/dist/index.d.ts +220 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -1
- package/src/errors/harness-capability-unsupported-error.ts +41 -0
- package/src/errors/harness-error.ts +22 -0
- package/src/index.ts +7 -0
- package/src/v1/harness-v1-network-sandbox-session.ts +123 -0
- package/src/v1/harness-v1-sandbox-provider.ts +76 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @ai-sdk/harness
|
|
2
|
+
|
|
3
|
+
## 1.0.0-canary.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 0.0.0-canary.1
|
|
10
|
+
|
|
11
|
+
### Major Changes
|
|
12
|
+
|
|
13
|
+
- 9d6dbe0: feat(harness): add sandbox specific expansion for harness abstraction, add `sandbox-just-bash` and `sandbox-vercel`
|
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2023 Vercel, Inc.
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# AI SDK - Harness
|
|
2
|
+
|
|
3
|
+
_This package is **experimental**._
|
|
4
|
+
|
|
5
|
+
Harness abstraction, including an expanded network session sandbox interface to support harness sandbox needs.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @ai-sdk/harness
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
TBD.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { AISDKError } from '@ai-sdk/provider';
|
|
2
|
+
import { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
|
|
3
|
+
|
|
4
|
+
declare const symbol$1: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* Base error type for failures originating in or signalled by a harness
|
|
7
|
+
* adapter. Specific failure modes (e.g. unsupported capability) extend this
|
|
8
|
+
* class.
|
|
9
|
+
*/
|
|
10
|
+
declare class HarnessError extends AISDKError {
|
|
11
|
+
private readonly [symbol$1];
|
|
12
|
+
constructor({ message, cause }: {
|
|
13
|
+
message: string;
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
});
|
|
16
|
+
static isInstance(error: unknown): error is HarnessError;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare const symbol: unique symbol;
|
|
20
|
+
/**
|
|
21
|
+
* Thrown when a caller asks the harness to do something the adapter (or the
|
|
22
|
+
* supplied sandbox) does not support, e.g. requesting manual compaction from
|
|
23
|
+
* an adapter that only auto-compacts, or invoking `getPortUrl` on a sandbox
|
|
24
|
+
* that does not expose one.
|
|
25
|
+
*
|
|
26
|
+
* The caller supplies the full human-readable message. Optional `harnessId`
|
|
27
|
+
* is recorded as structured context for tooling.
|
|
28
|
+
*/
|
|
29
|
+
declare class HarnessCapabilityUnsupportedError extends HarnessError {
|
|
30
|
+
private readonly [symbol];
|
|
31
|
+
readonly harnessId?: string;
|
|
32
|
+
constructor({ message, harnessId, cause, }: {
|
|
33
|
+
message: string;
|
|
34
|
+
harnessId?: string;
|
|
35
|
+
cause?: unknown;
|
|
36
|
+
});
|
|
37
|
+
static isInstance(error: unknown): error is HarnessCapabilityUnsupportedError;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Network sandbox session returned by `HarnessV1SandboxProvider.createSession()`. The
|
|
42
|
+
* harness keeps this for the lifetime of a session. It is itself a
|
|
43
|
+
* {@link SandboxSession} (file I/O, exec, spawn) and adds the infra surface on
|
|
44
|
+
* top: port resolution, lifecycle, and network-policy mutation.
|
|
45
|
+
*
|
|
46
|
+
* Code that should only touch the filesystem and spawn processes receives the
|
|
47
|
+
* reduced view from {@link HarnessV1NetworkSandboxSession.restricted}, never the
|
|
48
|
+
* network sandbox session itself — so it cannot stop the sandbox or change its
|
|
49
|
+
* network policy.
|
|
50
|
+
*/
|
|
51
|
+
interface HarnessV1NetworkSandboxSession extends Experimental_SandboxSession {
|
|
52
|
+
/**
|
|
53
|
+
* Stable identifier for the underlying sandbox resource. Used by the
|
|
54
|
+
* harness session manager as the durable lookup key for cross-process
|
|
55
|
+
* resume — the framework persists this on the resume payload so a future
|
|
56
|
+
* process can call `HarnessV1SandboxProvider.resume?({ sessionId })` and
|
|
57
|
+
* reach the same resource. Providers populate it from their native
|
|
58
|
+
* identifier (Vercel: the sandbox name; just-bash: a UUID minted at
|
|
59
|
+
* create time).
|
|
60
|
+
*/
|
|
61
|
+
readonly id: string;
|
|
62
|
+
/**
|
|
63
|
+
* The sandbox's default working directory — the absolute path that
|
|
64
|
+
* `run`/`spawn` resolve relative commands against when no `workingDirectory`
|
|
65
|
+
* is given. Read from the live sandbox (it is provider-specific and
|
|
66
|
+
* configurable at create time: Vercel defaults to `/vercel/sandbox`,
|
|
67
|
+
* just-bash to `/home/user`), never hardcoded.
|
|
68
|
+
*
|
|
69
|
+
* The framework composes each session's working directory underneath this
|
|
70
|
+
* path (`<defaultWorkingDirectory>/<harnessId>-<sessionId>`) so adapters do
|
|
71
|
+
* not bake a provider-specific base into their own paths.
|
|
72
|
+
*/
|
|
73
|
+
readonly defaultWorkingDirectory: string;
|
|
74
|
+
/** Ports the sandbox exposes; resolvable to public URLs via `getPortUrl`. */
|
|
75
|
+
readonly ports: ReadonlyArray<number>;
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a publicly-reachable URL for a sandbox-exposed port. Bridge-backed
|
|
78
|
+
* adapters call this to open their WebSocket to the in-sandbox bridge.
|
|
79
|
+
*/
|
|
80
|
+
readonly getPortUrl: (options: {
|
|
81
|
+
port: number;
|
|
82
|
+
protocol?: 'http' | 'https' | 'ws';
|
|
83
|
+
}) => PromiseLike<string>;
|
|
84
|
+
/** Stop the sandbox. Idempotent. */
|
|
85
|
+
readonly stop: () => PromiseLike<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Destroy/delete the sandbox resource when supported. Optional because some
|
|
88
|
+
* providers only have a stop/dispose concept. Implementations must handle
|
|
89
|
+
* both a still-running sandbox and a previously stopped sandbox.
|
|
90
|
+
*/
|
|
91
|
+
readonly destroy?: () => PromiseLike<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Update the sandbox's outbound network policy. Optional — implementations
|
|
94
|
+
* without a local enforcement primitive (e.g. just-bash) omit this. Callers
|
|
95
|
+
* use optional-call (`sandboxSession.setNetworkPolicy?.(policy)`); a
|
|
96
|
+
* missing implementation is a no-op.
|
|
97
|
+
*/
|
|
98
|
+
readonly setNetworkPolicy?: (policy: HarnessV1NetworkPolicy) => PromiseLike<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Replace the set of ports exposed by the sandbox. Full-replacement
|
|
101
|
+
* semantics: ports omitted from the array are deregistered. Optional —
|
|
102
|
+
* implementations that cannot expose ports (e.g. just-bash) omit this.
|
|
103
|
+
*/
|
|
104
|
+
readonly setPorts?: (ports: ReadonlyArray<number>, options?: {
|
|
105
|
+
abortSignal?: AbortSignal;
|
|
106
|
+
}) => PromiseLike<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Reduced view of this session, typed as the bare {@link SandboxSession}
|
|
109
|
+
* (file I/O, exec, spawn) — nothing that could stop the sandbox or change
|
|
110
|
+
* its network policy. Pass this to user-tool `execute()` calls and other
|
|
111
|
+
* code that must not reach the infra surface.
|
|
112
|
+
*
|
|
113
|
+
* The returned object points at exactly the same underlying sandbox
|
|
114
|
+
* resource as the network sandbox session it was produced from; it is only a
|
|
115
|
+
* narrower surface over the same resource, not a separate sandbox.
|
|
116
|
+
*/
|
|
117
|
+
readonly restricted: () => Experimental_SandboxSession;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Outbound network policy applied by the sandbox runtime.
|
|
121
|
+
*
|
|
122
|
+
* `'allow-all'` and `'deny-all'` are convenience presets. `'custom'` is an
|
|
123
|
+
* allow-list with an optional CIDR deny-list that takes precedence:
|
|
124
|
+
*
|
|
125
|
+
* - Reachable hosts are the union of `allowedHosts` and `allowedCIDRs`.
|
|
126
|
+
* - `deniedCIDRs` wins over both, useful for blocking cloud-metadata IPs while
|
|
127
|
+
* otherwise allowing broad access.
|
|
128
|
+
*
|
|
129
|
+
* The two `'custom'` branches share the same discriminator but each requires
|
|
130
|
+
* a different allow field. Specifying `'custom'` with only `deniedCIDRs`
|
|
131
|
+
* (deny-only) is rejected at compile time — functionally it would be
|
|
132
|
+
* equivalent to `'deny-all'`.
|
|
133
|
+
*/
|
|
134
|
+
type HarnessV1NetworkPolicy = {
|
|
135
|
+
mode: 'allow-all';
|
|
136
|
+
} | {
|
|
137
|
+
mode: 'deny-all';
|
|
138
|
+
} | {
|
|
139
|
+
mode: 'custom';
|
|
140
|
+
allowedHosts: ReadonlyArray<string>;
|
|
141
|
+
allowedCIDRs?: ReadonlyArray<string>;
|
|
142
|
+
deniedCIDRs?: ReadonlyArray<string>;
|
|
143
|
+
} | {
|
|
144
|
+
mode: 'custom';
|
|
145
|
+
allowedHosts?: ReadonlyArray<string>;
|
|
146
|
+
allowedCIDRs: ReadonlyArray<string>;
|
|
147
|
+
deniedCIDRs?: ReadonlyArray<string>;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Provider that produces network sandbox sessions for harness sessions. Lives at
|
|
152
|
+
* module scope as a stable, synchronous object — analogous to
|
|
153
|
+
* `LanguageModelV4` providers, no I/O performed at construction. The actual
|
|
154
|
+
* sandbox is created (or wrapped) when `HarnessAgent` calls `createSession()`.
|
|
155
|
+
*/
|
|
156
|
+
interface HarnessV1SandboxProvider {
|
|
157
|
+
readonly specificationVersion: 'harness-sandbox-v1';
|
|
158
|
+
readonly providerId: string;
|
|
159
|
+
/**
|
|
160
|
+
* Pool of ports the consumer reserved on a caller-provided sandbox for
|
|
161
|
+
* concurrent harness sessions. The session manager leases one port per
|
|
162
|
+
* session and releases on stop or destroy.
|
|
163
|
+
*
|
|
164
|
+
* Only meaningful when the provider wraps a caller-provided sandbox
|
|
165
|
+
* (the caller pre-declared the ports). In create-new modes the provider
|
|
166
|
+
* mints a fresh sandbox per session, so no leasing is needed; providers
|
|
167
|
+
* leave this undefined.
|
|
168
|
+
*/
|
|
169
|
+
readonly bridgePorts?: ReadonlyArray<number>;
|
|
170
|
+
readonly createSession: (options?: {
|
|
171
|
+
/**
|
|
172
|
+
* Stable per-session identifier. When supplied, the provider names the
|
|
173
|
+
* underlying resource deterministically so a future call to `resume`
|
|
174
|
+
* (potentially from a different process) can find the same sandbox.
|
|
175
|
+
* Omitted from prewarm and other paths that don't need a resumable
|
|
176
|
+
* resource — in that case the provider falls back to its native
|
|
177
|
+
* auto-naming.
|
|
178
|
+
*/
|
|
179
|
+
sessionId?: string;
|
|
180
|
+
abortSignal?: AbortSignal;
|
|
181
|
+
/**
|
|
182
|
+
* Stable identity for snapshot-based reuse. Providers that support
|
|
183
|
+
* persistence/snapshots use this as part of the persistent sandbox
|
|
184
|
+
* name; subsequent calls with the same identity resume from snapshot.
|
|
185
|
+
*
|
|
186
|
+
* Ignored when the provider is wrapping a caller-provided sandbox.
|
|
187
|
+
*/
|
|
188
|
+
identity?: string;
|
|
189
|
+
/**
|
|
190
|
+
* Called exactly once per identity, on fresh creation. Snapshot-capable
|
|
191
|
+
* providers wire this into the platform's one-time-setup hook so the
|
|
192
|
+
* side effects are baked into the snapshot. Providers without snapshot
|
|
193
|
+
* support run it immediately after fresh create.
|
|
194
|
+
*
|
|
195
|
+
* Not called when the provider is wrapping a caller-provided sandbox
|
|
196
|
+
* (the caller owns the sandbox; the framework applies its own
|
|
197
|
+
* idempotent bootstrap post-create instead).
|
|
198
|
+
*/
|
|
199
|
+
onFirstCreate?: (session: Experimental_SandboxSession, opts: {
|
|
200
|
+
abortSignal?: AbortSignal;
|
|
201
|
+
}) => Promise<void>;
|
|
202
|
+
}) => PromiseLike<HarnessV1NetworkSandboxSession>;
|
|
203
|
+
/**
|
|
204
|
+
* Reattach to an existing sandbox previously created with the same
|
|
205
|
+
* `sessionId`. Optional — providers that cannot rehydrate by id (e.g.
|
|
206
|
+
* just-bash) omit this; the harness throws
|
|
207
|
+
* `HarnessCapabilityUnsupportedError` when resume is attempted against
|
|
208
|
+
* them.
|
|
209
|
+
*
|
|
210
|
+
* The provider derives the sandbox identifier from `sessionId` using the
|
|
211
|
+
* same deterministic naming scheme it used in `createSession`. Returns a
|
|
212
|
+
* network sandbox session bound to the existing resource.
|
|
213
|
+
*/
|
|
214
|
+
readonly resumeSession?: (options: {
|
|
215
|
+
sessionId: string;
|
|
216
|
+
abortSignal?: AbortSignal;
|
|
217
|
+
}) => PromiseLike<HarnessV1NetworkSandboxSession>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { HarnessCapabilityUnsupportedError, HarnessError, type HarnessV1NetworkPolicy, type HarnessV1NetworkSandboxSession, type HarnessV1SandboxProvider };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/errors/harness-error.ts
|
|
2
|
+
import { AISDKError } from "@ai-sdk/provider";
|
|
3
|
+
var name = "AI_HarnessError";
|
|
4
|
+
var marker = `vercel.ai.error.${name}`;
|
|
5
|
+
var symbol = Symbol.for(marker);
|
|
6
|
+
var _a, _b;
|
|
7
|
+
var HarnessError = class extends (_b = AISDKError, _a = symbol, _b) {
|
|
8
|
+
constructor({ message, cause }) {
|
|
9
|
+
super({ name, message, cause });
|
|
10
|
+
this[_a] = true;
|
|
11
|
+
}
|
|
12
|
+
static isInstance(error) {
|
|
13
|
+
return AISDKError.hasMarker(error, marker);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/errors/harness-capability-unsupported-error.ts
|
|
18
|
+
import { AISDKError as AISDKError2 } from "@ai-sdk/provider";
|
|
19
|
+
var name2 = "AI_HarnessCapabilityUnsupportedError";
|
|
20
|
+
var marker2 = `vercel.ai.error.${name2}`;
|
|
21
|
+
var symbol2 = Symbol.for(marker2);
|
|
22
|
+
var _a2, _b2;
|
|
23
|
+
var HarnessCapabilityUnsupportedError = class extends (_b2 = HarnessError, _a2 = symbol2, _b2) {
|
|
24
|
+
constructor({
|
|
25
|
+
message,
|
|
26
|
+
harnessId,
|
|
27
|
+
cause
|
|
28
|
+
}) {
|
|
29
|
+
super({ message, cause });
|
|
30
|
+
this[_a2] = true;
|
|
31
|
+
Object.defineProperty(this, "name", { value: name2 });
|
|
32
|
+
this.harnessId = harnessId;
|
|
33
|
+
}
|
|
34
|
+
static isInstance(error) {
|
|
35
|
+
return AISDKError2.hasMarker(error, marker2);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
export {
|
|
39
|
+
HarnessCapabilityUnsupportedError,
|
|
40
|
+
HarnessError
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors/harness-error.ts","../src/errors/harness-capability-unsupported-error.ts"],"sourcesContent":["import { AISDKError } from '@ai-sdk/provider';\n\nconst name = 'AI_HarnessError';\nconst marker = `vercel.ai.error.${name}`;\nconst symbol = Symbol.for(marker);\n\n/**\n * Base error type for failures originating in or signalled by a harness\n * adapter. Specific failure modes (e.g. unsupported capability) extend this\n * class.\n */\nexport class HarnessError extends AISDKError {\n private readonly [symbol] = true;\n\n constructor({ message, cause }: { message: string; cause?: unknown }) {\n super({ name, message, cause });\n }\n\n static isInstance(error: unknown): error is HarnessError {\n return AISDKError.hasMarker(error, marker);\n }\n}\n","import { AISDKError } from '@ai-sdk/provider';\nimport { HarnessError } from './harness-error';\n\nconst name = 'AI_HarnessCapabilityUnsupportedError';\nconst marker = `vercel.ai.error.${name}`;\nconst symbol = Symbol.for(marker);\n\n/**\n * Thrown when a caller asks the harness to do something the adapter (or the\n * supplied sandbox) does not support, e.g. requesting manual compaction from\n * an adapter that only auto-compacts, or invoking `getPortUrl` on a sandbox\n * that does not expose one.\n *\n * The caller supplies the full human-readable message. Optional `harnessId`\n * is recorded as structured context for tooling.\n */\nexport class HarnessCapabilityUnsupportedError extends HarnessError {\n private readonly [symbol] = true;\n\n readonly harnessId?: string;\n\n constructor({\n message,\n harnessId,\n cause,\n }: {\n message: string;\n harnessId?: string;\n cause?: unknown;\n }) {\n super({ message, cause });\n Object.defineProperty(this, 'name', { value: name });\n this.harnessId = harnessId;\n }\n\n static isInstance(\n error: unknown,\n ): error is HarnessCapabilityUnsupportedError {\n return AISDKError.hasMarker(error, marker);\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;AAE3B,IAAM,OAAO;AACb,IAAM,SAAS,mBAAmB,IAAI;AACtC,IAAM,SAAS,OAAO,IAAI,MAAM;AAJhC;AAWO,IAAM,eAAN,eAA2B,iBACd,aADc,IAAW;AAAA,EAG3C,YAAY,EAAE,SAAS,MAAM,GAAyC;AACpE,UAAM,EAAE,MAAM,SAAS,MAAM,CAAC;AAHhC,SAAkB,MAAU;AAAA,EAI5B;AAAA,EAEA,OAAO,WAAW,OAAuC;AACvD,WAAO,WAAW,UAAU,OAAO,MAAM;AAAA,EAC3C;AACF;;;ACrBA,SAAS,cAAAA,mBAAkB;AAG3B,IAAMC,QAAO;AACb,IAAMC,UAAS,mBAAmBD,KAAI;AACtC,IAAME,UAAS,OAAO,IAAID,OAAM;AALhC,IAAAE,KAAAC;AAgBO,IAAM,oCAAN,eAAgDA,MAAA,cACnCD,MAAAD,SADmCE,KAAa;AAAA,EAKlE,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAIG;AACD,UAAM,EAAE,SAAS,MAAM,CAAC;AAb1B,SAAkBD,OAAU;AAc1B,WAAO,eAAe,MAAM,QAAQ,EAAE,OAAOH,MAAK,CAAC;AACnD,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,OAAO,WACL,OAC4C;AAC5C,WAAOK,YAAW,UAAU,OAAOJ,OAAM;AAAA,EAC3C;AACF;","names":["AISDKError","name","marker","symbol","_a","_b","AISDKError"]}
|
package/package.json
CHANGED
|
@@ -1 +1,64 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@ai-sdk/harness",
|
|
3
|
+
"version": "1.0.0-canary.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"source": "./src/index.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*",
|
|
12
|
+
"src",
|
|
13
|
+
"!src/**/*.test.ts",
|
|
14
|
+
"!src/**/*.test-d.ts",
|
|
15
|
+
"!src/**/__snapshots__",
|
|
16
|
+
"!src/**/__fixtures__",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
"./package.json": "./package.json",
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@ai-sdk/provider": "4.0.0-canary.18",
|
|
29
|
+
"@ai-sdk/provider-utils": "5.0.0-canary.46"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "22.19.19",
|
|
33
|
+
"tsup": "^8.5.1",
|
|
34
|
+
"typescript": "5.8.3",
|
|
35
|
+
"@vercel/ai-tsconfig": "0.0.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://ai-sdk.dev/docs",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/vercel/ai",
|
|
48
|
+
"directory": "packages/harness"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/vercel/ai/issues"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"ai",
|
|
55
|
+
"harness",
|
|
56
|
+
"agent"
|
|
57
|
+
],
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "pnpm clean && tsup --tsconfig tsconfig.build.json",
|
|
60
|
+
"build:watch": "pnpm clean && tsup --watch",
|
|
61
|
+
"clean": "del-cli dist *.tsbuildinfo",
|
|
62
|
+
"type-check": "tsc --build"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AISDKError } from '@ai-sdk/provider';
|
|
2
|
+
import { HarnessError } from './harness-error';
|
|
3
|
+
|
|
4
|
+
const name = 'AI_HarnessCapabilityUnsupportedError';
|
|
5
|
+
const marker = `vercel.ai.error.${name}`;
|
|
6
|
+
const symbol = Symbol.for(marker);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Thrown when a caller asks the harness to do something the adapter (or the
|
|
10
|
+
* supplied sandbox) does not support, e.g. requesting manual compaction from
|
|
11
|
+
* an adapter that only auto-compacts, or invoking `getPortUrl` on a sandbox
|
|
12
|
+
* that does not expose one.
|
|
13
|
+
*
|
|
14
|
+
* The caller supplies the full human-readable message. Optional `harnessId`
|
|
15
|
+
* is recorded as structured context for tooling.
|
|
16
|
+
*/
|
|
17
|
+
export class HarnessCapabilityUnsupportedError extends HarnessError {
|
|
18
|
+
private readonly [symbol] = true;
|
|
19
|
+
|
|
20
|
+
readonly harnessId?: string;
|
|
21
|
+
|
|
22
|
+
constructor({
|
|
23
|
+
message,
|
|
24
|
+
harnessId,
|
|
25
|
+
cause,
|
|
26
|
+
}: {
|
|
27
|
+
message: string;
|
|
28
|
+
harnessId?: string;
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
}) {
|
|
31
|
+
super({ message, cause });
|
|
32
|
+
Object.defineProperty(this, 'name', { value: name });
|
|
33
|
+
this.harnessId = harnessId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static isInstance(
|
|
37
|
+
error: unknown,
|
|
38
|
+
): error is HarnessCapabilityUnsupportedError {
|
|
39
|
+
return AISDKError.hasMarker(error, marker);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AISDKError } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
const name = 'AI_HarnessError';
|
|
4
|
+
const marker = `vercel.ai.error.${name}`;
|
|
5
|
+
const symbol = Symbol.for(marker);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base error type for failures originating in or signalled by a harness
|
|
9
|
+
* adapter. Specific failure modes (e.g. unsupported capability) extend this
|
|
10
|
+
* class.
|
|
11
|
+
*/
|
|
12
|
+
export class HarnessError extends AISDKError {
|
|
13
|
+
private readonly [symbol] = true;
|
|
14
|
+
|
|
15
|
+
constructor({ message, cause }: { message: string; cause?: unknown }) {
|
|
16
|
+
super({ name, message, cause });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static isInstance(error: unknown): error is HarnessError {
|
|
20
|
+
return AISDKError.hasMarker(error, marker);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './errors/harness-error';
|
|
2
|
+
export * from './errors/harness-capability-unsupported-error';
|
|
3
|
+
export type { HarnessV1SandboxProvider } from './v1/harness-v1-sandbox-provider';
|
|
4
|
+
export type {
|
|
5
|
+
HarnessV1NetworkPolicy,
|
|
6
|
+
HarnessV1NetworkSandboxSession,
|
|
7
|
+
} from './v1/harness-v1-network-sandbox-session';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Network sandbox session returned by `HarnessV1SandboxProvider.createSession()`. The
|
|
5
|
+
* harness keeps this for the lifetime of a session. It is itself a
|
|
6
|
+
* {@link SandboxSession} (file I/O, exec, spawn) and adds the infra surface on
|
|
7
|
+
* top: port resolution, lifecycle, and network-policy mutation.
|
|
8
|
+
*
|
|
9
|
+
* Code that should only touch the filesystem and spawn processes receives the
|
|
10
|
+
* reduced view from {@link HarnessV1NetworkSandboxSession.restricted}, never the
|
|
11
|
+
* network sandbox session itself — so it cannot stop the sandbox or change its
|
|
12
|
+
* network policy.
|
|
13
|
+
*/
|
|
14
|
+
export interface HarnessV1NetworkSandboxSession extends SandboxSession {
|
|
15
|
+
/**
|
|
16
|
+
* Stable identifier for the underlying sandbox resource. Used by the
|
|
17
|
+
* harness session manager as the durable lookup key for cross-process
|
|
18
|
+
* resume — the framework persists this on the resume payload so a future
|
|
19
|
+
* process can call `HarnessV1SandboxProvider.resume?({ sessionId })` and
|
|
20
|
+
* reach the same resource. Providers populate it from their native
|
|
21
|
+
* identifier (Vercel: the sandbox name; just-bash: a UUID minted at
|
|
22
|
+
* create time).
|
|
23
|
+
*/
|
|
24
|
+
readonly id: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The sandbox's default working directory — the absolute path that
|
|
28
|
+
* `run`/`spawn` resolve relative commands against when no `workingDirectory`
|
|
29
|
+
* is given. Read from the live sandbox (it is provider-specific and
|
|
30
|
+
* configurable at create time: Vercel defaults to `/vercel/sandbox`,
|
|
31
|
+
* just-bash to `/home/user`), never hardcoded.
|
|
32
|
+
*
|
|
33
|
+
* The framework composes each session's working directory underneath this
|
|
34
|
+
* path (`<defaultWorkingDirectory>/<harnessId>-<sessionId>`) so adapters do
|
|
35
|
+
* not bake a provider-specific base into their own paths.
|
|
36
|
+
*/
|
|
37
|
+
readonly defaultWorkingDirectory: string;
|
|
38
|
+
|
|
39
|
+
/** Ports the sandbox exposes; resolvable to public URLs via `getPortUrl`. */
|
|
40
|
+
readonly ports: ReadonlyArray<number>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a publicly-reachable URL for a sandbox-exposed port. Bridge-backed
|
|
44
|
+
* adapters call this to open their WebSocket to the in-sandbox bridge.
|
|
45
|
+
*/
|
|
46
|
+
readonly getPortUrl: (options: {
|
|
47
|
+
port: number;
|
|
48
|
+
protocol?: 'http' | 'https' | 'ws';
|
|
49
|
+
}) => PromiseLike<string>;
|
|
50
|
+
|
|
51
|
+
/** Stop the sandbox. Idempotent. */
|
|
52
|
+
readonly stop: () => PromiseLike<void>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Destroy/delete the sandbox resource when supported. Optional because some
|
|
56
|
+
* providers only have a stop/dispose concept. Implementations must handle
|
|
57
|
+
* both a still-running sandbox and a previously stopped sandbox.
|
|
58
|
+
*/
|
|
59
|
+
readonly destroy?: () => PromiseLike<void>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Update the sandbox's outbound network policy. Optional — implementations
|
|
63
|
+
* without a local enforcement primitive (e.g. just-bash) omit this. Callers
|
|
64
|
+
* use optional-call (`sandboxSession.setNetworkPolicy?.(policy)`); a
|
|
65
|
+
* missing implementation is a no-op.
|
|
66
|
+
*/
|
|
67
|
+
readonly setNetworkPolicy?: (
|
|
68
|
+
policy: HarnessV1NetworkPolicy,
|
|
69
|
+
) => PromiseLike<void>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replace the set of ports exposed by the sandbox. Full-replacement
|
|
73
|
+
* semantics: ports omitted from the array are deregistered. Optional —
|
|
74
|
+
* implementations that cannot expose ports (e.g. just-bash) omit this.
|
|
75
|
+
*/
|
|
76
|
+
readonly setPorts?: (
|
|
77
|
+
ports: ReadonlyArray<number>,
|
|
78
|
+
options?: { abortSignal?: AbortSignal },
|
|
79
|
+
) => PromiseLike<void>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reduced view of this session, typed as the bare {@link SandboxSession}
|
|
83
|
+
* (file I/O, exec, spawn) — nothing that could stop the sandbox or change
|
|
84
|
+
* its network policy. Pass this to user-tool `execute()` calls and other
|
|
85
|
+
* code that must not reach the infra surface.
|
|
86
|
+
*
|
|
87
|
+
* The returned object points at exactly the same underlying sandbox
|
|
88
|
+
* resource as the network sandbox session it was produced from; it is only a
|
|
89
|
+
* narrower surface over the same resource, not a separate sandbox.
|
|
90
|
+
*/
|
|
91
|
+
readonly restricted: () => SandboxSession;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Outbound network policy applied by the sandbox runtime.
|
|
96
|
+
*
|
|
97
|
+
* `'allow-all'` and `'deny-all'` are convenience presets. `'custom'` is an
|
|
98
|
+
* allow-list with an optional CIDR deny-list that takes precedence:
|
|
99
|
+
*
|
|
100
|
+
* - Reachable hosts are the union of `allowedHosts` and `allowedCIDRs`.
|
|
101
|
+
* - `deniedCIDRs` wins over both, useful for blocking cloud-metadata IPs while
|
|
102
|
+
* otherwise allowing broad access.
|
|
103
|
+
*
|
|
104
|
+
* The two `'custom'` branches share the same discriminator but each requires
|
|
105
|
+
* a different allow field. Specifying `'custom'` with only `deniedCIDRs`
|
|
106
|
+
* (deny-only) is rejected at compile time — functionally it would be
|
|
107
|
+
* equivalent to `'deny-all'`.
|
|
108
|
+
*/
|
|
109
|
+
export type HarnessV1NetworkPolicy =
|
|
110
|
+
| { mode: 'allow-all' }
|
|
111
|
+
| { mode: 'deny-all' }
|
|
112
|
+
| {
|
|
113
|
+
mode: 'custom';
|
|
114
|
+
allowedHosts: ReadonlyArray<string>;
|
|
115
|
+
allowedCIDRs?: ReadonlyArray<string>;
|
|
116
|
+
deniedCIDRs?: ReadonlyArray<string>;
|
|
117
|
+
}
|
|
118
|
+
| {
|
|
119
|
+
mode: 'custom';
|
|
120
|
+
allowedHosts?: ReadonlyArray<string>;
|
|
121
|
+
allowedCIDRs: ReadonlyArray<string>;
|
|
122
|
+
deniedCIDRs?: ReadonlyArray<string>;
|
|
123
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
2
|
+
import type { HarnessV1NetworkSandboxSession } from './harness-v1-network-sandbox-session';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Provider that produces network sandbox sessions for harness sessions. Lives at
|
|
6
|
+
* module scope as a stable, synchronous object — analogous to
|
|
7
|
+
* `LanguageModelV4` providers, no I/O performed at construction. The actual
|
|
8
|
+
* sandbox is created (or wrapped) when `HarnessAgent` calls `createSession()`.
|
|
9
|
+
*/
|
|
10
|
+
export interface HarnessV1SandboxProvider {
|
|
11
|
+
readonly specificationVersion: 'harness-sandbox-v1';
|
|
12
|
+
readonly providerId: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pool of ports the consumer reserved on a caller-provided sandbox for
|
|
16
|
+
* concurrent harness sessions. The session manager leases one port per
|
|
17
|
+
* session and releases on stop or destroy.
|
|
18
|
+
*
|
|
19
|
+
* Only meaningful when the provider wraps a caller-provided sandbox
|
|
20
|
+
* (the caller pre-declared the ports). In create-new modes the provider
|
|
21
|
+
* mints a fresh sandbox per session, so no leasing is needed; providers
|
|
22
|
+
* leave this undefined.
|
|
23
|
+
*/
|
|
24
|
+
readonly bridgePorts?: ReadonlyArray<number>;
|
|
25
|
+
|
|
26
|
+
readonly createSession: (options?: {
|
|
27
|
+
/**
|
|
28
|
+
* Stable per-session identifier. When supplied, the provider names the
|
|
29
|
+
* underlying resource deterministically so a future call to `resume`
|
|
30
|
+
* (potentially from a different process) can find the same sandbox.
|
|
31
|
+
* Omitted from prewarm and other paths that don't need a resumable
|
|
32
|
+
* resource — in that case the provider falls back to its native
|
|
33
|
+
* auto-naming.
|
|
34
|
+
*/
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
abortSignal?: AbortSignal;
|
|
37
|
+
/**
|
|
38
|
+
* Stable identity for snapshot-based reuse. Providers that support
|
|
39
|
+
* persistence/snapshots use this as part of the persistent sandbox
|
|
40
|
+
* name; subsequent calls with the same identity resume from snapshot.
|
|
41
|
+
*
|
|
42
|
+
* Ignored when the provider is wrapping a caller-provided sandbox.
|
|
43
|
+
*/
|
|
44
|
+
identity?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Called exactly once per identity, on fresh creation. Snapshot-capable
|
|
47
|
+
* providers wire this into the platform's one-time-setup hook so the
|
|
48
|
+
* side effects are baked into the snapshot. Providers without snapshot
|
|
49
|
+
* support run it immediately after fresh create.
|
|
50
|
+
*
|
|
51
|
+
* Not called when the provider is wrapping a caller-provided sandbox
|
|
52
|
+
* (the caller owns the sandbox; the framework applies its own
|
|
53
|
+
* idempotent bootstrap post-create instead).
|
|
54
|
+
*/
|
|
55
|
+
onFirstCreate?: (
|
|
56
|
+
session: SandboxSession,
|
|
57
|
+
opts: { abortSignal?: AbortSignal },
|
|
58
|
+
) => Promise<void>;
|
|
59
|
+
}) => PromiseLike<HarnessV1NetworkSandboxSession>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reattach to an existing sandbox previously created with the same
|
|
63
|
+
* `sessionId`. Optional — providers that cannot rehydrate by id (e.g.
|
|
64
|
+
* just-bash) omit this; the harness throws
|
|
65
|
+
* `HarnessCapabilityUnsupportedError` when resume is attempted against
|
|
66
|
+
* them.
|
|
67
|
+
*
|
|
68
|
+
* The provider derives the sandbox identifier from `sessionId` using the
|
|
69
|
+
* same deterministic naming scheme it used in `createSession`. Returns a
|
|
70
|
+
* network sandbox session bound to the existing resource.
|
|
71
|
+
*/
|
|
72
|
+
readonly resumeSession?: (options: {
|
|
73
|
+
sessionId: string;
|
|
74
|
+
abortSignal?: AbortSignal;
|
|
75
|
+
}) => PromiseLike<HarnessV1NetworkSandboxSession>;
|
|
76
|
+
}
|