vigiles 12.3.0 โ 12.4.0
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/README.md +9 -1
- package/dist/experimental.d.ts +27 -0
- package/dist/experimental.js +34 -0
- package/dist/services-docker.d.ts +50 -0
- package/dist/services-docker.js +234 -0
- package/dist/services.d.ts +209 -0
- package/dist/services.js +118 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -174,7 +174,15 @@ A hook that blocks nothing, a skill that hijacks unrelated prompts, context that
|
|
|
174
174
|
|
|
175
175
|
### ๐ Eval โ does a skill help, or just cost more?
|
|
176
176
|
|
|
177
|
-
_"65%
|
|
177
|
+
_"Caveman Mode cuts 65% of your tokens." Says who?_ vigiles A/Bs the claim on real coding tasks and hands you three numbers: the **token bill**, whether it hit its **target**, and whether your code still **works**.
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
caveman vs verbose ยท haiku ยท $0 on your subscription
|
|
181
|
+
output tokens 762 โ 842 (+11% โ the "saving" reversed)
|
|
182
|
+
correctness 1.0 โ 1.0 (the fact survived)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Point it at any harness change that claims a number โ does a compression skill pay for itself, is a subagent worth its cost, which model is cheapest here. promptfoo and DeepEval bill **per token, every run**; vigiles runs on your own Claude Pro/Max subscription, so you measure on every change, not once. A committed lock file (like `package-lock`) keeps CI honest without re-calling the model. (Claude Code today; Codex landing.)
|
|
178
186
|
**[Measure a skill โ](docs/measuring-skills.md)**
|
|
179
187
|
|
|
180
188
|
## Quick start
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vigiles/experimental` โ โ ๏ธ EXPERIMENTAL, UNSTABLE public surface.
|
|
3
|
+
*
|
|
4
|
+
* Everything re-exported here is a DRAFT. It is deliberately quarantined behind
|
|
5
|
+
* the `experimental` subpath (and the `experimental_` name prefix on runtime
|
|
6
|
+
* exports) so the import itself signals the risk at the call site:
|
|
7
|
+
*
|
|
8
|
+
* import { experimental_startServices } from "vigiles/experimental";
|
|
9
|
+
*
|
|
10
|
+
* NOT covered by the stability guarantee (STABILITY.md): the shape may change or
|
|
11
|
+
* be removed WITHOUT a major-version bump. Do not depend on it in production.
|
|
12
|
+
*
|
|
13
|
+
* Current contents โ the R3 disposable-service tier (real side-effect testing;
|
|
14
|
+
* see docs/measuring-skills.md ยง Experimental and src/services.ts).
|
|
15
|
+
*
|
|
16
|
+
* โ ๏ธ SAFETY: R3 runs a model-driven skill FOR REAL. The disposable container is
|
|
17
|
+
* the ONLY isolation vigiles provides โ it does not confine the skill's filesystem
|
|
18
|
+
* or network. Run it in a disposable environment with NO production access and
|
|
19
|
+
* keep real credentials out of the run. See the SAFETY note in src/services.ts and
|
|
20
|
+
* docs/measuring-skills.md ยง Experimental.
|
|
21
|
+
*
|
|
22
|
+
* @experimental
|
|
23
|
+
* @module vigiles/experimental
|
|
24
|
+
*/
|
|
25
|
+
export { experimental_startServices, experimental_withServices, type ServiceSpec, type ServiceReady, type ServiceReset, type ServiceHandle, type ServiceSession, type ContainerRuntime, } from "./services.js";
|
|
26
|
+
export { experimental_dockerRuntime, makeDockerRuntime, type DockerExec, type NetProbe, } from "./services-docker.js";
|
|
27
|
+
//# sourceMappingURL=experimental.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeDockerRuntime = exports.experimental_dockerRuntime = exports.experimental_withServices = exports.experimental_startServices = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `vigiles/experimental` โ โ ๏ธ EXPERIMENTAL, UNSTABLE public surface.
|
|
6
|
+
*
|
|
7
|
+
* Everything re-exported here is a DRAFT. It is deliberately quarantined behind
|
|
8
|
+
* the `experimental` subpath (and the `experimental_` name prefix on runtime
|
|
9
|
+
* exports) so the import itself signals the risk at the call site:
|
|
10
|
+
*
|
|
11
|
+
* import { experimental_startServices } from "vigiles/experimental";
|
|
12
|
+
*
|
|
13
|
+
* NOT covered by the stability guarantee (STABILITY.md): the shape may change or
|
|
14
|
+
* be removed WITHOUT a major-version bump. Do not depend on it in production.
|
|
15
|
+
*
|
|
16
|
+
* Current contents โ the R3 disposable-service tier (real side-effect testing;
|
|
17
|
+
* see docs/measuring-skills.md ยง Experimental and src/services.ts).
|
|
18
|
+
*
|
|
19
|
+
* โ ๏ธ SAFETY: R3 runs a model-driven skill FOR REAL. The disposable container is
|
|
20
|
+
* the ONLY isolation vigiles provides โ it does not confine the skill's filesystem
|
|
21
|
+
* or network. Run it in a disposable environment with NO production access and
|
|
22
|
+
* keep real credentials out of the run. See the SAFETY note in src/services.ts and
|
|
23
|
+
* docs/measuring-skills.md ยง Experimental.
|
|
24
|
+
*
|
|
25
|
+
* @experimental
|
|
26
|
+
* @module vigiles/experimental
|
|
27
|
+
*/
|
|
28
|
+
var services_js_1 = require("./services.js");
|
|
29
|
+
Object.defineProperty(exports, "experimental_startServices", { enumerable: true, get: function () { return services_js_1.experimental_startServices; } });
|
|
30
|
+
Object.defineProperty(exports, "experimental_withServices", { enumerable: true, get: function () { return services_js_1.experimental_withServices; } });
|
|
31
|
+
var services_docker_js_1 = require("./services-docker.js");
|
|
32
|
+
Object.defineProperty(exports, "experimental_dockerRuntime", { enumerable: true, get: function () { return services_docker_js_1.experimental_dockerRuntime; } });
|
|
33
|
+
Object.defineProperty(exports, "makeDockerRuntime", { enumerable: true, get: function () { return services_docker_js_1.makeDockerRuntime; } });
|
|
34
|
+
//# sourceMappingURL=experimental.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ContainerRuntime, ServiceSpec } from "./services.js";
|
|
2
|
+
/** A synchronous docker CLI call โ the injected real-IO seam. */
|
|
3
|
+
export type DockerExec = (args: readonly string[]) => {
|
|
4
|
+
readonly stdout: string;
|
|
5
|
+
readonly stderr: string;
|
|
6
|
+
readonly code: number;
|
|
7
|
+
};
|
|
8
|
+
/** A TCP readiness probe against a published host port โ the second IO seam. */
|
|
9
|
+
export type NetProbe = (port: number) => Promise<boolean>;
|
|
10
|
+
/**
|
|
11
|
+
* A docker-safe, collision-free container name for a declared service. Sanitises
|
|
12
|
+
* the service name to docker's `[a-zA-Z0-9_.-]` charset and appends the process
|
|
13
|
+
* id + a monotonic counter so parallel/repeated runs never clash. Pure given the
|
|
14
|
+
* injected suffix, so it's unit-tested.
|
|
15
|
+
*/
|
|
16
|
+
export declare function containerNameFor(name: string, suffix: string): string;
|
|
17
|
+
/** The `docker run -d โฆ` argv for a service. Pure. */
|
|
18
|
+
export declare function dockerRunArgs(spec: ServiceSpec, containerName: string): string[];
|
|
19
|
+
/** The `docker exec <c> sh -c <cmd>` argv. Pure. */
|
|
20
|
+
export declare function dockerExecArgs(containerName: string, command: string): string[];
|
|
21
|
+
/** The port to publish โ a 0-or-1 element list (v0 is single-port). Pure. */
|
|
22
|
+
export declare function publishedPorts(spec: ServiceSpec): number[];
|
|
23
|
+
/**
|
|
24
|
+
* Parse the published host port out of `docker port <c> <containerPort>` output,
|
|
25
|
+
* e.g. `0.0.0.0:49153` or `127.0.0.1:49153\n[::]:49153` โ `49153`. Returns
|
|
26
|
+
* `undefined` when nothing is published โ parse-don't-validate: the caller gets
|
|
27
|
+
* "a port or nothing", never a magic `0` to special-case. Pure.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseDockerPort(output: string): number | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Build a Docker-backed {@link ContainerRuntime}. The two real-IO seams default
|
|
32
|
+
* to the real docker CLI + a real TCP probe, but both are injectable so the
|
|
33
|
+
* lifecycle is unit-testable with fakes.
|
|
34
|
+
*
|
|
35
|
+
* @experimental
|
|
36
|
+
*/
|
|
37
|
+
export declare function makeDockerRuntime(deps?: {
|
|
38
|
+
exec?: DockerExec;
|
|
39
|
+
netProbe?: NetProbe;
|
|
40
|
+
sleep?: (ms: number) => Promise<void>;
|
|
41
|
+
/** Readiness deadline; injectable so a test can cover the timeout path. */
|
|
42
|
+
readyTimeoutMs?: number;
|
|
43
|
+
}): ContainerRuntime;
|
|
44
|
+
/**
|
|
45
|
+
* The default Docker-backed runtime โ pass to `experimental_startServices`.
|
|
46
|
+
*
|
|
47
|
+
* @experimental
|
|
48
|
+
*/
|
|
49
|
+
export declare const experimental_dockerRuntime: ContainerRuntime;
|
|
50
|
+
//# sourceMappingURL=services-docker.d.ts.map
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.experimental_dockerRuntime = void 0;
|
|
4
|
+
exports.containerNameFor = containerNameFor;
|
|
5
|
+
exports.dockerRunArgs = dockerRunArgs;
|
|
6
|
+
exports.dockerExecArgs = dockerExecArgs;
|
|
7
|
+
exports.publishedPorts = publishedPorts;
|
|
8
|
+
exports.parseDockerPort = parseDockerPort;
|
|
9
|
+
exports.makeDockerRuntime = makeDockerRuntime;
|
|
10
|
+
/**
|
|
11
|
+
* vigiles โ a Docker-backed {@link ContainerRuntime} for the R3 disposable-service
|
|
12
|
+
* tier (โ ๏ธ EXPERIMENTAL / UNSTABLE โ see src/services.ts and `vigiles/experimental`).
|
|
13
|
+
*
|
|
14
|
+
* This is the v0 backend the R3 build spec (research/r3-disposable-services.md)
|
|
15
|
+
* scopes: `docker run` a throwaway service, wait for it to be ready, run its seed,
|
|
16
|
+
* hand back a {@link ServiceHandle} whose `exec` shells into the container, and
|
|
17
|
+
* `docker rm -f` it on teardown.
|
|
18
|
+
*
|
|
19
|
+
* The design mirrors `src/sandbox.ts` / `src/egress.ts`: the COMMAND BUILDERS and
|
|
20
|
+
* output parsers are pure (unit-tested), and the two real-IO seams โ the
|
|
21
|
+
* synchronous docker CLI (`exec`) and the TCP readiness probe (`netProbe`) โ are
|
|
22
|
+
* INJECTED, so the orchestration is fully testable with fakes and only the
|
|
23
|
+
* end-to-end integration test needs a live daemon (it skips when absent).
|
|
24
|
+
*
|
|
25
|
+
* @experimental
|
|
26
|
+
* @module vigiles/experimental (docker backend)
|
|
27
|
+
*/
|
|
28
|
+
const node_child_process_1 = require("node:child_process");
|
|
29
|
+
const node_net_1 = require("node:net");
|
|
30
|
+
const hash_js_1 = require("./core/hash.js");
|
|
31
|
+
/** How long to wait for a service to become ready before giving up. */
|
|
32
|
+
const DEFAULT_READY_TIMEOUT_MS = 30_000;
|
|
33
|
+
/** Delay between readiness polls. */
|
|
34
|
+
const READY_POLL_MS = 250;
|
|
35
|
+
let containerCounter = 0;
|
|
36
|
+
/**
|
|
37
|
+
* A docker-safe, collision-free container name for a declared service. Sanitises
|
|
38
|
+
* the service name to docker's `[a-zA-Z0-9_.-]` charset and appends the process
|
|
39
|
+
* id + a monotonic counter so parallel/repeated runs never clash. Pure given the
|
|
40
|
+
* injected suffix, so it's unit-tested.
|
|
41
|
+
*/
|
|
42
|
+
function containerNameFor(name, suffix) {
|
|
43
|
+
const safe = name.replace(/[^a-zA-Z0-9_.-]/g, "-").replace(/^[-.]+/, "") || "svc";
|
|
44
|
+
return `vigiles-${safe}-${suffix}`;
|
|
45
|
+
}
|
|
46
|
+
/** The `docker run -d โฆ` argv for a service. Pure. */
|
|
47
|
+
function dockerRunArgs(spec, containerName) {
|
|
48
|
+
const args = ["run", "-d", "--rm", "--name", containerName];
|
|
49
|
+
for (const [k, v] of Object.entries(spec.env ?? {})) {
|
|
50
|
+
args.push("-e", `${k}=${v}`);
|
|
51
|
+
}
|
|
52
|
+
for (const port of publishedPorts(spec)) {
|
|
53
|
+
// Publish to an ephemeral loopback host port; `docker port` discovers it.
|
|
54
|
+
args.push("-p", `127.0.0.1::${port}`);
|
|
55
|
+
}
|
|
56
|
+
args.push(spec.image);
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
/** The `docker exec <c> sh -c <cmd>` argv. Pure. */
|
|
60
|
+
function dockerExecArgs(containerName, command) {
|
|
61
|
+
return ["exec", containerName, "sh", "-c", command];
|
|
62
|
+
}
|
|
63
|
+
/** The port to publish โ a 0-or-1 element list (v0 is single-port). Pure. */
|
|
64
|
+
function publishedPorts(spec) {
|
|
65
|
+
return spec.port === undefined ? [] : [spec.port];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parse the published host port out of `docker port <c> <containerPort>` output,
|
|
69
|
+
* e.g. `0.0.0.0:49153` or `127.0.0.1:49153\n[::]:49153` โ `49153`. Returns
|
|
70
|
+
* `undefined` when nothing is published โ parse-don't-validate: the caller gets
|
|
71
|
+
* "a port or nothing", never a magic `0` to special-case. Pure.
|
|
72
|
+
*/
|
|
73
|
+
function parseDockerPort(output) {
|
|
74
|
+
for (const line of output.split("\n")) {
|
|
75
|
+
const m = /:(\d+)\s*$/.exec(line.trim());
|
|
76
|
+
if (m)
|
|
77
|
+
return Number(m[1]);
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/* v8 ignore start -- the real-daemon IO seams: they spawn the real `docker` CLI /
|
|
82
|
+
open a real socket / sleep, and are exercised by the gated integration test
|
|
83
|
+
(skipped without a daemon) + the real-socket unit test โ not the coverage gate,
|
|
84
|
+
which the pure builders + the fake-injected orchestration below carry. Same
|
|
85
|
+
pattern as sandbox.ts's `runSandboxed`. */
|
|
86
|
+
/** Default real docker CLI seam (synchronous). */
|
|
87
|
+
const realDockerExec = (args) => {
|
|
88
|
+
const r = (0, node_child_process_1.spawnSync)("docker", args, { encoding: "utf-8" });
|
|
89
|
+
return {
|
|
90
|
+
stdout: r.stdout ?? "",
|
|
91
|
+
stderr: r.stderr ?? "",
|
|
92
|
+
code: r.status ?? 1,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
/** Default TCP probe: resolve true if a connection to 127.0.0.1:port succeeds. */
|
|
96
|
+
const realNetProbe = (port) => new Promise((resolve) => {
|
|
97
|
+
const sock = (0, node_net_1.connect)({ host: "127.0.0.1", port }, () => {
|
|
98
|
+
sock.destroy();
|
|
99
|
+
resolve(true);
|
|
100
|
+
});
|
|
101
|
+
sock.on("error", () => {
|
|
102
|
+
sock.destroy();
|
|
103
|
+
resolve(false);
|
|
104
|
+
});
|
|
105
|
+
sock.setTimeout(1000, () => {
|
|
106
|
+
sock.destroy();
|
|
107
|
+
resolve(false);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
111
|
+
/** Parse the authoring union into a tagged {@link ReadyProbe} (exec > log > tcp). */
|
|
112
|
+
function parseReady(ready) {
|
|
113
|
+
if ("exec" in ready)
|
|
114
|
+
return { kind: "exec", command: ready.exec };
|
|
115
|
+
if ("log" in ready)
|
|
116
|
+
return { kind: "log", pattern: ready.log };
|
|
117
|
+
return { kind: "tcp", containerPort: ready.tcp }; // carry the DECLARED port
|
|
118
|
+
}
|
|
119
|
+
/** One readiness attempt for a parsed {@link ReadyProbe} โ exhaustive by `kind`. */
|
|
120
|
+
function probeReady(ctx, probe) {
|
|
121
|
+
switch (probe.kind) {
|
|
122
|
+
case "exec":
|
|
123
|
+
return Promise.resolve(ctx.exec(dockerExecArgs(ctx.containerName, probe.command)).code === 0);
|
|
124
|
+
case "log": {
|
|
125
|
+
const r = ctx.exec(["logs", ctx.containerName]);
|
|
126
|
+
return Promise.resolve(probe.pattern.test(r.stdout + r.stderr));
|
|
127
|
+
}
|
|
128
|
+
case "tcp": {
|
|
129
|
+
// Resolve the DECLARED tcp port's host mapping (it must be the published
|
|
130
|
+
// port) โ the value isn't discarded, so a wrong port surfaces as an error.
|
|
131
|
+
const hostPort = parseDockerPort(ctx.exec(["port", ctx.containerName, String(probe.containerPort)])
|
|
132
|
+
.stdout);
|
|
133
|
+
if (hostPort === undefined) {
|
|
134
|
+
throw new Error(`readiness { tcp: ${probe.containerPort} } โ container port ${probe.containerPort} is not published for "${ctx.containerName}"`);
|
|
135
|
+
}
|
|
136
|
+
return ctx.netProbe(hostPort);
|
|
137
|
+
}
|
|
138
|
+
/* v8 ignore next 2 -- exhaustiveness guard, unreachable given ReadyProbe */
|
|
139
|
+
default:
|
|
140
|
+
return (0, hash_js_1.assertNever)(probe);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Poll {@link probeReady} until it passes or the deadline elapses. */
|
|
144
|
+
async function waitReady(ctx, ready, timeoutMs) {
|
|
145
|
+
if (!ready)
|
|
146
|
+
return;
|
|
147
|
+
const probe = parseReady(ready); // parse ONCE, then poll
|
|
148
|
+
const started = numericNow();
|
|
149
|
+
for (;;) {
|
|
150
|
+
if (await probeReady(ctx, probe))
|
|
151
|
+
return;
|
|
152
|
+
if (numericNow() - started >= timeoutMs) {
|
|
153
|
+
throw new Error(`service "${ctx.containerName}" did not become ready within ${timeoutMs}ms`);
|
|
154
|
+
}
|
|
155
|
+
await ctx.sleep(READY_POLL_MS);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Isolated so the deadline math is injectable-free but still centralised.
|
|
159
|
+
function numericNow() {
|
|
160
|
+
return Date.now();
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Build a Docker-backed {@link ContainerRuntime}. The two real-IO seams default
|
|
164
|
+
* to the real docker CLI + a real TCP probe, but both are injectable so the
|
|
165
|
+
* lifecycle is unit-testable with fakes.
|
|
166
|
+
*
|
|
167
|
+
* @experimental
|
|
168
|
+
*/
|
|
169
|
+
function makeDockerRuntime(deps = {}) {
|
|
170
|
+
const exec = deps.exec ?? realDockerExec;
|
|
171
|
+
const netProbe = deps.netProbe ?? realNetProbe;
|
|
172
|
+
const sleep = deps.sleep ?? delay;
|
|
173
|
+
const readyTimeoutMs = deps.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
174
|
+
return {
|
|
175
|
+
name: "docker",
|
|
176
|
+
available() {
|
|
177
|
+
return exec(["info"]).code === 0;
|
|
178
|
+
},
|
|
179
|
+
async start(name, spec) {
|
|
180
|
+
const containerName = containerNameFor(name, `${process.pid}-${containerCounter++}`);
|
|
181
|
+
const run = exec(dockerRunArgs(spec, containerName));
|
|
182
|
+
if (run.code !== 0) {
|
|
183
|
+
throw new Error(`docker run failed for service "${name}" (${spec.image}): ${run.stderr.trim()}`);
|
|
184
|
+
}
|
|
185
|
+
const primary = publishedPorts(spec)[0];
|
|
186
|
+
const hostPort = primary === undefined
|
|
187
|
+
? undefined
|
|
188
|
+
: parseDockerPort(exec(["port", containerName, String(primary)]).stdout);
|
|
189
|
+
const handle = {
|
|
190
|
+
host: "127.0.0.1",
|
|
191
|
+
port: hostPort, // undefined when the service exposes no port
|
|
192
|
+
// no `url`: the docker backend can't form a generic scheme โ build your
|
|
193
|
+
// own from host + port. (Omitted, not `""`.)
|
|
194
|
+
exec(command) {
|
|
195
|
+
const r = exec(dockerExecArgs(containerName, command));
|
|
196
|
+
return { stdout: r.stdout, stderr: r.stderr, code: r.code };
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
try {
|
|
200
|
+
await waitReady({ exec, netProbe, sleep, containerName }, spec.ready, readyTimeoutMs);
|
|
201
|
+
if (spec.seed) {
|
|
202
|
+
const s = exec(dockerExecArgs(containerName, spec.seed));
|
|
203
|
+
if (s.code !== 0) {
|
|
204
|
+
throw new Error(`seed failed for service "${name}": ${s.stderr.trim()}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
exec(["rm", "-f", containerName]);
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
// Carry the container name for stop() via a non-enumerable back-reference.
|
|
213
|
+
containerNames.set(handle, containerName);
|
|
214
|
+
return handle;
|
|
215
|
+
},
|
|
216
|
+
async stop(handle) {
|
|
217
|
+
const containerName = containerNames.get(handle);
|
|
218
|
+
if (containerName) {
|
|
219
|
+
exec(["rm", "-f", containerName]);
|
|
220
|
+
containerNames.delete(handle);
|
|
221
|
+
}
|
|
222
|
+
return Promise.resolve();
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Maps a returned handle โ its container name, so stop() needs no public field. */
|
|
227
|
+
const containerNames = new WeakMap();
|
|
228
|
+
/**
|
|
229
|
+
* The default Docker-backed runtime โ pass to `experimental_startServices`.
|
|
230
|
+
*
|
|
231
|
+
* @experimental
|
|
232
|
+
*/
|
|
233
|
+
exports.experimental_dockerRuntime = makeDockerRuntime();
|
|
234
|
+
//# sourceMappingURL=services-docker.js.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles โ R3 disposable-service tier (โ ๏ธ EXPERIMENTAL / UNSTABLE).
|
|
3
|
+
*
|
|
4
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
* EXPERIMENTAL: this surface is a DRAFT. Import it from `vigiles/experimental`,
|
|
6
|
+
* NOT from a stable subpath. It is NOT covered by the stability guarantee and
|
|
7
|
+
* may change shape or be removed WITHOUT a major-version bump. Do not build a
|
|
8
|
+
* production workflow on it yet. See docs/measuring-skills.md ยง Experimental.
|
|
9
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
10
|
+
*
|
|
11
|
+
* WHAT THIS IS. The free tiers (`runHook`, `measureTriggerRate`, `runEval`)
|
|
12
|
+
* cover a skill's OUTPUT and its side-effect SAFETY (did it call / not call a
|
|
13
|
+
* tool, write / not write a file) with no container. What they do NOT do is let
|
|
14
|
+
* a skill actually PERFORM a side effect against a real service and verify the
|
|
15
|
+
* resulting state โ apply a migration to a real Postgres and check the row
|
|
16
|
+
* landed. That is the R3 rung (see research/eval-coverage-and-isolation.md): the
|
|
17
|
+
* REAL system's semantics are the thing under test, so it can't be faked.
|
|
18
|
+
*
|
|
19
|
+
* THE POSTURE. vigiles COMPOSES with a throwaway container; it does not reinvent
|
|
20
|
+
* the sandbox. A {@link ServiceSpec} declares a disposable service; the injected
|
|
21
|
+
* {@link ContainerRuntime} port starts it, and it is force-removed on teardown.
|
|
22
|
+
*
|
|
23
|
+
* โ ๏ธ SAFETY โ the isolation is the DISPOSABLE CONTAINER, nothing more. A skill is
|
|
24
|
+
* model-driven, so the MODEL chooses the actions: it can do anything the run
|
|
25
|
+
* ENVIRONMENT allows. vigiles creates and destroys the container; it does NOT, in
|
|
26
|
+
* this tier, confine the skill's filesystem or block its network. So an R3 run is
|
|
27
|
+
* only as safe as the environment you run it in:
|
|
28
|
+
* - run it in a DISPOSABLE environment โ a CI job, a throwaway container/VM, or
|
|
29
|
+
* a dev box with NO production access;
|
|
30
|
+
* - point the task at the disposable service's connection string ONLY;
|
|
31
|
+
* - keep real credentials OUT of the run (prod `DATABASE_URL`, cloud keys,
|
|
32
|
+
* `~/.ssh`) โ pair it with the eval tier's `ephemeralEnv` (throwaway HOME +
|
|
33
|
+
* cleared env) to scrub them so the model has no real keys to misuse.
|
|
34
|
+
* Treat it like running an untrusted script. A future increment adds an egress
|
|
35
|
+
* wall (the skill reaches only the model + the service); until then that job is
|
|
36
|
+
* the operator's. See docs/measuring-skills.md ยง Experimental.
|
|
37
|
+
*
|
|
38
|
+
* WHAT SHIPS TODAY vs LATER. Today: the TYPES, the {@link ContainerRuntime} port,
|
|
39
|
+
* the pure {@link experimental_startServices} / {@link experimental_withServices}
|
|
40
|
+
* orchestration, and a Docker backend (`src/services-docker.ts`). Deferred: the
|
|
41
|
+
* `runEval` / `measureArms` `services` option + `ctx.service(name)`, per-trial
|
|
42
|
+
* reset via an eval-loop hook, and the egress wall. Requires Docker (Linux-first).
|
|
43
|
+
* It is an explicit opt-in and NEVER part of `vigiles audit` (audit stays
|
|
44
|
+
* side-effect-free).
|
|
45
|
+
*
|
|
46
|
+
* @experimental
|
|
47
|
+
* @module vigiles/experimental (services)
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* How a service signals it is ready to accept work โ polled by the
|
|
51
|
+
* {@link ContainerRuntime} before {@link ContainerRuntime.start} resolves, so a
|
|
52
|
+
* consumer never races a half-booted service.
|
|
53
|
+
*
|
|
54
|
+
* @experimental
|
|
55
|
+
*/
|
|
56
|
+
export type ServiceReady = {
|
|
57
|
+
readonly tcp: number;
|
|
58
|
+
} | {
|
|
59
|
+
readonly log: RegExp;
|
|
60
|
+
} | {
|
|
61
|
+
readonly exec: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* When a fresh container is provisioned. `"per-trial"` (the DEFAULT) gives each
|
|
65
|
+
* eval trial a clean service so trial N can never observe trial N-1's writes โ
|
|
66
|
+
* the repeatability property that makes a side-effecting eval trustworthy.
|
|
67
|
+
* `"per-arm"` shares one container across an arm's trials: faster, but only sound
|
|
68
|
+
* when the task is idempotent or reads only.
|
|
69
|
+
*
|
|
70
|
+
* @experimental
|
|
71
|
+
*/
|
|
72
|
+
export type ServiceReset = "per-trial" | "per-arm";
|
|
73
|
+
/**
|
|
74
|
+
* A disposable service the agent's task may act against โ declared per arm/spec
|
|
75
|
+
* on the (future) `services` eval option.
|
|
76
|
+
*
|
|
77
|
+
* @experimental
|
|
78
|
+
*/
|
|
79
|
+
export interface ServiceSpec {
|
|
80
|
+
/** Container image, e.g. `"postgres:16"` or `"redis:7"`. */
|
|
81
|
+
readonly image: string;
|
|
82
|
+
/** Environment for the container (e.g. `POSTGRES_PASSWORD`). */
|
|
83
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
84
|
+
/**
|
|
85
|
+
* The port to publish + reach โ the one `ServiceHandle.port` reports and
|
|
86
|
+
* `endpoints` lists. v0 supports a SINGLE port (the common disposable-service
|
|
87
|
+
* case: a DB, a cache); multi-port publishing is deferred rather than half-done
|
|
88
|
+
* (a declared-but-unexposed secondary port would be a misleading state).
|
|
89
|
+
*/
|
|
90
|
+
readonly port?: number;
|
|
91
|
+
/** Readiness gate โ start() resolves only once this passes. */
|
|
92
|
+
readonly ready?: ServiceReady;
|
|
93
|
+
/**
|
|
94
|
+
* A one-shot command run INSIDE the fresh container after it is ready and
|
|
95
|
+
* before the agent runs โ e.g. `"psql -U postgres -d app -f schema.sql"`. Its
|
|
96
|
+
* output is recorded once; it is NOT model-synthesized.
|
|
97
|
+
*/
|
|
98
|
+
readonly seed?: string;
|
|
99
|
+
/** Provisioning cadence โ {@link ServiceReset}. Defaults to `"per-trial"`. */
|
|
100
|
+
readonly reset?: ServiceReset;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A started service, handed to `measure((ctx) => โฆ)` as `ctx.service(name)` so a
|
|
104
|
+
* check can inspect the REAL resulting state.
|
|
105
|
+
*
|
|
106
|
+
* NOTE: {@link ServiceHandle.exec} is SYNCHRONOUS on purpose โ the eval `measure`
|
|
107
|
+
* callback is synchronous (it blocks via `spawnSync`, like `judge()`), so state
|
|
108
|
+
* inspection must be sync too. `exec` is the ONLY inspection primitive by design:
|
|
109
|
+
* it shells the service's own CLI (`psql`, `redis-cli`, โฆ) so vigiles never takes
|
|
110
|
+
* a per-service client dependency.
|
|
111
|
+
*
|
|
112
|
+
* @experimental
|
|
113
|
+
*/
|
|
114
|
+
export interface ServiceHandle {
|
|
115
|
+
/** Host the agent reaches the service on (from inside the run environment). */
|
|
116
|
+
readonly host: string;
|
|
117
|
+
/** The published host port โ `undefined` for a service that exposes none. */
|
|
118
|
+
readonly port?: number;
|
|
119
|
+
/**
|
|
120
|
+
* A connection URL when the runtime can form one; omitted otherwise (build your
|
|
121
|
+
* own from `host` + `port` โ the scheme differs per service, so no generic URL).
|
|
122
|
+
*/
|
|
123
|
+
readonly url?: string;
|
|
124
|
+
/** Run a command inside the container and read its result (synchronous). */
|
|
125
|
+
exec(command: string): {
|
|
126
|
+
readonly stdout: string;
|
|
127
|
+
readonly stderr: string;
|
|
128
|
+
readonly code: number;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A live set of started services + the teardown that disposes them. Returned by
|
|
133
|
+
* {@link experimental_startServices}; the caller wires `endpoints` into the run's
|
|
134
|
+
* egress allowlist and maps `handles` into the agent's env / into `measure`.
|
|
135
|
+
*
|
|
136
|
+
* @experimental
|
|
137
|
+
*/
|
|
138
|
+
export interface ServiceSession {
|
|
139
|
+
/** The started services, keyed by the name declared in the `services` map. */
|
|
140
|
+
readonly handles: Readonly<Record<string, ServiceHandle>>;
|
|
141
|
+
/**
|
|
142
|
+
* `host:port` endpoints for the started services โ where to point your task.
|
|
143
|
+
* Intended to ALSO pin an egress allowlist (composing with `src/egress.ts`),
|
|
144
|
+
* but that wall is a FUTURE hardening: the eval tier does NOT apply it today, so
|
|
145
|
+
* right now these just identify the services. Do not read them as a network
|
|
146
|
+
* confinement guarantee โ see the SAFETY note in this module's header.
|
|
147
|
+
*/
|
|
148
|
+
readonly endpoints: readonly string[];
|
|
149
|
+
/** Stop + remove every started container. Always await this in a `finally`. */
|
|
150
|
+
teardown(): Promise<void>;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The container-lifecycle PORT โ the seam a concrete backend implements (raw
|
|
154
|
+
* `docker` is the planned v0 default; podman / testcontainers / the macOS
|
|
155
|
+
* os-isolation backend swap in by injection, never a conditional). Kept
|
|
156
|
+
* harness-agnostic and dependency-free at the core, exactly like the five
|
|
157
|
+
* harness ports: the abstraction exists so a second backend is a NEW OBJECT, not
|
|
158
|
+
* an edit threaded through the orchestrator.
|
|
159
|
+
*
|
|
160
|
+
* `start` is responsible for HONOURING {@link ServiceSpec.ready} + running
|
|
161
|
+
* {@link ServiceSpec.seed}, so it resolves only with a ready, seeded service.
|
|
162
|
+
*
|
|
163
|
+
* @experimental
|
|
164
|
+
*/
|
|
165
|
+
export interface ContainerRuntime {
|
|
166
|
+
/** Backend name for diagnostics, e.g. `"docker"`. */
|
|
167
|
+
readonly name: string;
|
|
168
|
+
/** Whether this backend can actually run here (e.g. `docker info` succeeds). */
|
|
169
|
+
available(): boolean | Promise<boolean>;
|
|
170
|
+
/** Provision + ready + seed one service, returning its handle. */
|
|
171
|
+
start(name: string, spec: ServiceSpec): Promise<ServiceHandle>;
|
|
172
|
+
/** Stop + remove a started service. */
|
|
173
|
+
stop(handle: ServiceHandle): Promise<void>;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Start every declared service against an injected {@link ContainerRuntime},
|
|
177
|
+
* returning a {@link ServiceSession}. Pure orchestration over the port โ no
|
|
178
|
+
* `docker` is imported here, so this is fully unit-testable with a fake runtime
|
|
179
|
+
* (the `decideSandbox`-is-pure / `runSandboxed`-is-real split). If any service
|
|
180
|
+
* fails to start, the ones already up are torn down before the error propagates,
|
|
181
|
+
* so a partial failure never leaks a container.
|
|
182
|
+
*
|
|
183
|
+
* A Docker-backed `ContainerRuntime` and the `runEval` / `measureArms` wiring are
|
|
184
|
+
* the next increment โ until then this is the primitive you compose by hand:
|
|
185
|
+
* start โ pin egress to `session.endpoints` โ run โ read `session.handles` in
|
|
186
|
+
* `measure` โ `await session.teardown()`.
|
|
187
|
+
*
|
|
188
|
+
* @experimental โ surface may change without a major-version bump.
|
|
189
|
+
*/
|
|
190
|
+
export declare function experimental_startServices(services: Readonly<Record<string, ServiceSpec>>, runtime: ContainerRuntime): Promise<ServiceSession>;
|
|
191
|
+
/**
|
|
192
|
+
* Run `fn` with the declared services up, disposing them afterwards โ even if
|
|
193
|
+
* `fn` throws. The scope-guard form of {@link experimental_startServices}: it
|
|
194
|
+
* removes the manual `try/finally` so a `measureArms` / `measure` call can be
|
|
195
|
+
* wrapped in one line and its containers are always cleaned up.
|
|
196
|
+
*
|
|
197
|
+
* โ ๏ธ Read the SAFETY note in this module's header first โ the container is the
|
|
198
|
+
* only isolation; keep real credentials out of the run.
|
|
199
|
+
*
|
|
200
|
+
* LIFECYCLE NOTE (honest): the services live for the WHOLE `fn` โ i.e. per-RUN,
|
|
201
|
+
* not per-trial. An eval that mutates service state across trials should make its
|
|
202
|
+
* task self-contained (e.g. `drop โฆ if exists; create; migrate`) or run
|
|
203
|
+
* `trials: 1`. True per-trial reset needs an eval-loop hook and is the next
|
|
204
|
+
* increment (research/r3-disposable-services.md).
|
|
205
|
+
*
|
|
206
|
+
* @experimental โ surface may change without a major-version bump.
|
|
207
|
+
*/
|
|
208
|
+
export declare function experimental_withServices<T>(services: Readonly<Record<string, ServiceSpec>>, runtime: ContainerRuntime, fn: (session: ServiceSession) => Promise<T>): Promise<T>;
|
|
209
|
+
//# sourceMappingURL=services.d.ts.map
|
package/dist/services.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles โ R3 disposable-service tier (โ ๏ธ EXPERIMENTAL / UNSTABLE).
|
|
4
|
+
*
|
|
5
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
6
|
+
* EXPERIMENTAL: this surface is a DRAFT. Import it from `vigiles/experimental`,
|
|
7
|
+
* NOT from a stable subpath. It is NOT covered by the stability guarantee and
|
|
8
|
+
* may change shape or be removed WITHOUT a major-version bump. Do not build a
|
|
9
|
+
* production workflow on it yet. See docs/measuring-skills.md ยง Experimental.
|
|
10
|
+
* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
11
|
+
*
|
|
12
|
+
* WHAT THIS IS. The free tiers (`runHook`, `measureTriggerRate`, `runEval`)
|
|
13
|
+
* cover a skill's OUTPUT and its side-effect SAFETY (did it call / not call a
|
|
14
|
+
* tool, write / not write a file) with no container. What they do NOT do is let
|
|
15
|
+
* a skill actually PERFORM a side effect against a real service and verify the
|
|
16
|
+
* resulting state โ apply a migration to a real Postgres and check the row
|
|
17
|
+
* landed. That is the R3 rung (see research/eval-coverage-and-isolation.md): the
|
|
18
|
+
* REAL system's semantics are the thing under test, so it can't be faked.
|
|
19
|
+
*
|
|
20
|
+
* THE POSTURE. vigiles COMPOSES with a throwaway container; it does not reinvent
|
|
21
|
+
* the sandbox. A {@link ServiceSpec} declares a disposable service; the injected
|
|
22
|
+
* {@link ContainerRuntime} port starts it, and it is force-removed on teardown.
|
|
23
|
+
*
|
|
24
|
+
* โ ๏ธ SAFETY โ the isolation is the DISPOSABLE CONTAINER, nothing more. A skill is
|
|
25
|
+
* model-driven, so the MODEL chooses the actions: it can do anything the run
|
|
26
|
+
* ENVIRONMENT allows. vigiles creates and destroys the container; it does NOT, in
|
|
27
|
+
* this tier, confine the skill's filesystem or block its network. So an R3 run is
|
|
28
|
+
* only as safe as the environment you run it in:
|
|
29
|
+
* - run it in a DISPOSABLE environment โ a CI job, a throwaway container/VM, or
|
|
30
|
+
* a dev box with NO production access;
|
|
31
|
+
* - point the task at the disposable service's connection string ONLY;
|
|
32
|
+
* - keep real credentials OUT of the run (prod `DATABASE_URL`, cloud keys,
|
|
33
|
+
* `~/.ssh`) โ pair it with the eval tier's `ephemeralEnv` (throwaway HOME +
|
|
34
|
+
* cleared env) to scrub them so the model has no real keys to misuse.
|
|
35
|
+
* Treat it like running an untrusted script. A future increment adds an egress
|
|
36
|
+
* wall (the skill reaches only the model + the service); until then that job is
|
|
37
|
+
* the operator's. See docs/measuring-skills.md ยง Experimental.
|
|
38
|
+
*
|
|
39
|
+
* WHAT SHIPS TODAY vs LATER. Today: the TYPES, the {@link ContainerRuntime} port,
|
|
40
|
+
* the pure {@link experimental_startServices} / {@link experimental_withServices}
|
|
41
|
+
* orchestration, and a Docker backend (`src/services-docker.ts`). Deferred: the
|
|
42
|
+
* `runEval` / `measureArms` `services` option + `ctx.service(name)`, per-trial
|
|
43
|
+
* reset via an eval-loop hook, and the egress wall. Requires Docker (Linux-first).
|
|
44
|
+
* It is an explicit opt-in and NEVER part of `vigiles audit` (audit stays
|
|
45
|
+
* side-effect-free).
|
|
46
|
+
*
|
|
47
|
+
* @experimental
|
|
48
|
+
* @module vigiles/experimental (services)
|
|
49
|
+
*/
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.experimental_startServices = experimental_startServices;
|
|
52
|
+
exports.experimental_withServices = experimental_withServices;
|
|
53
|
+
/**
|
|
54
|
+
* Start every declared service against an injected {@link ContainerRuntime},
|
|
55
|
+
* returning a {@link ServiceSession}. Pure orchestration over the port โ no
|
|
56
|
+
* `docker` is imported here, so this is fully unit-testable with a fake runtime
|
|
57
|
+
* (the `decideSandbox`-is-pure / `runSandboxed`-is-real split). If any service
|
|
58
|
+
* fails to start, the ones already up are torn down before the error propagates,
|
|
59
|
+
* so a partial failure never leaks a container.
|
|
60
|
+
*
|
|
61
|
+
* A Docker-backed `ContainerRuntime` and the `runEval` / `measureArms` wiring are
|
|
62
|
+
* the next increment โ until then this is the primitive you compose by hand:
|
|
63
|
+
* start โ pin egress to `session.endpoints` โ run โ read `session.handles` in
|
|
64
|
+
* `measure` โ `await session.teardown()`.
|
|
65
|
+
*
|
|
66
|
+
* @experimental โ surface may change without a major-version bump.
|
|
67
|
+
*/
|
|
68
|
+
async function experimental_startServices(services, runtime) {
|
|
69
|
+
const handles = {};
|
|
70
|
+
const endpoints = [];
|
|
71
|
+
const started = [];
|
|
72
|
+
const stopAll = async () => {
|
|
73
|
+
await Promise.allSettled(started.map((h) => runtime.stop(h)));
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
for (const [name, spec] of Object.entries(services)) {
|
|
77
|
+
const handle = await runtime.start(name, spec);
|
|
78
|
+
handles[name] = handle;
|
|
79
|
+
started.push(handle);
|
|
80
|
+
// Only a service that published a port has a reachable endpoint.
|
|
81
|
+
if (handle.port !== undefined) {
|
|
82
|
+
endpoints.push(`${handle.host}:${handle.port}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
await stopAll();
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
return { handles, endpoints, teardown: stopAll };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Run `fn` with the declared services up, disposing them afterwards โ even if
|
|
94
|
+
* `fn` throws. The scope-guard form of {@link experimental_startServices}: it
|
|
95
|
+
* removes the manual `try/finally` so a `measureArms` / `measure` call can be
|
|
96
|
+
* wrapped in one line and its containers are always cleaned up.
|
|
97
|
+
*
|
|
98
|
+
* โ ๏ธ Read the SAFETY note in this module's header first โ the container is the
|
|
99
|
+
* only isolation; keep real credentials out of the run.
|
|
100
|
+
*
|
|
101
|
+
* LIFECYCLE NOTE (honest): the services live for the WHOLE `fn` โ i.e. per-RUN,
|
|
102
|
+
* not per-trial. An eval that mutates service state across trials should make its
|
|
103
|
+
* task self-contained (e.g. `drop โฆ if exists; create; migrate`) or run
|
|
104
|
+
* `trials: 1`. True per-trial reset needs an eval-loop hook and is the next
|
|
105
|
+
* increment (research/r3-disposable-services.md).
|
|
106
|
+
*
|
|
107
|
+
* @experimental โ surface may change without a major-version bump.
|
|
108
|
+
*/
|
|
109
|
+
async function experimental_withServices(services, runtime, fn) {
|
|
110
|
+
const session = await experimental_startServices(services, runtime);
|
|
111
|
+
try {
|
|
112
|
+
return await fn(session);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await session.teardown();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=services.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.4.0",
|
|
4
4
|
"description": "Lint & test the harness your AI agent runs on โ verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"./claude-code": "./dist/claude-code.js",
|
|
45
45
|
"./codex": "./dist/codex.js",
|
|
46
46
|
"./adapter": "./dist/adapter.js",
|
|
47
|
+
"./experimental": "./dist/experimental.js",
|
|
47
48
|
"./vitest": {
|
|
48
49
|
"types": "./dist/vitest.d.mts",
|
|
49
50
|
"default": "./dist/vitest.mjs"
|